AssetBundle定义和基础机制。
AssetBundle可以理解为一种Unity特殊的压缩包。它的主要作用是,压缩大小,保证资源依赖完整。
以上两个特性延伸出来两个主要的应用场景和使用目的。热更新(因为体积更小,又能简单保证资源的完整性),性能(内存)优化。
AssetBundle存放资源时分为
- serialized file : 里面存放一些碎片的,可序列化的文件(比如txt文本),serialized file会把所有的可序列化的散资源整合成一个文件,也就是说一个AssetBundle中只会存在一个serialized file(这个理解就好,没啥用)
- resource files : 源文件,以二进制形式存放一些图片,声音等资源。(这个是内存占用最大的部分)。
AssetBundle的简单使用流程
简述流程为
- 为资源打Bundle的标签(编译时用代码打也可以),
- 编译资源包(
BuildPipeline.BuildAssetBundles(dir, BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows64)这个时候选择压缩格式,加密格式,目标平台等) - 放到远程服务器与下载(其实是热更新)
- 使用AssetBundle类加载到内存(同时要解析和加载依赖),
- 使用AssetBundle加载对应类型的资源LoadAsset
- 卸载AssetBundle(强制卸载和非强制卸载)。
AssetBundle的常用打包选项。
注: 这段的介绍最初来自Siki学院的课件,但是深究之后发现是Unity
Manual的翻译版本,若要追溯请看UnityManual2017。
关于BuildPipeline.BuildAssetBundles(string outputPath, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform);的第二个参数,打包选项设置,简述如下。
BuildAssetBundleOptions.None:使用LZMA算法进行整体压缩,压缩的包更小,但是加载时间更长。而且使用前要对Bundle进行整体解压,并且会整个Bundle加载到内存中去生成内存镜像,解压后的Bundle会使用LZ4压缩。 Unity官方推荐的使用场景是,如果对资源下载大小的要求比较高,可以使用该压缩格式,因为下载后会自动解压成LZ4格式存放。
- 注意:这里官方没说是否是使用WWW下载就会自动解压,使用较少,不深究,不过如果该压缩格式下载之后,只需要解压一次,或者自动解压成LZ4格式,并且存储到硬盘上而不是内存中的话,其实是可以考虑使用的,特别是对下载资源大小有极大地优化需求时。
LZMA是流压缩(stream-based)。流压缩(LZMA)在处理整个数据块时使用同一个字典,它提供了最大可能的压缩率但只支持顺序读取。
BuildAssetBundleOptions.UncompressedAssetBundle:不压缩包体大小,读取自然更快。原汁原味,即快又大,但是这样还不如用resources呢,更快,又不一定更大。
BuildAssetBundleOptions.ChunkBasedCompression:最推荐使用得压缩格式。该选项会使用分段压缩方式,压缩算法为LZ4压缩,虽然压缩率没有LZMA高,但是我们可以在加载指定资源解压而不用解压全部资源。况且,LZMA也会解压成LZ4再进行使用呀。
LZ4是块压缩(chunk-based),块压缩(LZ4)指的是原始数据被分成大小相同的子块并单独压缩。如果你想要实时解压/随机读取开销小,则应该使用这种。
BuildAssetBundleOptions.CollectDependencies:使用该选项时,打包会包含所有依赖资源。也就是我们常说的依赖打包。同样也非常推荐使用。
附全部压缩选项介绍:https://blog.csdn.net/AnYuanLzh/article/details/81485762
另外要注意,打包选项是按位与操作,也就是多个选项可以合并使用,一般情况下,大家最推荐的是ChunkBasedCompression|CollectDependencies的组合使用。既能进行大小压缩,又能获得完整引用的资源。
依赖打包的简单分析
所谓依赖打包,即Bundle打包时选择CollectDependencies时,会将Bundle依赖的所有资源打包到Bundle中,以供后续正确的加载全部需要的资源。
依赖打包的资源冗余。 简而言之,若两个不同的AssetBundle中的资源,引用了统一个未标记AssetBundle标签的资源,那么该资源将会分别被拷贝到这两个AssetBundle中去,造成资源冗余。(这里推荐用代码动态添加Bundle标签,即提前遍历所有需要打Bundle包的资源,先为他们加上Bundle标签,防止出现冗余问题。)
AsseBundle资源之间的相互引用关系:
AsseBundle资源依赖加载方式:
加载一个Bundle中的资源前,要先加载该Bundle和所有被该Bundle引用的AssetBundle,这里官方特别提出了,AssetBundle之间的加载顺序对引用没有影响,但是要保证使用资源前(LoadAsset)正确的加载了所有的需要的Bundle。
(不过测试中确实有Bundle都加载出来,但是资源还是会丢失的情况,这种特殊处理就好:等待一帧,提前手动引用,检查Shader的设置等):
AssetBundle循环依赖问题(即A->B,B->A的问题):
其实这个问题很好解决,所有加载进来的Bundle,都用字典(Dictionary)存起来,加载新的Bundle之前判断该Bundle有没有被加载过就好了。
至于网上说的那些要控制Bundle加载先后顺序的问题纯属谬论。官方明确说明,无需关心加载顺序,只要保证在使用资源前,将所有被引用的Bundle都被正确的加载过一次就好了。
AssetBundle与图集依赖的问题
这种问题多出现在UGUI中,因为UGUI的图集在工程中是散图,且未打图集Tag的图片会被打包到一个默认的图集中去。
1 这种情况下如果多个Bundle引用了同一个图片,那么该默认图集就会被重复打到两个Bundle中去(这里特意说明默认图集,是因为这种默认图集更容易出问题且不易察觉,非默认图集也是同样的情况)。
2 另外,如果同一个图集Tag的Sprite被指定了不同的Bundle标签,也会出现上述问题。
所以我们要注意的是,一个AssetBundle可以包含多个图集,但一个图集的资源只能存在于一个AssetBundle中,否则会造成非常严重的资源冗余。
参考链接 :
官方对依赖的解释(注意2019.4文档中加了一个案例解释,但是原理解释没变)https://docs.unity3d.com/Manual/AssetBundles-Dependencies.html
打包依赖分析 https://zhuanlan.zhihu.com/p/34137188AssetBundle
图集Atlas与AB包https://www.jianshu.com/p/0d18ac565563
如何分配AssetBundle(优化)。
关于AssetBundle的分配方案,这里大概总结一下网上提出的解决方案,并会简单注明其优势和引用出处。最后再说下,目前字项目中的解决方案。
首先是Unity官方的几种方案:
- 逻辑实体分组
a,一个UI界面或者所有UI界面一个包(这个界面里面的贴图和布局信息一个包)。
b,一个角色或者所有角色一个包(这个角色里面的模型和动画一个包)
c,所有的场景所共享的部分一个包(包括贴图和模型)
注: 这个最常用,逻辑实体的优势是,一般情况下一个逻辑实体的加载和卸载时机是一致的,而且按照同一个逻辑实体缓存和卸载,逻辑上更容易理解,且不易出现逻辑BUG。
- 所有声音资源打成一个包,所有shader打成一个包,所有模型打成一个包,所有材质打成一个包。
注: 这个一般作为备用方案,无特殊之处。
- 按照使用分组
把在某一时间内使用的所有资源打成一个包。可以按照关卡分,一个关卡所需要的所有资源包括角色、贴图、声音等打成一个包。
也可以按照场景分,一个场景所需要的资源一个包。
注:这个也思路比较重要,但是一般具体方案还是按照上述逻辑实体分组设计。若项目管理比较严格的,会应该会两者相辅使用,比如某关卡的背景图,背景音效分为一组,关卡里面的每个角色分为一组。
其他诸位前辈总结的方案:
- 把经常更新的资源放在一个单独的包里面,跟不经常更新的包分离。
注:之前小包更新时有这方面的要求,这个具体看需求吧,如果按照逻辑实体分类导致每次更新包容量都太大,可以考虑对逻辑实体再划分。
- 可以把其他包共享的资源放在一个单独的包里面。
注:这个强烈推荐,甚至该方案应该作为最基础的一种设计思路,能减少资源冗余,减少内存加载占用,同时减少频繁更新包的大小。
- 把一些需要同时加载的大量的零散小资源打包成一个包。
注:这个比较实用,主要思路是同时加载一个大的的资源会比同时加载数量较多的小资源快(考虑到IO的切换)。但也比较少遇到,需要按需设计。
AssetBundle的加载方式与其优劣势
常见的加载API(只总结异步的)。Ps:别忘了异步需要启动协程。
- 从内存加载 :不推荐使用,因为需要先加载到内存中(ReadAllBytes也会申请内存),也就类似强行做了个内存镜像,虽然会GC,但是还是在一段时间内会多占用一份内存。
AssetBundleCreateRequest request = AssetBundle.LoadFromMemoryAsync(File.ReadAllBytes(path));
yield return request;
AssetBundle ab = request.assetBundle;
- 从文件加载 :最常用也是最推荐的使用方式,不过LoadFromMemoryAsync加载LZMA格式依旧会将其解压到内存中。
AssetBundleCreateRequest request = AssetBundle.LoadFromFileAsync(path);
- 从WWW加载:即可以访问远程也可以访问本地资源,但改方法即将被淘汰,后续使用UnityWebRequest代替。
WWW www = WWW.LoadFromCacheOrDownload(path, 1);
yield return www;
if( !string.IsNullOrEmpty(www.error) )
{
Debug.Log(www.error);
yield break;
}
AssetBundle ab = www.assetBundle;
- 从UnityWebRequest加载:替代www的加载方式,相比www的优势是,webrequest提供了更丰富的API(比如上传下载,progress,timeout,支持bundle.loadfile等),和更为标准的网络加载流程(详见unity官方文档)。
//1、使用UnityWebRequest.GetAssetBundle(路径)【服务器 / 本地都可以】 去获取到网页请求
UnityWebRequest request = UnityWebRequest.GetAssetBundle(path);
//2、等待这个请求进行发送完
yield return request.SendWebRequest();
//3、发送完请求之后,就要从DownloadHandlerAssetBundle进行获取一个request,得到出来的是一个AssetBundle类对象
AssetBundle ab = DownloadHandlerAssetBundle.GetContent(request);
资源加密后的加载方式:
商业项目中,多需要对资源进行加密。而我遇到的加密方式,基本是对资源进行字节加密,这必然会影响unity对assetbundle的识别,所以加载时也一般只能先使用IO加载到内存中解密,然后再从内存中加载Bundle。(这种方式硬伤很大,参考LoadFromMemoryAsync注释)
AssetBundle移动端加载内存解释
(这位大神解释的很清楚,这里不再整理笔记)https://blog.csdn.net/bloodshadow/article/details/52923492
注意,这张图片只看卸载关系即可。紫色部分的bundle镜像内存,在5.4后已经修改为加载headr方式,不再直接拷贝内存镜像。
Bundle卸载
bundle卸载分为强制卸载(传入true)和弱卸载(参考链接https://www.jianshu.com/p/93d4617f9942)。
注意:如果是依赖打包要考虑引用关系,最好是做个引用计数,以防止出现资源丢失
以休闲游戏为例,提供一个统一的优化解决方案。
首先该方案旨在解决的问题(或者优势)是,
- 通过文件结构的设计,一劳永逸的,解决相互依赖的问题 (只在棋牌游戏实践,其他大型游戏有待实验)
- 通过工具自动打Bundle标签和图集标签
- 统一管理依赖加载和按模块的卸载逻辑
- 解决图集依赖导致Bundle大量冗余问题
- 解决更新包过大的问题(即小包增量更新)
以下是文件结构简图
-
Bundle中的资源按照文件类型分文件夹,他们将由代码按文件夹路径打上Bundle标签
每个命名为Image的文件夹,将由代码遍历其内所有图片,如果该图片无图集Tag,则将该图片打成按照文件夹路径打图集Tag。如果有图集Tag则跳过该图片。 -
被Prefab直接引用的资源放到Scatter里面并按照文件类型分文件夹,图片也要命名为Image以便自动打图集Tag。
-
被多个模块引用的资源,将放到CommonModule中去(也就是功能模块思路)。
最后说缺点
- 实际使用过程中发现,偶尔发现模块图集和Bundle粒度过大和过小时,无法妥善解决。
ps:过大实际上可以特殊处理,看实际影响吧。图集过大时可以自定义图集Tag
Bundle过大时可以分文件夹出来,会自动打成新的Bundle,但是加载逻辑要特殊处理。
粒度过小问题无解决方案,也无需特别在意。 - 没有太考虑图集的大小分配逻辑,如果对图集要求很高,很多数情况下还是要手动设置。
其他需要注意的问题
- 不要分包过大(150Mb以内)或过小
过大时,有可能出现一次性向移动端申请连续内存过大,导致IO异常。
过小时,IO切换处理太过频繁,反而会导致读取速度下降。
AssetBundle中其他常见问题和一些思考
打包Bundle后出现Shader依赖丢失问题
将shader放入GraphicsSettings->Always Included Shaders中后,打包时会将相应的shader抽离,运行时加载时会自动加载其依赖的shader。同时也意味着,如果修改了Always Included Shaders或在一个新建项目中使用该Bundle,会出现shader丢失的问题。注意检查即可。
打包Bundle后编辑器平台出现Spine动画丢失材质问题。
未找到具体原因,估计是spine 编辑器平台支持不好,改用prefab加载即可。
常看到移动端读取AssetBundle时会先读取Head文件,那么这个Head究竟是什么呢?
来自https://blog.csdn.net/lodypig/article/details/51863683
AssetBundleFileHead : 记录了版本、是否压缩等主要描述信息。
AssetFileHeader :包含一个文件列表,记录了每个资源的name,offset,length等。
Asset1 : 第一个资源本身,内部结构如下
AssetHeader :包含了TypeTree大小、文件大小、format等。
增量打包是什么?
所谓增量打包,就是只打包有修改的部分,以减少打包时长。 参考链接https://www.jianshu.com/p/68e66a51f6a8
Resources.Load后第一次Instantiate卡顿?
使用Resources.Load的时候在第一次Instantiate之前,相应的Asset对象还没有被创建,直到第一次Instantiate时才会真正去读取文件创建这些Assets。它的目的是实现一种OnDemand的使用方式,到该资源真正使用时才会去创建这些资源。
而使用AssetBundle.Load方法时,会直接将资源文件读取出来创建这些Assets,因此第一次Instantiate的代价会相对较小。
上述区别可以帮助我们解释为什么发射第一发子弹时有明显的卡顿现象的出现。
参考链接 https://blog.csdn.net/wotingdaonile/article/details/80111164
使用AssetBundle打包和加载场景资源
参考链接https://blog.csdn.net/sinat_28962939/article/details/89396577
打包
BuildPipeline.BuildPlayer(BuildPlayerOptions options )
options 在options中填入需要打包的场景路径,和打包选项设置为BuildAdditionalStreamedScenes
加载
SceneManager.LoadSceneAsync ("Test")
先将Bundle加载到内存中,然后直接调用SceneManager的场景加载接口即可,只要Bundle在内存中,SceneManager就可以自动从内存中加载到对应的场景,无需在特殊指向这个Bundle。