1. 资源管理框架的架构总览为什么值得单独拎出来讲做过Unity项目的人大概都有这种体会项目前期资源随便放Resources文件夹一塞Load一把梭跑得挺欢。等到项目中期美术资源破千包体膨胀到几个G加载卡顿、内存泄漏、热更困难这些问题就全冒出来了。这时候再回头重构资源管理成本高得吓人。所以一个靠谱的资源管理框架它的架构设计必须在项目启动阶段就想清楚而不是等到出问题了再打补丁。YooAsset这个资源管理框架这两年在Unity圈子里讨论度很高经常被拿来和Addressable做对比。我从它早期版本开始用到现在经历了几个正式项目的完整生命周期对它整体架构的理解也经历了一个从“会用API”到“理解设计意图”的过程。这篇内容就围绕YooAsset的整体架构展开把Editor侧和Runtime侧的分工、资源清单的设计、以及几个核心模块之间的协作关系讲透。不管你是刚接触YooAsset想搞清楚它内部怎么转的还是已经在用但想深入理解架构以便做定制扩展这篇应该都能给你一些参考。需要提前说明的是YooAsset的版本迭代比较快不同版本之间API和内部实现有差异。我下面讲的内容主要基于2.x版本的架构设计部分细节在1.x上可能对不上建议对照你实际使用的版本来看。另外架构层面的东西光看文档容易浮在表面我会尽量结合实际的代码路径和操作场景来讲让每个模块的职责都能落到具体的地方。2. 整体架构分层与核心模块职责拆解2.1 Editor侧与Runtime侧的边界划分YooAsset的架构第一刀就切在Editor和Runtime的边界上。这个划分看起来理所当然但实际做的时候很多框架会切不干净导致Runtime代码里混入Editor的依赖打包的时候报一堆编译错误。YooAsset在这块的处理比较干净它的核心思路是Editor侧负责资源的收集、构建、清单生成Runtime侧只负责根据清单去加载和卸载资源两边通过一个序列化后的资源清单文件来通信。Editor侧的核心入口是AssetBundleCollector和AssetBundleBuilder这两个模块。AssetBundleCollector管的是“哪些资源要被打进哪个包”它通过一个可配置的收集器规则来工作你可以按文件夹、按标签、按显式列表来指定资源的打包方式。AssetBundleBuilder管的是“怎么把这些包构建出来”包括构建参数配置、依赖分析、增量构建、清单文件生成这一整套流程。Runtime侧的核心入口是YooAssets这个静态类它相当于整个运行时资源管理的大门。通过它你可以初始化资源包、创建资源下载器、加载资源、卸载资源。Runtime侧不关心资源是怎么打的包它只认清单文件里描述的资源定位信息。这种设计的好处是职责清晰Editor侧的改动不会影响Runtime的稳定性Runtime侧的代码也可以做到极简减少出问题的概率。注意Editor侧和Runtime侧共享的数据结构比如清单文件的反序列化类要放在一个两边都能访问的程序集里但又不能引入Editor命名空间。YooAsset的做法是把这些共享类放在Runtime程序集里Editor侧通过引用Runtime程序集来使用。你自己做扩展的时候也要注意这个边界别把Editor的代码写到Runtime程序集里。2.2 资源清单整个架构的信息中枢资源清单Manifest是YooAsset架构里最核心的数据结构它相当于一份“资源地图”Runtime侧所有的加载操作都要先查这份地图。清单文件里记录了每个资源的定位地址、所属的资源包、依赖关系、以及资源包的哈希值和大小等信息。清单的生成发生在构建阶段。AssetBundleBuilder在打完所有资源包之后会遍历所有收集到的资源为每个资源生成一条记录同时分析资源之间的依赖关系把依赖信息也写进清单。最终生成的清单文件会被序列化成二进制格式放到StreamingAssets目录下随包发布或者放到CDN上供热更使用。Runtime侧在初始化的时候会先加载清单文件把它反序列化成内存中的数据结构。之后每次加载资源都是先通过资源的定位地址在清单里查到它属于哪个包然后检查这个包是否已经加载如果没加载就先加载包再从包里加载具体的资源。依赖关系也是在查清单的时候一并处理的比如你加载一个预制体清单里记录了它依赖哪些材质和贴图Runtime会自动把这些依赖也加载进来。清单文件的设计直接影响了资源加载的效率和灵活性。YooAsset的清单支持按资源包粒度进行增量更新也就是说热更的时候不需要全量替换清单只需要下载有变化的资源包对应的清单片段。这个设计在大型项目里很关键否则每次热更都下载完整清单光清单文件本身就可能好几兆。2.3 资源包运行时的生命周期管理资源包在Runtime侧的生命周期管理是YooAsset架构里比较精妙的一块。一个资源包从被加载到被卸载中间经历了几个状态未加载、加载中、已加载、卸载中。YooAsset通过引用计数来管理这些状态确保资源包不会在还有人在用的时候被卸载也不会在没人用的时候一直占着内存。具体来说当你通过YooAssets.LoadAssetAsync加载一个资源时框架会先找到这个资源所属的资源包然后对这个包做一次“引用计数加一”的操作。如果这个包还没加载就先触发包的加载流程。当资源使用完毕调用UnloadAsset或者释放资源句柄时框架会对包做“引用计数减一”。当引用计数归零并且没有其他包依赖这个包时这个包才会被真正卸载。这里有个细节值得注意YooAsset区分了“资源包引用计数”和“资源对象引用计数”两个层级。资源包级别的引用计数管的是包的加载和卸载资源对象级别的引用计数管的是具体资源实例的创建和销毁。这两个层级的分离让框架可以做到“包还在内存里但里面的某个资源对象已经被销毁”这种细粒度的控制。实操心得在实际项目里我建议对资源包的卸载策略做一层封装不要直接依赖框架的自动卸载。因为自动卸载的时机不好控制有时候你刚卸载完一个包下一帧又要加载同一个包里的另一个资源结果又得重新加载一遍。我的做法是在游戏逻辑层维护一个“资源包使用白名单”在场景切换或者特定时机手动触发卸载这样能避免频繁的加载卸载抖动。2.4 下载器的分层设计YooAsset的下载器模块采用了分层设计从下到上依次是文件下载器、资源包下载器、资源下载器。文件下载器负责最底层的文件传输支持断点续传和并发下载。资源包下载器在文件下载器之上负责管理一批资源包的下载任务包括依赖检查、下载顺序编排、失败重试等。资源下载器在最上层对外提供“下载某个资源”的接口内部会自动解析依赖并调用资源包下载器。这种分层设计的好处是每一层只关心自己的职责。文件下载器不需要知道资源包的概念它只管把文件从远端拉到本地。资源包下载器不需要关心具体资源的依赖关系它只管把一批包下载完整。资源下载器不需要关心文件传输的细节它只管根据资源定位地址找到对应的包并触发下载。下载器的状态机设计也值得说一下。每个下载器都有明确的状态流转None、Waiting、Downloading、Succeed、Failed。状态之间的转换有严格的约束比如不能从None直接跳到Downloading必须先经过Waiting。这种严格的状态管理避免了并发操作导致的状态混乱在多人协作的项目里尤其重要。3. 核心模块的协作流程与关键实现细节3.1 从资源收集到清单生成的完整链路资源收集是构建流程的起点。YooAsset通过AssetBundleCollectorSetting这个配置资产来管理收集规则。你可以在Editor里打开收集器配置窗口添加收集器分组每个分组里可以配置多条收集规则。每条规则指定了收集的路径、收集方式按文件夹、按文件、按标签、以及打包规则打包到一个包、按文件夹打包、按标签打包等。收集规则配置好之后AssetBundleCollector会遍历所有规则把符合条件的资源收集到一个临时的数据结构里。这个过程中会做几件事检查资源是否重复收集、检查资源类型是否合法、计算资源的定位地址。定位地址是Runtime侧加载资源时用的唯一标识默认是资源的AssetPath你也可以通过自定义定位规则来修改。收集完成后进入构建阶段。AssetBundleBuilder首先会做依赖分析找出资源之间的引用关系。这个依赖分析是基于AssetDatabase的接口做的它会遍历每个资源的依赖项把依赖关系记录到一张依赖图里。依赖图的作用是决定哪些资源应该被打到同一个包里以及包与包之间的依赖关系。依赖分析完成后Builder会根据打包规则把资源分配到不同的资源包。分配的时候会考虑几个因素资源的大小、资源的类型、资源的加载频率。比如频繁加载的资源适合单独打小包大纹理适合按文件夹打包共享的材质和Shader适合打到一个公共包里。这些策略没有绝对的对错要根据项目的实际情况来调整。最后是清单生成。Builder会为每个资源包生成一条记录包含包的名称、哈希值、大小、包含的资源列表。同时会生成资源定位表记录每个资源的定位地址到资源包的映射关系。清单文件最终会被序列化成二进制格式输出到构建目录。3.2 运行时初始化与资源加载的调用链Runtime侧的初始化从YooAssets.Initialize开始。这个调用会创建一个ResourcePackage实例并初始化一些全局状态。之后你需要调用package.InitializeAsync来加载清单文件。清单文件的加载路径取决于你的运行模式单机模式从StreamingAssets加载联机模式从CDN加载WebGL模式从WebRequest加载。初始化完成后就可以通过package.LoadAssetAsync来加载资源了。这个方法的内部调用链大致是这样的首先通过资源定位地址在清单里查到资源所属的包然后检查这个包是否已经加载。如果没加载就触发包的加载流程包括从本地缓存或远端加载包文件、校验哈希值、加载AssetBundle对象。包加载完成后从AssetBundle里加载具体的资源对象同时处理依赖关系。依赖关系的处理是加载流程里比较绕的一块。当你加载一个资源时框架会先查这个资源的依赖列表然后递归地加载所有依赖资源。依赖资源加载完成后才会真正加载目标资源。这个过程是异步的框架通过一个操作队列来管理加载顺序确保依赖资源先于目标资源完成加载。注意依赖资源的加载也会增加对应资源包的引用计数。所以当你卸载一个资源时框架会自动减少它依赖的资源包的引用计数。这个机制保证了依赖资源不会被提前卸载但也意味着如果你加载了一个依赖链很长的资源可能会一次性加载很多个包。在内存敏感的场景下要特别注意这一点。3.3 资源卸载与内存回收的时机控制资源卸载是资源管理里最容易出问题的地方。卸载早了会导致资源丢失卸载晚了会导致内存泄漏。YooAsset提供了几种卸载方式UnloadAsset卸载单个资源对象UnloadUnusedAssets卸载所有未使用的资源对象UnloadAllAssets卸载所有资源对象。UnloadAsset是最细粒度的卸载它只减少指定资源的引用计数当引用计数归零时销毁资源对象。但资源对象销毁了它所属的资源包不一定被卸载因为包里可能还有其他资源在使用。资源包的卸载需要等到包级别的引用计数归零。UnloadUnusedAssets会遍历所有已加载的资源对象把引用计数为零的资源对象销毁掉。这个方法适合在场景切换或者内存压力大的时候调用。但要注意它不会卸载资源包只是销毁资源对象。资源包的卸载还是需要依赖引用计数的自然归零。UnloadAllAssets是强制卸载所有资源包括资源对象和资源包。这个方法一般只在游戏退出或者需要彻底重置资源状态的时候用。在正常游戏流程里不建议频繁调用因为它会导致所有资源重新加载性能开销很大。实操心得我在项目里通常会做一个“资源卸载管理器”在场景切换的时候收集当前场景不再使用的资源包列表然后延迟几帧再触发卸载。延迟的目的是避免场景切换过程中还有异步加载操作在进行导致卸载了正在加载的资源。这个延迟时间一般设2到3帧就够了太长了会浪费内存太短了可能来不及。3.4 热更新流程与版本管理机制热更新是YooAsset架构里比较重要的一个能力。它的热更流程大致是这样的游戏启动时先请求远端的版本文件对比本地版本如果版本不一致就下载新的清单文件。然后对比新旧清单找出有变化的资源包下载这些包。下载完成后用新的清单替换旧清单后续加载就使用新清单。版本管理是通过一个版本号来控制的。每次构建资源包时Builder会生成一个版本号这个版本号可以手动指定也可以根据构建时间自动生成。版本文件里记录了当前版本号、清单文件的哈希值、以及所有资源包的哈希值和大小。Runtime侧通过对比本地版本文件和远端版本文件来判断是否需要更新。热更的粒度可以控制到单个资源包。也就是说如果只有一个资源包的内容变了热更的时候只需要下载这一个包不需要全量更新。这个粒度控制是通过对比新旧清单里每个资源包的哈希值来实现的。哈希值变了就说明包的内容变了需要下载。注意热更的时候要特别注意清单文件的兼容性。如果新旧清单的格式不一致比如你升级了YooAsset版本导致清单结构变了直接替换清单可能会导致Runtime解析失败。这种情况下需要做全量更新或者做一个清单格式的迁移逻辑。我在项目里遇到过因为升级框架版本导致热更后资源加载失败的情况排查了半天才发现是清单格式不兼容。4. 架构设计中的取舍与常见问题排查4.1 为什么选择清单驱动而不是目录扫描YooAsset选择清单驱动的方式而不是像Resources那样做目录扫描这个取舍背后有很实际的考虑。目录扫描的方式在Editor下很方便但在Runtime下有几个致命问题第一目录扫描需要遍历文件系统在移动端上性能很差第二目录扫描无法处理资源依赖你不知道一个资源依赖了哪些其他资源第三目录扫描无法做增量更新因为文件系统里没有版本信息。清单驱动的方式把所有这些信息都提前计算好Runtime只需要查表就行。查表的性能远高于目录扫描而且依赖关系、版本信息、包大小这些数据都是现成的。代价是构建流程更复杂需要额外维护清单文件。但对于中大型项目来说这个代价是值得的。4.2 资源包粒度划分的常见误区资源包粒度划分是使用YooAsset时最容易踩坑的地方。我见过不少项目把所有资源打成一个包结果热更的时候随便改一个资源就要下载整个包包体好几个G热更体验极差。也见过把每个资源都打成单独的包结果包数量上千加载的时候频繁打开AssetBundle性能反而更差。合理的粒度划分应该考虑几个因素资源的更新频率、资源的加载频率、资源的大小、资源之间的依赖关系。更新频率高的资源适合单独打小包更新频率低的资源可以合并成大包。加载频率高的资源适合单独打小包以便快速加载加载频率低的资源可以合并。大纹理、大模型适合按文件夹打包小配置、小图标适合合并到一个公共包。实操心得我的经验是先把资源按业务模块划分成几个大组每个大组内部再按更新频率和加载频率细分。比如UI资源按界面划分每个界面的图集打一个包场景资源按场景划分每个场景的模型和纹理打一个包公共资源单独打一个包。这样既不会太碎也不会太大热更的时候也能精确到具体模块。4.3 常见报错与排查思路速查在使用YooAsset的过程中有几个报错比较常见我整理了一个速查表报错信息可能原因排查思路资源定位地址找不到清单里没有这个资源或者定位地址写错了检查收集器配置里是否包含了这个资源检查定位规则是否匹配资源包加载失败包文件损坏、哈希校验不通过、CDN路径错误检查包文件是否完整检查CDN配置检查哈希值是否匹配依赖资源加载失败依赖资源没有被正确收集或者依赖关系分析有误检查依赖资源的收集规则检查依赖分析日志内存泄漏资源包引用计数没有归零或者资源对象没有释放用内存分析工具检查资源包和资源对象的引用计数热更后资源加载异常清单文件不兼容或者新旧包混用检查清单版本检查是否有残留的旧包文件排查资源加载问题的时候我一般会先打开YooAsset的日志开关看详细的加载日志。日志里会记录每个资源的加载路径、所属的包、依赖关系、加载耗时等信息。大部分问题看日志就能定位到。如果日志不够还可以用Unity的Profiler看AssetBundle的内存占用和加载耗时进一步定位性能问题。4.4 与Addressable的架构差异对比经常有人问YooAsset和Addressable怎么选。这两个框架在架构上有一些本质差异。Addressable是Unity官方推出的和Unity的编辑器集成更紧密支持Addressable Asset System的图形化配置。YooAsset是社区驱动的更轻量API更简洁对国内项目的适配更好比如支持微信小游戏、支持国产化的CDN服务。从架构上看Addressable的清单文件格式更复杂支持更多的元数据但也导致清单文件更大。YooAsset的清单更精简加载更快。Addressable的依赖分析是基于AssetDatabase的YooAsset也是但YooAsset在依赖分析上做了一些优化比如支持循环依赖检测。另一个差异是热更策略。Addressable的热更粒度可以到单个资源YooAsset的热更粒度是资源包。这意味着Addressable的热更更精细但清单文件也更大热更时的清单对比开销更高。YooAsset的粒度粗一些但清单小对比快适合资源包数量不是特别多的项目。注意选框架的时候不要只看功能列表要看项目的实际需求。如果项目是微信小游戏YooAsset的适配更好如果项目是主机平台Addressable的官方支持更完善。如果团队里没人熟悉这两个框架YooAsset的上手成本更低一些文档和社区案例也更多。5. 架构扩展与定制化的实操建议5.1 自定义资源定位规则的实现方式YooAsset默认的资源定位规则是用AssetPath作为定位地址也就是资源的完整路径。这个规则在大多数情况下够用但在一些场景下需要定制。比如你想用资源的文件名作为定位地址或者你想给资源加一个业务前缀这时候就需要自定义定位规则。自定义定位规则需要实现ILocationRule接口然后在收集器配置里指定使用这个规则。接口里主要实现两个方法GetLocation和GetRootPath。GetLocation负责把资源的AssetPath转换成定位地址GetRootPath负责返回资源的根路径。实现的时候要注意定位地址的唯一性不能有两个资源映射到同一个定位地址。我在项目里做过一个自定义规则把资源的定位地址改成“模块名/资源名”的格式。这样在代码里加载资源的时候只需要写模块名和资源名不需要写完整的路径。这个规则实现起来不复杂但要注意处理资源名冲突的情况比如两个模块下有同名资源需要在定位地址里加上模块前缀来区分。5.2 构建流程的自动化与CI集成YooAsset的构建流程可以通过代码调用来实现自动化。AssetBundleBuilder提供了Run方法你可以在Editor脚本里调用它来触发构建。构建参数可以通过AssetBundleBuilderSetting来配置包括构建模式、输出路径、压缩格式、加密方式等。把构建流程集成到CI里可以实现自动打包和自动热更。我的做法是写一个Editor脚本在CI的构建步骤里调用。脚本里先设置好构建参数然后调用AssetBundleBuilder.Run构建完成后把输出目录上传到CDN。同时生成一个版本文件记录本次构建的版本号和清单哈希值。实操心得CI集成的时候要注意构建缓存的问题。YooAsset支持增量构建但增量构建依赖本地的构建缓存。在CI环境里每次构建都是全新的环境没有缓存所以每次都是全量构建。如果项目资源很多全量构建的时间会很长。我的做法是在CI里缓存构建目录每次构建前先恢复缓存这样能利用增量构建加速。但要注意缓存的失效策略如果收集器配置变了缓存可能就不准了需要清空重建。5.3 运行时资源加载的性能优化点资源加载的性能优化有几个方向减少加载次数、减少加载大小、减少加载等待。减少加载次数可以通过合并资源包来实现把频繁一起加载的资源打到同一个包里这样一次加载就能拿到多个资源。减少加载大小可以通过压缩资源来实现YooAsset支持LZ4和LZMA两种压缩格式LZ4压缩率低但解压快LZMA压缩率高但解压慢要根据资源的类型来选择。减少加载等待可以通过预加载来实现。在场景加载的时候提前把场景里会用到的资源加载好这样进入场景后就不需要等待了。YooAsset提供了PreDownloadContent接口来做预下载你可以在Loading界面调用这个接口把后续场景需要的资源包提前下载到本地。还有一个优化点是资源包的加载顺序。如果多个资源包之间有依赖关系加载的时候要确保被依赖的包先加载。YooAsset的依赖分析会自动处理这个顺序但如果你手动管理加载就要注意这个顺序。我一般会在加载前先查一下依赖图把依赖包排到前面。5.4 多包管理与分布式资源架构的衔接对于大型项目单个资源包可能不够用需要做多包管理。YooAsset支持创建多个ResourcePackage实例每个实例可以有自己的清单文件和加载策略。比如你可以把游戏本体资源放在一个包里把DLC资源放在另一个包里把活动资源放在第三个包里。每个包独立更新互不影响。多包管理的关键是包之间的依赖关系。如果DLC资源依赖了本体资源那么加载DLC资源的时候要确保本体资源已经加载。YooAsset的多包依赖是通过清单里的依赖信息来处理的但跨包的依赖需要你手动管理。我的做法是在DLC包的清单里记录它依赖的本体包版本加载DLC之前先检查本体包的版本是否匹配。分布式资源架构是更进一步的扩展把资源分散到多个CDN节点上根据用户的地理位置选择最近的节点。这个在YooAsset层面没有直接支持但可以通过自定义下载器来实现。你可以实现一个IRemoteServices接口在里面根据用户IP选择CDN节点然后把下载请求路由到对应的节点。这个实现起来有一定复杂度适合有专门运维团队的大型项目。注意多包管理和分布式架构都会增加系统的复杂度不是所有项目都需要。如果项目资源量不大用户分布不广单包加单个CDN节点就够了。过度设计反而会增加维护成本和出问题的概率。我见过一些中小型项目上来就搞多包分布式结果光是包之间的依赖管理就搞得焦头烂额得不偿失。5.5 版本升级时的架构兼容性处理YooAsset的版本迭代比较快升级版本的时候要注意架构兼容性。主要关注几个点清单文件格式是否变化、API是否有破坏性变更、构建流程是否有调整。升级前建议先在测试环境验证确认热更流程和资源加载都正常。如果清单格式变了热更的时候需要做全量更新不能增量更新。因为旧版本的Runtime无法解析新格式的清单新版本的Runtime也无法解析旧格式的清单。这种情况下需要在版本文件里加一个格式版本号Runtime根据格式版本号来决定是否支持增量更新。API的破坏性变更一般会在Release Notes里说明升级的时候要仔细看。常见的变更包括方法签名调整、类名重命名、配置项增减等。如果项目里对YooAsset做了深度定制升级的时候要特别注意自定义代码是否还能正常工作。我在项目里升级YooAsset版本的时候一般会先在分支上做升级跑一遍完整的构建和热更流程确认没问题再合并到主干。升级后要重点测试几个场景首次安装的资源加载、热更后的资源加载、多包场景下的资源加载、以及资源卸载后的内存回收。这几个场景覆盖了资源管理的主要路径能过的话基本就没大问题了。