第五章:AssetBundle 的运行时行为

1. 本地加载 :内存分配与 I/O 策略

在 UnityEngine.AssetBundle 的源码中,提供了三种核心的本地加载方式。虽然它们最终都返回 AssetBundle 对象,但在底层内存分配和 I/O 机制上存在本质差异。

1.1 LoadFromFile

[MethodImpl(MethodImplOptions.InternalCall)]
[FreeFunction("LoadFromFile")]
internal static extern AssetBundle LoadFromFile_Internal(string path, uint crc, ulong offset);

机制
该方法通过 Native 层直接调用操作系统的文件系统 API。

对于 LZ4未压缩 的 AssetBundle,引擎仅读取文件头并建立虚拟文件索引,不进行完整文件的内存拷贝

实际的资源数据(Data Block)是在后续调用 LoadAsset 时按需读取的。

内存占用极低(仅限于 Header 和索引数据),是最高效的加载方式。
在本地存储场景下(如 StreamingAssets 或已下载到沙盒的包),应始终作为首选方案。

offset一般可以做个头偏移,对资源做个简易加密,还支持多个Bundle的组合,需要自己写代码管理组合,实际用处不大。

1.2 LoadFromMemory

[MethodImpl(MethodImplOptions.InternalCall)]
[FreeFunction("LoadFromMemory")]
internal static extern AssetBundle LoadFromMemory_Internal(byte[] binary, uint crc);

机制
该方法接收一个 byte[] 数组。这意味着在调用 API 之前,文件内容已经被完整加载到了 Managed Heap(托管堆) 中。
传入 Native 层后,Unity 引擎通常会在 Native Heap(原生堆) 中分配一块新的缓冲区来存储该数据,或者进行解压操作。
双重内存占用。对于一个 10MB 的 AssetBundle,至少需要消耗 10MB(托管堆)+ 10MB(原生堆)的内存峰值。
应尽量避免使用。仅在无法获取文件路径且必须从内存构建(如某些极其特殊的加密解密流程)时使用。

1.3 LoadFromStream

public static AssetBundle LoadFromStream(Stream stream, uint crc, uint managedReadBufferSize)
{
    ValidateLoadFromStream(stream); // 校验 CanRead 和 CanSeek
    return LoadFromStreamInternal(stream, crc, managedReadBufferSize);
}

机制
该方法允许传入一个 C# 的 Stream 对象。源码显式检查了 stream.CanSeek,表明引擎需要对流进行随机访问。
Unity 会通过托管/原生互操作调用流的 Read 和 Seek 方法,按需读取数据块。
内存占用取决于 managedReadBufferSize(默认通常较小),避免了 LoadFromMemory 的全量拷贝问题。
资源加密的最佳实践。开发者可以继承 Stream 类实现自定义的解密流,在 Read 方法中实时解密数据,从而在保证安全的同时维持较低的内存水位。

对于安卓平台建议使用BetterStreamingAssets 插件,否则C# Stream 无法访问apk或aab里的streamingAssetsPath.

2. UnityWebRequestAssetBundle的加载与缓存

