第五章:AssetBundle 的运行时行为
对应工程的 Assets/StreamingAssets。只读。内容随包体发布。对应操作系统的沙盒存储目录。读写。用于存储热更新下载的资源(即 UnityWebRequest 缓存的位置)。本地加载使用以获得最佳的内存和 I/O 性能。网络加载使用,利用其自动缓存和转码特性。加密处理如需加密,通过实现,避免 LoadFromMemory 带来的内存开销。生命周期构建基于引用计数的管理系统,确保仅在
第五章: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 关键路径定义
- Application.streamingAssetsPath
- 对应工程的 Assets/StreamingAssets。
- 只读。内容随包体发布。
- 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) : 完整释放
行为:
- 释放 AssetBundle 对象的内存头信息和文件句柄。
- 强制销毁所有从该 AssetBundle 加载并实例化的 Asset(如 Texture, Mesh, GameObject)。
结果:
内存被完全回收。但如果场景中仍有 GameObject 引用了被销毁的资源,会出现资源丢失(如变粉、Missing Reference)。
仅在确定资源不再被任何逻辑使用时调用。
5.2 Unload(false): 仅释放头部
行为:
- 释放 AssetBundle 对象的内存头信息和文件句柄。
- 保留当前已加载到内存中的 Asset。
隐患(资源脱管):
- 引用断裂:保留下来的 Asset 与 AssetBundle 的链接被切断。
- 内存冗余:如果后续再次加载同一个 AssetBundle,Unity 会将其视为一个新的 Bundle 实例。再次调用 LoadAsset 会在内存中创建该资源的新副本,导致内存中存在多份相同数据。
- 需要我调用Resources.UnloadUnusedAssets去除冗余内存,调用可能会造成卡顿。
在严格的资源管理架构中,应避免使用 Unload(false)。它会导致资源生命周期不可控。
总结
本地加载使用 LoadFromFile 以获得最佳的内存和 I/O 性能。
网络加载使用 UnityWebRequestAssetBundle,利用其自动缓存和 LZMA -> LZ4 转码特性。
加密处理如需加密,通过 LoadFromStream 实现,避免 LoadFromMemory 带来的内存开销。
生命周期构建基于引用计数的管理系统,确保仅在资源引用归零时调用 Unload(true),杜绝 Unload(false) 造成的资源冗余和泄漏。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)