
上一讲我把 Addressable 的资源构建和分组规则讲完了一遍很多朋友已经在项目里把本地加载链路跑通了。但真正把资源量做上去之后你会发现剩下一件绕不开的事资源怎么放到远端让用户下载。今天这篇就是专门聊 Addressable 资源托管服务Asset Hosting Services的从选型逻辑、核心配置、构建上传到线上版本管理和排障一条线说清楚。先说清楚使用场景。如果你的游戏做的是纯首包内资源那这篇文章基本可以不用看只要你的项目已经开始做增量更新、边下边玩、活动资源动态下发那就必须有一个远程资源入口。资源托管服务干的事情就是帮你把 Addressable 构建出来的产物放到一个可通过 URL 访问的地方同时处理好目录、版本、哈希和权限这些事让运行时可以直接拉取并缓存。1. 资源托管服务到底解决什么问题1.1 先理清谁是托管服务的用户很多开发者容易把资源托管服务和 CDN 混为一谈也确实有朋友直接用对象存储来跑远程资源。但 Addressable 的资源托管服务其实属于业务层托管它服务的不只是静态文件而是 Addressable 构建体系里的三类核心产物Catalog资源目录、Hash校验文件和 Bundle资源包本体。这三类产物对托管的要求很不一样。Catalog 决定了运行时去哪里找什么它的更新频率低但容错要求极高Hash 文件很小但更新时机必须精确Bundle 文件最大而且数量可能成百上千。通用对象存储可以存放这些文件但它不关心文件之间的关系也不提供按版本切换的语义Addressable 的托管服务在这基础上增加了与构建流程的衔接让产物上传和运行时加载路径可以自动对齐。1.2 托管服务要解决的四个痛点包体控制把远程资源从首包剥离后安装包体积能显著下降。对一个 2GB 以上的项目来说可能是 400MB 首包和 1.6GB 远程资源的区别。更新效率传统整包更新动辄重新下载几百 MB而资源托管配合 Catalog 可以做资源级更新只拉变更的 Bundle。版本不敏感资源目录和哈希文件让客户端能够识别本地缓存是否过期避免要更新但不知道该更新哪个文件的尴尬。权限与访问控制远程资源往往带有付费内容、首次曝光内容不能裸奔在公网上被随意抓取托管层需要提供基本的访问安全。1.3 什么时候适合引入资源托管服务如果你的项目满足以下任一条件基本就可以考虑接入资源总量超过 300MB、需要按月或按周版本迭代、活动内容频繁替换、有分包下载或边玩边下的需求。以某休闲游戏项目为例美术资源占了整体包体的 65%引入托管服务后首包压到 180MB 以内后续每周活动包只通过远程加载用户更新成本从重新下载安装包变成了后台静默拉取 50MB 增量次日留存数据也明显不一样了。2. 托管服务选型AAHS 与通用对象存储的差异2.1 两种托管方式的核心差别先说结论通用对象存储能跑但接入成本高Addressable 自带托管服务省事但有平台绑定。两者的差别集中在三个层面。第一是目录和哈希的同步逻辑。Addressable 构建后会生成catalog.json和对应的哈希文件运行时会先拉哈希再决定是否拉目录最后才按目录里的地址去加载 Bundle。自己用对象存储时你得手动保证这三个文件的路径关系、时效策略和缓存头都正确一旦配置错一个线上就会出现目录更新了但 Bundle 没传全的惨案。第二是上传工具链。AAHS 在构建阶段可以联动直接把BuildRemoteCatalog之后的结果同步到托管空间用通用对象存储则要自己写上传脚本、做差异比对、处理失败重传。这个差异在几次版本迭代后会被放大我见过有项目上传脚本没人维护每次发版本都要手动下载三个文件再手工传出错的概率非常高。第三是访问控制与域名策略。托管服务对下载域名、缓存路径有统一管理对象存储服务商的配置分散而且命名风格各异每换一个服务商就要重新研究权限模型、跨域配置和 CDN 加速策略。2.2 直接讲成本视角的选型建议很多中小团队会被对象存储的低价吸引觉得比 AAHS 便宜。但算账不能只看单价要把人力成本、运维成本和事故成本加进去。一次线上更新事故导致的用户流失抵得上大半年的托管费用差异。以我自己的经验看团队没有专职运维、版本节奏快的优先选托管服务团队有成熟运维平台、想彻底掌控资源分发链路的可以自建对象存储加 CDN。还有一种折中方案托管服务管开发环境生产环境自建存储两套环境共用一套构建脚本通过LoadPath切换。2.3 一张表看懂方案对比对比维度内置资源通用对象存储资源托管服务接入成本低中高中与构建流程联动无需自研脚本原生衔接目录/哈希管理内置手动维护自动管理版本回溯不适用依赖额外逻辑便捷团队运维要求无高低长期成本包体膨胀可议价相对标准化3. 接入实操从开通到完成第一次远程更新3.1 开通服务与前置准备开通资源托管服务前先把工程侧的 Addressable 版本升到 1.20 以上低版本的目录结构和托管接口差异很大后面对齐流程会很痛苦。开通后你会拿到一个远程根地址类似https://yourhost/{project}/这个地址稍后会填到 Addressable 的RemoteLoadPath里。同时要建立项目内部约定开发环境、预发布环境、生产环境必须使用相互独立的托管空间。我用过一套空间跑三种环境的方式结果开发分支的调试目录覆盖了生产目录用户端出现了半小时的资源错乱。后来改成三套空间、通过构建参数切换路径问题再没出现过。3.2 分组设置与远程构建路径在 Addressable Groups 面板中选中需要远程加载的分组把Build Path和Load Path都设为远程相关变量。示例配置如下Build PathRemote.BuildPath默认对应ServerData/{BuildTarget}/Load PathRemote.LoadPath默认对应以托管地址为前缀的 URL 模板关键在于理解这两个路径的作用Build Path 是本地构建输出目录资源打出来先落到这里Load Path 是运行时用于拼接远程 URL 的模板。两者不要求同一个地址但内容必须对应否则构建产物在本地运行时永远拉不到。配置完成后分组里Advanced Options还要检查Build Asset Bundles和Use Asset Bundles这类开关确保该分组是参与远程构建而不是被忽略的。3.3 构建上传的标准流程我的标准构建流程分四步切到目标平台执行Addressables - Build - New Build - Default Build Script生成包含远程 Bundle 的构建产物。到本地ServerData/{BuildTarget}目录检查产物包括catalog.json、哈希文件以及各 Bundle 文件。将ServerData/{BuildTarget}下的文件同步到托管空间保持目录结构一致。用一台测试设备清掉本地缓存执行加载 Remote 资源的逻辑确认日志里出现 HTTP 请求且无校验失败报错。步骤 3 最容易被忽略的是目录一致性。托管空间的根目录里应当是catalog.json和 Bundle 文件不能把上一级项目目录也传上去作为前缀否则运行时生成的 URL 会多一段路径直接 404。提示如果你发现构建产物里有多个catalog相关文件别急着删先确认项目是否开启了优化目录大小之类的选项它会改变 Hash 的生成方式。3.4 一键上传脚本的思路对象存储和托管服务都提供了各自的 CLI 或 API建议在构建机里放一套发布脚本流程大体是# 伪代码示意 # 1. 触发 Addressable 构建 # 2. 进入 ServerData/{BuildTarget} # 3. 上传所有文件到远程托管空间的对应版本目录 # 4. 默认保留最近 3 个版本过期版本自动清理脚本的价值不在于省那几十秒而在于让每个版本的操作步骤可审计、可回滚。团队里只要有一个人手工传文件早晚会出现传了 Bundle 忘传目录的情况。脚本里建议把哈希文件的本地内容打印出来方便和人沟通时快速确认版本是否落地。4. 版本、缓存与更新策略线上运营的关键4.1 理解 Catalog 的版本切换机制Addressable 的远程加载不是简单的文件存在就下载它的核心流程是客户端启动时读取本地已缓存的 Hash 文件向远程地址请求新的 Hash 文件对比内容哈希不一致时下载新的 Catalog按 Catalog 中的 URL 逐一下载缺失或更新的 Bundle。当过了一段时间你更新了资源并重新上传旧的客户端通过哈希对比发现自己需要更新就会重新拉取目录和变更 Bundle。这个机制让热更新可以在不替换代码的情况下完成资源层面的升级。有个细节是Hash 文件本身很小它的缓存时间要尽量短否则客户端拿不到最新版本判断依据。线上排查时如果发现客户端一直报已是最新版本先看托管空间里 Hash 文件的响应头是否被 CDN 或其他层缓存了过长时间。4.2 资源级更新的正确姿势我之前遇到过一个很典型的错误做法每次都把整个ServerData目录无脑上传整个包全部覆盖。虽然短时间看起来一切正常但因为 Bundle 的哈希发生改变客户端会把所有远程资源重新下载一遍更新体验和整包更新没有区别。正确做法是在构建前启用Check for Content Update Restrictions或者采用带版本标记的目录组织方式。这里有一个关键认知远程资源的粒度取决于你在分组里的资源拆分而不是托管服务本身。分组拆得越细更新影响面越小但也要控制 Bundle 数量几万个 Bundle 会让目录体积失控。操作上我会做两件事构建产出后用脚本对比上一个版本的catalog输出发生变更的 Bundle 列表上传时先传 Bundle再传新的 Catalog最后更新 Hash 文件。这个顺序能尽量避免客户端拉到新目录但 Bundle 还没传完的中间状态。4.3 缓存策略与本地缓存管理托管服务要兼顾远端和本地两层缓存。远端的 CDN 层会按响应头缓存资源本地 UnityWebRequest 也有自己的缓存行为。一般对 Bundle 文件希望缓存较长时间对 Hash 文件希望缓存极短时间甚至不缓存。这个诉求和托管服务的配置有关也和构建脚本中是否能注入响应头有关。本地缓存的管理则用到 Addressable 自身的缓存 API可以按版本号主动清理旧缓存。实盘项目里我会在登录跳转后执行一次轻量缓存清理避免用户长时间不清理磁盘导致异常告警。清理时注意不要清掉正在加载中的 Bundle否则可能触发资源释放异常。常见做法每次 App 大版本号变化时清空 Addressable 的全部缓存小迭代只按哈希差异更新不清理缓存。这个策略简单有效能避免很多老版本加载残留资源的问题。5. 排障记录与长期维护经验5.1 高频问题速查表现象可能原因处理思路运行时加载 URL 404LoadPath 模板与托管空间目录不一致核对远程根地址和实际目录拼接结果一直提示已是最新版本Hash 被缓存调整缓存头强制校验一次更新后出现资源花屏或类型错乱Bundle 上传顺序错误先传 Bundle 再传 Catalog最后传 Hash部分用户更新失败、其他人正常CDN 节点缓存脏数据刷新对应 URL 或降低资源缓存时间新版本上传后旧版本无法启动旧版和新的 Catalog 不兼容保留每个版本独立目录或做兼容性判断5.2 一个我踩过不止一次的坑目录漂移所谓目录漂移就是本地构建时的路径参数和托管空间里的实际目录不一致导致客户端拼接出的 URL 是对的但服务端没有对应文件。有一次我在改分组配置时把LoadPath模板里的版本号写成了手写固定值构建三次版本后才发现三个版本的 URL 全部指向同一个目录前面的产物被覆盖掉了。排查方式是看日志里的完整 URL再手动 curl 该 URL 检查响应码这一步建议做成发布脚本的强制自检项。后来我在构建流程里固定把LoadPath打印到日志文件每次发版前都要核对一次杜绝了这类低级问题。5.3 配额、监控与备份托管资源不是无限空间尤其一段时间内的版本迭代会产生大量历史 Bundle 文件需要建立清理策略。我一般保留最近三个稳定版本以及一个回滚版本更早的产物归档到冷存储不是直接删以防万一。监控方面一定要看两个指标更新失败率和平均下载耗时。前者反映托管配置或网络问题后者直接关联资源体积和并发量。我还习惯做一次弱网演练在测试环境下故意延迟或限速看 Addressable 在加载超时后的表现很多线上问题都是这里先暴露出来的。最后版本回滚别等到出事故了才想。我比较推荐的做法是每次上传前先记录上一版本所有文件的 URL 列表一旦新版本出了问题通过托管服务把 URL 列表指回上一版本。这里的核心是不要覆盖原始版本目录而是保留版本独立的目录结构这样回滚成本几乎为零。资源托管服务本身并不复杂真正复杂的是它在构建、上传、缓存、更新和回滚这条链路中的位置。挑一个周末把工程里的非核心资源拆出一个远程分组走一遍完整流程比你读十篇理论文章都有用。等你把它玩顺手了你会发现后续做分包下载、分平台资源和运营内容推送整个思路都是通的。