对于需要从服务器下载 AssetBundle 的场景,Unity 提供了专门的 API UnityWebRequestAssetBundle。它不仅仅是一个下载器,也集成下载、缓存、解压于一体,不仅仅可以加载网络资源,也可以加载本地资源(加载本地资源uri 要加入前缀**“file://”**)。

2.1 UnityWebRequestAssetBundle.GetAssetBundle

public static UnityWebRequest GetAssetBundle(string uri, CachedAssetBundle cachedAssetBundle, uint crc = 0);

机制
该 API 创建一个 UnityWebRequest,并自动挂载 DownloadHandlerAssetBundle。

流式写入:与普通 UnityWebRequest.Get 不同,它不会将下载的数据完整缓存在内存中。数据流会直接写入磁盘缓存(Cache)或以流的形式处理。

自动缓存:根据传入的 Hash 或 Version,引擎会自动检查 Caching 系统。

  • Cache Hit (命中):直接从本地磁盘缓存加载(行为等同于 LoadFromFile)。
  • Cache Miss (未命中):从网络下载,写入缓存,然后加载。

2.2 特性:LZMA 自动转码

该 API 实际用处不多,有这么个特性。

为了节省带宽,服务器通常部署 LZMA 格式(包体最小)的 AssetBundle。

LZMA 不支持随机读取,运行时加载性能差。DownloadHandlerAssetBundle 在下载 LZMA 包的过程中,会利用后台线程自动将其解压并重压缩为 LZ4 格式,然后存储到本地缓存中。

传输层:享受 LZMA 的低带宽。

存储/运行层:享受 LZ4 的随机读取和低内存占用。

2.3 内存注意事项

虽然 DownloadHandlerAssetBundle 优化了内存,但仍需注意:

  • WebStream Buffer:下载过程中仍会占用少量的原生内存缓冲区。
  • 获取对象:下载完成后,必须调用 DownloadHandlerAssetBundle.GetContent(UnityWebRequest) 来获取 AssetBundle 对象引用。此操作类似于 LoadFromFile,是低开销的。

3. 寻址:平台路径差异与加载策略

在使用 LoadFromFile 时,路径参数的处理在不同平台(特别是 Android)存在显著差异。

3.1 关键路径定义

  1. Application.streamingAssetsPath
    • 对应工程的 Assets/StreamingAssets。
    • 只读。内容随包体发布。
  2. Application.persistentDataPath
    • 对应操作系统的沙盒存储目录。
    • 读写。用于存储热更新下载的资源(即 UnityWebRequest 缓存的位置)。

3.2 Android 平台的特殊性

在 Android 平台上,StreamingAssets 位于 APK(Zip 压缩包)内部。

  • System.IO 的限制
    C# 的 File.Exists 或 FileStream 无法直接访问 APK 内部的路径(如 jar:file:///…/assets/bundle)。
  • Unity API 的特权
    AssetBundle.LoadFromFile 在底层进行了特殊处理。该 API 可以直接从 APK 内部读取数据,无需将文件解压或拷贝到沙盒目录。

3.3 运行时加载策略

在实现资源管理器时,通常采用“双路径回退”逻辑:

public AssetBundle LoadBundle(string bundleName)
{
    // 1. 优先检查沙盒目录(热更新版本)
    string hotPath = Path.Combine(Application.persistentDataPath, bundleName);
    if (File.Exists(hotPath)) 
    {
        return AssetBundle.LoadFromFile(hotPath);
    }

    // 2. 回退到包内目录(初始版本)
    string builtInPath = Path.Combine(Application.streamingAssetsPath, bundleName);
    return AssetBundle.LoadFromFile(builtInPath);
}

4. 提取 :反序列化行为

加载 AssetBundle 对象后,需要通过 LoadAsset 系列方法提取内容。

4.1 LoadAsset

public T LoadAsset<T>(string name) where T : Object

根据文件名或路径,在 AssetBundle 的序列化数据中查找对象,并执行反序列化(Deserialization)。
这是一个同步阻塞操作。对于包含大量组件或复杂层级的 Prefab,反序列化过程可能耗时数毫秒至数十毫秒,导致主线程掉帧。
对于较大资源,建议使用 LoadAssetAsync,将反序列化任务分摊到多个帧执行。

4.2 LoadAllAssets

public Object[] LoadAllAssets()

遍历 AssetBundle 中的所有对象并全部加载。该操作会导致包内所有资源同时进入内存。除非该 AssetBundle 是专门设计的图集(Atlas)或 Shader 集合包,否则应避免使用此 API,以免造成不必要的内存峰值。

5. 卸载:生命周期管理的核心

Unload 方法是资源管理中最关键也最容易出错的环节。其参数 bool unloadAllLoadedObjects 决定了截然不同的内存行为。

[MethodImpl(MethodImplOptions.InternalCall)]
[NativeMethod("Unload")]
public extern void Unload(bool unloadAllLoadedObjects);

5.1 Unload(true) : 完整释放

行为

  1. 释放 AssetBundle 对象的内存头信息和文件句柄。
  2. 强制销毁所有从该 AssetBundle 加载并实例化的 Asset(如 Texture, Mesh, GameObject)。

结果
内存被完全回收。但如果场景中仍有 GameObject 引用了被销毁的资源,会出现资源丢失(如变粉、Missing Reference)。
仅在确定资源不再被任何逻辑使用时调用。

5.2 Unload(false): 仅释放头部

行为

  1. 释放 AssetBundle 对象的内存头信息和文件句柄。
  2. 保留当前已加载到内存中的 Asset。

隐患(资源脱管)

  • 引用断裂:保留下来的 Asset 与 AssetBundle 的链接被切断。
  • 内存冗余:如果后续再次加载同一个 AssetBundle,Unity 会将其视为一个新的 Bundle 实例。再次调用 LoadAsset 会在内存中创建该资源的新副本,导致内存中存在多份相同数据。
  • 需要我调用Resources.UnloadUnusedAssets去除冗余内存,调用可能会造成卡顿。

在严格的资源管理架构中,应避免使用 Unload(false)。它会导致资源生命周期不可控。

总结

本地加载使用 LoadFromFile 以获得最佳的内存和 I/O 性能。

网络加载使用 UnityWebRequestAssetBundle,利用其自动缓存和 LZMA -> LZ4 转码特性。

加密处理如需加密,通过 LoadFromStream 实现,避免 LoadFromMemory 带来的内存开销。

生命周期构建基于引用计数的管理系统,确保仅在资源引用归零时调用 Unload(true),杜绝 Unload(false) 造成的资源冗余和泄漏。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