← ALL ARTICLES

资源依赖与异步加载

全库目录221
本文目录14

打开一个场景,资源文件已经读完,画面却仍要等纹理上传和对象激活。加载入口背后还有共享依赖、解压、反序列化与发布阶段,各阶段的线程和寿命不同。我们先把依赖解析成图,再按阶段调度,才能说明什么时候资源真正可用、取消请求以后谁来回收它。

依赖与分包

资源依赖图

Prefab 可能依赖 Mesh、Material、Texture、Animation 和其他 Prefab。打包系统需要把它们变成有向依赖图。

加载一个入口资源时,系统必须先保证依赖可用。卸载时,也要确认没有其他资源继续引用共享依赖。

依赖图构建

构建从入口资源开始做引用遍历:每个资源解析出自己的直接引用列表,递归展开成图。两个工程要点:

环的处理。资源引用可以形成环。若依赖表示“必须先完成才能创建”,拓扑排序无法完成就说明存在加载死锁,需要报告具体环。若只是对象互相引用,可先创建壳,再解析引用,或把强连通分量作为一组发布。依赖图必须先说明边的含义,才能决定拆环还是分阶段解析。

确定性。同一批输入必须产出同一份图。遍历顺序、哈希表迭代、并行收集都可能引入不确定性,导致两次构建的包体内容相同但布局不同,破坏增量更新与缓存。输出前按稳定键排序是最便宜的做法。

只有逐个加载资源后才发现下一级依赖时,深链才形成多轮串行发现。构建 manifest 预先保存依赖闭包,就能同时发起独立 IO;最终发布仍受真实依赖约束。先区分发现链与执行链,再决定压平包布局还是并行调度。

共享依赖与分包

场景A、B都引用纹理T。若两份场景包各自内嵌T,下载与驻留可能重复;把T放入共享包,则两份入口复用同一依赖。但共享包还会改变卸载粒度:B仍在使用T时,退出A不能卸载它。

包因此按同时使用、同时卸载和更新频率划分。把所有纹理集中进一个包,可能让A只需一张纹理却挂载大量无关资源;把每张都独立,又会增加文件、元数据和请求数量。对一次场景加载导出实际包闭包,再对单张纹理更新计算失效集合,便能看到下载量与更新耦合,而不是仅凭目录结构判断分包。

加载框架与对象管理

Unity AssetBundle

AssetBundle 保存平台相关的序列化资源。构建会生成 Bundle 和依赖 Manifest/Hash。

需要区分:

  • 加载 Bundle 文件;
  • 从 Bundle 反序列化 Asset;
  • 实例化 GameObject;
  • 释放 Asset 引用;
  • 卸载 Bundle 容器。

这些步骤的 CPU、IO 和内存成本不同。

Unity 6000.0中,Unload(false)释放Bundle容器数据并保留已加载对象,之后不能继续从该容器加载;Unload(true)同时销毁从Bundle加载的对象,场景中的引用可能因此失效。引用计数必须覆盖仍被实例使用的资源,不能只按容器是否关闭判断可用性。

Addressables的所有权

Addressables用地址或标签从目录定位资产,再解析依赖、下载或命中缓存,最终返回异步句柄。目录描述的是这一版本中逻辑ID到包与依赖的映射;句柄保存的是调用者对本次加载结果的所有权。地址相同而目录版本不同,可能定位到不同内容,因此请求记录要固定目录与内容哈希。

Addressables 1.21.21文档要求每次显式加载都有对应释放。资源引用归零与内存立即下降不同:同一Bundle中的其他资源仍在使用时,Bundle可能继续驻留。分组因此同时影响下载范围和可释放粒度。

异步阶段与运行时发布

异步加载阶段

“异步”可能只覆盖部分过程:

  1. 网络下载;
  2. 磁盘读取;
  3. 解压;
  4. 反序列化;
  5. GPU Upload;
  6. 主线程对象创建和激活;
  7. Shader/PSO 首次使用。

文件读取在后台完成,不代表实例化和 GPU Upload 没有主线程尖峰。

加载瀑布的定量看法

把各阶段看成流水线上的工位:稳定批量吞吐受最慢阶段限制,单个请求的延迟仍包含各阶段工作和排队时间。把速率换算成同一种压缩前或压缩后字节单位后,如果读取为 200 MB/s、解压为 100 MB/s,稳定吞吐上限才约为 100 MB/s,读得再快也只会堆积在解压队列里。

队列长度是这条链路的隐藏指标。中间队列无限增长表现为内存持续上涨;队列过短则上游频繁空等。每段记录输入输出速率和队列深度,比只看端到端时间更能定位瓶颈。

发布阶段要遵守目标对象API的线程要求。Unity场景对象的创建与激活通常需要主线程入口,框架自有的纯数据对象则可以在工作线程构建,再在同步点发布。把确实需要主线程的部分按帧预算分摊,才能控制加载完成时的尖峰;预加载安排的是执行时间,激活工作量仍需要单独测量。

