一.问题
首先,这里说明一下,我这边的GameObject有点笼统,就是表达的是游戏中的具体实例。
二.概念
1)Asset是什么?
游戏中具体的资源,像texture,mesh,material,shader,script等,实实在在的游戏项目文件夹中所需要堆放的资源。比如,var obj = Resource.Load<GameObject>(\"Prefabs/testItem\"),这个obj就是Asset。
2)GameObject是什么?
var gameItem= Instantiate(obj),这个gameItem就是可以存在于游戏的实际场景中(这个比较简单,不多说了)。GameObject是游戏中实际使用的对象(就是你会在屏幕中实际看到的),是由Asset实例化后的对象。本质上其实还是Asset的衍变,是对部分Asset的引用和复制出来的新东西,其本质还是Asset。
3)AssetBundle是什么?
由上述可知,我们在游戏中生成实际的物体,需要Asset。而Asset,比如一张图片,也是一个Asset,实际大小1M,游戏中这种图片很多,那就轻轻松松几百M,几个G的Asset,都是很有可能的。作为程序员,对于这种“原汁原味”拿过来肯定不行。比如我们工作中把文件什么的发给同事的时候都知道压缩一下,可以传输的过程中小一些。当然了,我们在游戏开发中使用Asset,也是需要类似的。于是,就推出了AssetBundle这一概念。当然我们推出AssetBundle,远不止压缩这一需求。但是你需要知道,主要是为了更好的传输,还有比如减少资源大小,利于网络那边的传输,方便加载。
简而言之,AssetBundle就是为了让游戏项目中大量Asset适应实际游戏运行时而被压缩后的一种二进制文件。
三.分析
1)Asset和GameObject的关系?
①复制+引用关系
Instaniate一个Prefab(Asset),是对Asset进行clone(复制)+引用结合的过程。GameObject,transform是clone的。其他mesh/texture/material/shader等,这些都是纯引用的。引用的Asset对象不会被复制,只是一个简单的指针指向已经load的Asset对象。专门要提一下Script Asset,Unity里每个Script都是一个封闭的class定义,并没有写调用代码。光class是不会工作的。其实Unity引擎就是那个调用代码。clone一个script asset等于new一个class实例,实例才会工作 ,把它挂到Unity主线程的调用链去,class实例里的update和start才会被执行。多个物体挂同一个脚本,其实就是多个物体挂了挂了那个脚本的类的实例。在new class过程中,数据区是复制的,代码区是共享的,算是一种复制+引用关系。引用关系的话,会有一对一,一对多,多对一的关系。如下图:
②释放
如果你Destroy(GameObject,这个是游戏中具有实例),只是释放了clone的asset,引用的asset并不会被干掉。还有因为destroy并不知道有没有被别的asset引用。但是如果你想把asset也释放,有两种方案。
I. Resources.UnloadUnusedAssets()
释放当前所有的没有被引用的(无用)asset,但是不能保证释放掉当前的被引用的资源(因为可能还被其他资源引用,就不会去释放了)。缺点:异步,会卡。由于Unity资源的相互引用关系比较复杂,想要明确判断某一资源不存在引用关系是有一定难度的,并且,如果我 们想要释放的资源存在隐形的引用关系,UnloadUnUsedAssets将会无视这个资源而无任何反馈。根据实战来看,最佳使用的时机是在场景切换进入新的场景后,Unity场景关闭会有效的销毁所有的对象和所有代码的引用,即在新场景开头最为稳妥。必要时加上GC.Collect().
II.Resouce.UnLoad(obj)
释放当前实例的所有被引用的asset(不管这个asset是否还有被引用,所以,风险很大,除非保证确定被他引用的资源没有再被其他资源引用,一般用于单独的一张纹理释放)缺点:风险太大,容易被报:UnloadAsset may only be used on individual assets and can not be used on GameObject\'s / Components or AssetBundles,不能用作卸载GameObject,只能用于纹理释放。不然会报这个错,我也不知道为啥,有知道的告我一下,额,还有就是这个方法很少用,应用的话,也只是对纹理释放。
对于这种使用,对于资源大的且无引用的可以使用,如果资源消耗不大,可以等到场景切换,使用I中 Resources.unloadUnusedAssets方案。
III.GameObject.DestroyImmediate(asset,true)
代替上述方案,可以针对卸载asset(好像还可以直接使用GameObject.Destroy(asset) ,也可以直接使用。(待验证))
IV.AssetBundle.unload(true) !!!慎用
这个也可以卸载asset,但是卸载的是这个assetBundle里面的所有的asset。(后面详说)
2)AssetBundle和Asset的关系
①包含,依赖关系
一个AssetBundle中可以包含一个或多个Asset。一个Asset依赖于AssetBundle。
②释放
AssetBundle的释放只能通过以下两种方式释放。即使系统在加载新场景的时候所有的内存对象都会被自动销毁,包括你用AssetBundle.load加载的对象和Instaniate克隆的,但是不包括AssetBundle文件本身的内存镜像,那个必须要用Unload来释放,用.Net术语说该资源是非托管的。
I.AssetBundle.Unload(false) 使用频率较多
用AssetBundle.Load加载需要的asset之后应该立即使用unlaod(false),释放assetbundle文件本身的内存镜像,但不销毁该assetBundle加载过的asset对象。(尽量释放一部分内存,大多数游戏这么做)
II.AssetBundle.Unload(true)
释放该assetBundle文件镜像并释放该assetBundle所有loaded的asset内存对象。(风险很大,因为一般不太能确定是否该loaded的asset是否还被其他资源引用)。
四.实例测试
AssetBundle和Asset 项目工程中大小分析
①首先,准备10个一样大小的texture(为了测试结果更加准备,每个图1.33M)
texture 的原大小,即Asset本身的大小:1.33M *10 = 13.3M
②LZ4和LZMA打包方式案例对比
I.BuildAssetBundleOptions.None 打包方式,该为默认压缩,即LZMA,在使用AssetBundle之前需要解压缩。使用LAMA格式压缩的AssetBundle的包体最小
(高压缩比),但是会增加相应的解压缩时间和内存。
BuildPipeline.BuildAssetBundles(dir, BuildAssetBundleOptions.None,BuildTarget.StandaloneWindows64);
以上述方式打包完之后 每张 texture :613K *10 = 6M
执行下面代码。(为方便数据对比,是在找不到大的texture了)
使用AssetBundle.LoadFromFile 加载那10个AssetBundle之后的内存显示
对比上2张图的消耗,大概消耗了13.6M的Unity内存,0.5M其他内存。因为是十张纹理,原纹理没有打包之前的大小大概是1.33m,也就是说,大概技术释放的是原Asset的大小。
再Load其中的Asset
由上图可知,申请的是Unity内存爆增100M左右,Mono的内存也相应的增加了。
II.ChunkBasedCompression,即LZ4压缩方式,压缩比一般,压缩后的包体较大,但是解压速度快,消耗内存小。
BuildPipeline.BuildAssetBundles(dir, BuildAssetBundleOptions.ChunkBasedCompression,BuildTarget.StandaloneWindows64);
以上述方式打包完之后 每张texture :1110K*10=11100K,11M左右
再执行同上一致的Load脚本.
AssetBundle,Asset,GameObject 加载中的内存对比
同上述步骤一致,给出LoadAssetBundle,即AssetBundle.LoadFromFile后的内存大小
大概消耗2M内存。(上述LAMA方式打出的包是23.5M,相差10倍啊,果断使用这种方式,我们项目也是这种方式)
再给出LoadAsset之后的内存图
LoadAsset之后,Unity内存,Mono内存都增加很多
III.总结对比,I加载的时候LoadAssetBundle消耗明显大于II方式10倍(甚至不止,因为总量越大,差距就越大)。移动游戏中的内存多么珍贵大家懂得。使用ChunkBasedCompression,即LZ4压缩,更为划算。因为宁愿牺牲一点包体大小,也要消耗内存小一些。(我们游戏项目是这个需求,具体还是看项目吧)
当然还有其他打包选项,各有利弊,具体看游戏实际情况需求。我这里只是对比出了告诉你打包选项会决定你的打出的资源包体大小和游戏中加载消耗内存。很重要。
具体打包选项BuildAssetBundleOptions参考:https://blog.csdn.net/AnYuanLzh/article/details/81485762
补充:由上,我们知道打包时压缩方式会导致打出的包体和加载时的消耗都不同。加载时候的加载方式我们这边使用AssetBundle.CreateFromFile直接加载AssetBundle,Unity其实还提供了WWW加载AssetBundle的方式。但是这里不作详述了(讲不完...)。
五.总结
由上可知,文章虽说AB,Asset,GameObject三者联系,但是GameObject主要是由Asset实例化而来,GameObject是Asset的引用和复制的关系(主要引用),这个也可以说是Asset的一种。问题也就可以简化为AB和Asset之间的关系。
由上图,再总结一下,打包方式不同,加载方式不同,造成的消耗不同。即,如果想优化游戏中的资源,需要注意打包AB的方式,以及加载AB的方式。当然,还要注意AB的卸载,和Asset的卸载。
六.参考
关于加载AssetBundle和加载Asset的区别,详情就不多说了,见UWA:https://blog.uwa4d.com/archives/ABTheory.html
打包加密压缩算法区别参考:https://www.cnblogs.com/murongxiaopifu/p/5629415.html#autoid-3-3-0
附:测试Demo源码:链接:https://pan.baidu.com/s/1ZHPoQbuxgdUVh9PGbz6ArA 提取码:7lkv