参考链接:(本文参自各位大佬,按个人风格整理,请点击链接查看原文。)
UWA: https://blog.uwa4d.com/archives/ABTheory.html
陈嘉栋(慕容小匹夫):http://www.cnblogs.com/murongxiaopifu/p/5629415.html#autoid-3-3-0
官方文档:https://docs.unity3d.com/ScriptReference/AssetBundle.html
一、获取AssetBundle对象的常用API:
1.先获取WWW对象,再通过WWW.assetbundle获取AssetBundle对象:
// -1-
WWW www = new WWW(string url);
加载Bundle文件并获取WWW对象,完成后会在内存中创建较大的WebStream(解压后的内容,通常为原Bundle文件的4-5倍大小,纹理资源比例可能更大),因此后续的AssetBundle.Load(5.3后为AssetBundle.LoadAsset)可以直接在内存中进行。
// -2-
WWW www = WWW.LoadFromCacheOrDownLoad(string url,int version,uint crc=0);
加载Bundle文件并获取WWW对象,同时将解压形式的Bundle内容存入磁盘作为缓存(若该Bundle已经在缓存中,则省去这一步),完成后只会在内存中创建较小的SerializedFile,而后续的AssetBundle.Load(5.3后为AssetBundle.LoadAsset)需要通过IO从磁盘中的缓存获取。
public AssetBundle assetBundle;
//通过之前两个接口获取WWW对象后,即可通过WWW.assetBundle获取AssetBundle对象。
2.直接获取AssetBundle:
// -1- 从文件中读取AB
//5-5.3
public static AssetBundle CreateFromFile(string path); //不支持压缩
//通过未压缩的Bundle文件,同步创建AssetBundle对象,这是最快的创建方式。
//(CreateFromFile因为不支持压缩,所以只能读取未压缩的路径和资源或者用WWW去读)
//创建完成后只会在内存中创建较小的SerializedFile,而后续的AssetBundle.Load需要通过IO从磁盘中获取。
//5.3以后为
public static AssetBundle LoadFromFile(string path,uint crc = 0, ulong offset = 0) //支持任意压缩格式
//crc:可选的未压缩内容的CRC-32校验和,若不为0则内容加载之前要和校验和比较,不匹配则报错
//offset:可选的字节偏移。这个值指定从哪里开始读取AssetBundle。
//LZMA压缩:数据会解压到内存;未压缩和LZ4(块)压缩:直接从磁盘读取资源包,不会有额外的内存开销的。
// -2- 从字节数组中读取AB
//5-5.3
public static AssetBundleCreateRequest CreateFromMemory(byte[] binary);
//通过Bundle的二进制数据,异步创建AssetBundle对象,完成后会在内存中创建较大的WebStream。
//调用时,Bundle的解压是异步进行的,因此对于未压缩的Bundle文件,该接口与CreateFromMemoryImmediate等价。
//5.3以后为
public static AssetBundleCreateRequest LoadFromMemoryAsync(byte[] binary,uint crc = 0);
//当你用WWW去下载一个加密的数据,然后需要用解密后的bytes去创建AB时这个很好用。
//与同步方法相比,这个会在后台线程解压AB,不会立即创建AB。
//加载完成用assetBundle属性去获取AB
// -3-
public static AssetBundle CreateFromMemoryImmediate(byte[] binary);
//5.3以后:LoadFromMemory 该接口是上条从字节数组加载AB的同步版本。
注:5.3下分别改名为LoadFromFile,LoadFromMemory,LoadFromMemoryAsync并增加了LoadFromFileAsync,且机制也有一定的变化,可详见Unity官方文档。
这几种方法内存消耗和性能比较:(Unity5.4)
注意:当使用WWW来下载一个bundle时,WebRequest还会有一个8*64KB的缓存区用来存储来自socket的数据。
二、从AssetBundle加载资源的常用API:
// -1- 5.0-5.3
public Object Load(string name, Type type);
//通过给定的名字和资源类型,加载资源。加载时会自动加载其依赖的资源,即Load一个Prefab时,会自动Load其引用的Texture资源;
//多个资源打到一个AB上时,根据名称及后缀去加载指定资源。
//5.3以后 为 public Object LoadAsset(string name);
//两个资源打到一个AB上时,根据名称及后缀去加载指定的资源
//注:LoadAsset可以根据名称加载资源,但是有些资源没有设置ABName但是被依赖了然后也被打进AB中
//这类资源无法通过Load及LoadAll返回获取到(LoadAll虽然会加载但返回的数组中不含它们)。
。
这里参自:UWA问答
// -2- 5.0-5.3
public Object[] LoadAll(Type type);
//一次性加载Bundle中给定资源类型的所有资源。
// 5.3以后
public Object[] LoadAllAssets(Type type);
// -3- 5.0-5.3
public AssetBundleRequest LoadAsync(string name, Type type);
//该接口是Load的异步版本。
// 5.3以后
public AssetBundleRequest LoadAssetAsync(string name);
注:5.x下分别改名为LoadAsset,LoadAllAssets,LoadAssetAsync,并增加了LoadAllAssetsAsync。
三、AssetBundle加载接口对比:
1.new WWW与WWW.LoadFromCacheOrDownload
前者优势:
-后续的Load操作在内存中进行,相比后者的IO操作开销更小;
-不形成缓存文件,而后者则需要额外的磁盘空间存放缓存;
-能通过WWW.texture,WWW.bytes,WWW.audioClip等接口直接加载外部资源,而后者只能用于加载AssetBundle
前者劣势:
-每次加载都涉及到解压操作,而后者在第二次加载时就省去了解压的开销;
-在内存中会有较大的WebStream,而后者在内存中只有通常较小的SerializedFile。(此项为一般情况,但并不绝对,对于序列化信息较多的Prefab,很可能出现SerializedFile比WebStream更大的情况)
四、内存分析
在管理AssetBundle时,了解其加载过程中对内存的影响意义重大。在上图中,我们在中间列出了AssetBundle加载资源后,内存中各类物件的分布图,在左侧则列出了每一类内存的产生所涉及到的加载API:
-WWW对象:在第一步的方式1中产生,内存开销小;
-WebStream:在使用new WWW或CreateFromMemory时产生,内存开销通常较大;
-SerializedFile:在第一步中两种方式都会产生,内存开销通常较小;
-AssetBundle对象:在第一步中两种方式都会产生,内存开销小;
-资源(包括Prefab):在第二步中通过Load产生,根据资源类型,内存开销各有大小;
-场景物件(GameObject):在第二步中通过Instantiate产生,内存开销通常较小。
-在后续的章节中,我们还将针对该图中各类内存物件分析其卸载的方式,从而避免内存残留甚至泄露。
五、注意点
1.Unity5.4之前:
CreateFromFile只能适用于未压缩的AssetBundle,而Android系统下StreamingAssets是在压缩目录(.jar)中,因此需要先将未压缩的AssetBundle放到SD卡中才能对其使用CreateFromFile;WWW接口可以加载压缩的AB。Unity5.4之后的LoadFromFile支持任意压缩格式的AB,所以没有太大必要使用WWW了,而且这个接口像WWW.LoadFromCacheOrDownload接口一样,加载不压缩或者LZ4压缩格式的AB的时候是不会有额外的内存开销的。
2.iOS系统有256个开启文件的上限,因此,内存中通过CreateFromFile或WWW.LoadFromCacheOrDownload加载的AssetBundle对象也会低于该值,在较新的版本中,如果LoadFromCacheOrDownload超过上限,则会自动改为new WWW的形式加载,而较早的版本中则会加载失败。
3.CreateFromFile和WWW.LoadFromCacheOrDownload的调用会增加RersistentManager.Remapper的大小,而PersistentManager负责维护资源的持久化存储,Remapper保存的是加载到内存的资源HeapID与源数据FileID的映射关系,它是一个Memory Pool,其行为类似Mono堆内存,只增不减,因此需要对这两个接口的使用做合理的规划。
4.对于存在依赖关系的Bundle包,在加载时主要注意顺序。举例来说,假设CanvasA在BundleA中,所依赖的AtlasB在BundleB中,为了确保资源正确引用,那么最晚创建BundleB的AssetBundle对象的时间点是在实例化CanvasA之前。即,创建BundleA的AssetBundle对象时、Load(“CanvasA”)时,BundleB的AssetBundle对象都可以不在内存中。
根据经验,建议AssetBundle文件的大小不超过1MB,因为在普遍情况下Bundle的加载时间与其大小并非呈线性关系,过大的Bundle可能引起较大的加载开销。
由于WWW对象的加载是异步的,因此逐个加载容易出现下图中CPU空闲的情况(选中帧处Vsync占了大部分),此时建议适当地同时加载多个对象,以增加CPU的使用率,同时加快加载的完成。
六、AB卸载
前文提到了通过AssetBundle加载资源时的内存分配情况,下面,我们结合常用的API来介绍如何将已分配的内存进行卸载,最终达到清空所有相关内存的目的。
在上图中的右侧,我们列出了各种内存物件的卸载方式:
场景物件(GameObject):这类物件可通过Destroy函数进行卸载;
资源(包括Prefab):除了Prefab以外,资源文件可以通过三种方式来卸载:
1) 通过Resources.UnloadAsset卸载指定的资源,CPU开销小;
2)通过Resources.UnloadUnusedAssets一次性卸载所有未被引用的资源,CPU开销大;
3)通过AssetBundle.Unload(true)在卸载AssetBundle对象时,将加载出来的资源一起卸载。WWW对象:调用对象的Dispose函数或将其置为null即可;
WebStream:在卸载WWW对象以及对应的AssetBundle对象后,这部分内存即会被引擎自动卸载;
SerializedFile:卸载AssetBundle后,这部分内存会被引擎自动卸载;
AssetBundle对象:AssetBundle的卸载有两种方式:
1)通过AssetBundle.Unload(false),卸载AssetBundle对象时保留内存中已加载的资源;
2)通过AssetBundle.Unload(true),卸载AssetBundle对象时卸载内存中已加载的资源,由于该方法容易引起资源引用丢失,因此并不建议经常使用;
2.注意点
在通过AssetBundle.Unload(false)卸载AssetBundle对象后,如果重新创建该对象并加载之前加载过的资源到内存时,会出现冗余,即两份相同的资源。
被脚本的静态变量引用的资源,在调用Resources.UnloadUnusedAssets时,并不会被卸载,在Profiler中能够看到其引用情况。
七、针对项目的建议
来自:http://www.cnblogs.com/murongxiaopifu/p/5629415.html#autoid-3-3-0
由于以上分析的几种加载手段各有各的使用情景和特点。因此建议在我们的项目中按照以下情景使用这些方法:
1、 随游戏一同发布的AssetBundle(一般位于StreamingAssets文件夹中):
在打AssetBundle包时,使用LZ4压缩格式进行打包(开启BuildAssetBundleOptions.ChunkBasedCompression即可)。
在运行时需要加载AssetBundle对象时,使用LoadFromFile方法进行加载。
这样做的好处是:即可以将AssetBundle文件压缩,又可以兼顾加载速度,且节约内存。
2、作为更新包,需要从服务端下载的AssetBundle:
在打AssetBundle包时,使用默认的LZMA格式压缩。
使用WWW.LoadFromCacheOrDownload方法下载并缓存AssetBundle包文件。
这样做的好处是:获得了最大的压缩率,在下载过程中可以减少数据传输量。同时,在本地磁盘创建缓存之后,又可以兼顾之后的加载速度,且节约内存。
3、我们自己进行加密的AssetBundle:
在打AssetBundle包时,使用LZ4压缩格式进行打包(开启BuildAssetBundleOptions.ChunkBasedCompression即可)。
在运行时需要加载AssetBundle对象时,使用LoadFromMemory方法进行加载。(这也是从内存中使用流数据加载AssetBundle对象的仅有的使用场景。)
4、我们自己压缩的AssetBundle:
我们自己也可以使用第三方库或工具对生成的AssetBundle包文件进行压缩,如果需要这样做,则我们最好不要再使用Unity3D对AssetBundle进行压缩,因此在打包时选择开启BuildAssetBundleOptions.UncompressedAssetBundle。
在运行时需要加载AssetBundle对象时,使用LoadFromFileAsync方法进行异步加载
八、UWA的建议:
1/ 对于需要常驻内存的Bundle文件来说,优先考虑减小内存占用,因此对于存放非Prefab资源(特别是纹理)的Bundle文件,可以考虑使用WWW.LoadFromCacheOrDownload或AssetBundle.CreateFromFile加载,从而避免WebStream常驻内存;
2/ 而对于存放较多Prefab资源的Bundle,则考虑使用new WWW加载,因为这类Bundle用WWW.LoadFromCacheOrDownload加载时产生的SerializedFile可能会比new WWW产生的WebStream更大。
3/ 对于加载完后即卸载的Bundle文件,则分两种情况:优先考虑速度(加载场景时)和优先考虑流畅度(游戏进行时)。
1)加载场景的情况下,需要注意的是避免WWW对象的逐个加载导致的CPU空闲,可以考虑使用加载速度较快的WWW.LoadFromCacheOrDownload或AssetBundle.CreateFromFile,但需要避免后续大量地进行Load资源的操作,引起IO开销(可以尝试直接LoadAll)。
2) 游戏进行的情况下,则需要避免使用同步操作引起卡顿,因此可以考虑使用new WWW配合AssetBundle.LoadAsync来进行平滑的资源加载,但需要注意的是,对于Shader、较大的Texture等资源,其初始化操作通常很耗时,容易引起卡顿,因此建议将这类资源在加载场景时进行预加载。
只在Bundle需要加密的情况下,考虑使用CreateFromMemory,因为该接口加载速度较慢。
尽量避免在游戏进行中调用Resources.UnloadUnusedAssets(),因为该接口开销较大,容易引起卡顿,可尝试使用Resources.Unload(obj)来逐个进行卸载,以保证游戏的流畅度。
需要说明的是,以上内存管理较适合于Unity 5.3之前的版本。Unity引擎在5.3中对AssetBundle的内存占用进行一定的调整,目前我们也在进一步的学习和研究中。
以上即为我们这次为您带来的AssetBundle管理机制,希望对您的项目研发有所帮助。我们会在后续技术文章通过大量的案例来进一步解释AssetBundle的管理机制,敬请关注。