预加载与版本发布

预加载把下一关的读取、解压和上传放到结算界面,并不消除这些工作。当前关和下一关同时驻留时,峰值内存可能高于任一单独场景;后台无限加速读取,还会把数据堆在解压队列里。按队列容量向上游施加背压,并为主线程激活设置预算,才能把延迟换成可控制的重叠成本。

请求还必须固定目录版本。若读取了新Catalog,却下载旧Bundle,逻辑ID可能解析成功但内容不匹配。更新先准备并校验完整依赖集合,再切换一个版本入口;新请求使用新目录,已运行请求继续持有其版本直到结束。缓存按内容哈希区分,代码对资源格式版本执行兼容或拒绝。发布的通用协议见 资产构建与发布。

异步加载状态机

下面是C++风格的阶段伪代码,输入逻辑资产ID,输出异步资源句柄。各阶段分别交给IO、工作、渲染与主线程队列;CreateGpuResources仅表示创建与提交上传,实际可用还要满足GPU同步和依赖完成条件。Future、解码类型与取消协议由宿主提供:

cpp
Future<AssetHandle> LoadAsset(AssetId id)
{
    return ResolveManifest(id)
        .Then(IoQueue, [](Location p) { return ReadAsync(p); })
        .Then(WorkerQueue, [](Bytes b) { return DecompressAndDeserialize(b); })
        .Then(RenderQueue, [](DecodedAsset a) { return CreateGpuResources(a); })
        .Then(MainQueue, [](GpuAsset a) { return PublishHandle(a); });
}

每一阶段都要传递取消、错误和依赖信息。取消请求不能立即释放 GPU 正在使用的中间资源,引用计数也要区分加载请求和已发布句柄。验证时给 IO、解压、上传和主线程发布分别打时间戳;只看总耗时无法判断卡顿来自哪个队列。

请求取消与最终发布

同一个纹理被场景A和B同时请求时,底层读取可以共享,两个请求的使用意图却各自独立。A取消只撤销A的等待者;B仍需要结果,读取就应继续。把“取消一个请求”直接等同于“卸载资源”,会破坏B的依赖。

例如A、B各有一张等待票据,底层纹理T只加载一次。A在上传后取消,等待者仍有B;完成路径只给B创建一个已发布句柄,T的liveHandles从0变1。B释放后计数回到0,若上传或后续绘制还在途,则进入GPU退役队列,不能立即销毁。若A、B都在发布前取消,完成路径不创建句柄,但同样要结束在途资源的寿命。

给每次请求分配唯一票据,票据保存取消状态和目标世界代数。加载中的资源记录等待票据集合,发布成功后再把所有权转换为已发布句柄。下面是串行发布线程上的机制伪代码,GPU上传事件先在各自队列正确同步:

text
CompleteLoad(resource):
    if resource.failed:
        向有效等待者报告错误并结束所有票据
        清空等待者
        将已取得的依赖和临时资源移交退役队列
        return
    for ticket in resource.waiters:
        if ticket.cancelled or ticket.worldGeneration != currentWorldGeneration:
            continue
        resource.liveHandles += 1
        发布给ticket的独立句柄
    清空等待者
    if resource.liveHandles == 0:
        加入退役队列,停止新增引用,等已录制命令提交或丢弃且所有GPU使用完成后回收

完成事件与取消事件由同一所有者序列化,避免两边同时递减同一计数。已发布以后发生取消,调用者释放它收到的句柄;发布以前取消,完成路径跳过交付。每条路径都需要恰好一次结束记录。失败路径把已经取得的依赖按逆序移交退役流程,待相关GPU使用完成后再释放;纯CPU且已无人使用的部分可以立即回收。这样既能平衡引用计数,也能保住在途命令的资源寿命。

退役也覆盖已经录制、尚未提交的命令及上传暂存数据,由命令所有者保留它们直到对应GPU使用完成或明确丢弃;各阶段传递数据时同时交接这份所有权,见GPU资源生命周期。

GPU命令已经提交时,取消改变的是资源是否继续交付,不能收回设备正在执行的读写。退役记录保存对应完成值,达到该值后才回收暂存区和资源。可复现检查应覆盖“读盘前取消、上传后取消、发布后释放、共享请求仅取消一方”,分别核对等待者、句柄和GPU在途引用三类数量。

验证方法

  • 导出依赖图和重复 Asset 报表。
  • 对每个 Bundle 记录大小、依赖、更新频率和所有者。
  • 用 Memory Profiler 检查卸载前后对象和原生资源。
  • 模拟断网、取消、版本不一致和磁盘不足。
  • 记录下载、IO、解压、反序列化、实例化和 GPU Upload 各阶段时间。

相关主题

参考资料

本文异步状态机为框架无关设计,取消票据和GPU退役协议是明确前提下的机制推导。完整加载器、断网回归和目标引擎Profiler实测尚未执行。