本文转自Unity官方论坛:forum.china.unity3d.com
此前Unity官方技术支持工程师田彪为大家分享了Unity项目设计与管理的一些注意事项,其中最重要的莫过于资源加载与管理了。今天这篇文章将由Unity官方技术支持工程师柳振东,针对一些常见的Unity项目资源加载与管理问题进行解答。
将材质打包为AssetBundle,运行时使用LoadAsset加载材质也成功了,为什么还会出现材质丢失呢?
这也是开发者经常会遇到的问题。一般情况都是,开发者先把被依赖AssetBundle中的资源加载出来,然后用Unload卸载掉这个被依赖的AssetBundle,最后发现从其它AssetBundle中加载出来的GameObject丢失了前面加载出来的资源。
要弄明白这个问题,我们需要了解AssetBundle中的Asset在加载时寻找其依赖资源的规则。其实很简单,现在AssetBundle实现的机制是只会在内存中寻找其依赖资源所在的AssetBundle,并自动从中加载出所需的资源。
那么现在我们知道,其实通过LoadAsset从被依赖资源的AssetBundle中加载出来的资源,除非我们手动绑定回主体资源中(例如把加载出来的材质通过代码绑定回GameObject的Renderer中),否则是不会被自动索引到的。
也就是说,通过AssetBundle来动态加载资源时,我们并不需要自己加载被依赖的资源,而是只要保证主体在加载时被依赖资源所在AssetBundle依然处于开启状态就可以正常加载资源了。
为什么游戏切换到后台一段时间后再切换回来材质会变成粉色呢?
大家都应该知道,粉色材质在Unity中是Shader出错或丢失时的默认警示材质,而在这种情况下就是Shader丢失的问题。
首先我们要了解一个系统对于GPU资源管理的机制。在PC平台上,用户在锁屏一段时间后GPU会自动把部分后台程序的资源清除掉,而Android或者iOS这种移动平台系统一般会在切换到主界面或其它程序一段时间后进行类似的操作。
那么当Unity程序在GPU中的Shader和Texture等资源被清除之后再切换回该程序时,CPU端会接收到GPU Graphics Context 丢失的消息,然后Unity会尝试从内存中将丢失的资源重新加载到GPU端。
这时,如果原来的Shader和Texture资源是从AssetBundle中加载出来的,并且该AssetBundle已经被卸载掉的话,那么Unity就无法再从内存中加载到这些资源,从而导致GPU丢失Shader与Texture了。
可能有朋友会说,这些Shader和Texture不是曾经被加载到内存中吗?是的,但是它们在被加载到GPU之后会被从内存中清除掉。因此要防止这种问题的发生最稳健的办法就是Shader和Texture的AssetBundle在场景切换前都不要卸载掉。而如果担心AssetBundle本身消耗内存问题的话可以参考下一个问题的解答。
新的ChunkBasedCompression压缩方式相比原来的压缩方式有什么区别呢,应该如何取舍呢?
Unity默认的AssetBundle压缩方式是LZMA,这种压缩方式的AssetBundle在加载到内存时会马上执行解压过程,并在AssetBundle处于开启状态期间在内存中保留一个解压过后的Cache(例外情况:UnityWebRequest.GetAssetBundle与WWW.LoadFromCacheOrDownload均会在第一次解压LZMA AssetBundle时把解压后版本Cache到文件系统中,后续的调用就无需在内存中保留解压后的AssetBundle了),因此内存占用会比较明显。
而Unity5.3以后提供的ChunkBasedCompression是一种基于Chunk的LZ4压缩方式,这种压缩方式可以让AssetBundle对单独的Asset进行压缩,而不是AssetBundle整体压缩。因此加载这种压缩方式的AssetBundle时,无需事先解压,只需在内存中保留一个头结构,然后在加载某个Asset的时候才即时从文件中读取该Asset所在的chunk并解压到内存中。
由于LZ4是公认的解压速度极快的压缩方式,并且需要即时解压的数据量一般不会很大,因此实时解压Asset带来的CPU时间消耗其实很小,另一方面把解压时间分散在不同地方也减轻了一次性解压带来的卡顿问题。当然,最重要的优点还是在于大大减轻了内存的压力,可以放心地让有可能被再次索引的AssetBundle常驻在内存中,因此我们非常推荐大家使用LZ4压缩方式的AssetBundle。
不过大家在使用过LZ4压缩方式后应该也会发现,在资源数比较多的情况下,LZ4格式的AssetBundle大小基本都要比LZMA格式的大一些,这也是分块压缩不可避免的缺陷。但考虑到它的内存消耗表现优秀,移动端的开发朋友应该可以忽略这一点吧。
Resources文件夹里的资源越多,程序的启动画面时间就越长。这是为什么呢?
要理解这个问题,我们需要知道Unity程序启动时的一个操作。在启动加载的过程中,Unity需要为Resources文件夹(此时其实就是一个序列化文件)中的所有资源构建一个查找树作为后面加载具体Asset时所需的索引数据,而这个结构的构造时间是非线性的(比线性稍高一点),因此在Resources文件夹中的文件越多,启动加载的时间就越明显,基本10000个资源在一些低端手机上需要5到10秒的加载时间。
另一方面,考虑到Resources文件夹无法动态更新,也没有AssetBundle Variant这种设定不同资源版本的功能,因此我们极力推荐大家主要采用AssetBundle进行资源的动态加载,而Resources文件夹的使用可以只考虑这几种情况:
这些资源在整个游戏的运行期间都会用到;
这些资源无需为不同平台或硬件适配定制资源;
这些资源无需动态更新;
正在制作游戏的原型。
程序中主要使用AssetBundle.Unload(false)卸载AssetBundle,有时会发现AssetBundle中的资源在内存中会存在多份,这是为什么呢?
这种问题产生的根源在于从AssetBundle中加载出来的资源,在该AssetBundle卸载之后与此AssetBundle的联系就断开了。举个例子,我从AssetBundle A 中加载出来一个Prefab p1, 那么p1本身依赖的资源,例如一个Texture tex1也会自动加载到内存中。然后我用AssetBundle.Unload(false)来卸载AssetBundle A,此时p1与AssetBundle A已断开关系。之后过了一段时间,我需要从AssetBundle A中加载另一个Prefab p2 ,假设p2也依赖于Texture tex1,那么我再次加载AssetBundle A,从中加载p2时tex1会再次被加载到内存中,导致此时内存中存在两份tex1。
这个AssetBundle的资源索引策略我们官方在之后的版本中会进行修改以避免这种情况产生的内存消耗。而现阶段大家其实只要注意AssetBundle的卸载时机即可避免此情况的发生,即在保证当前场景中同一AssetBundle不会再被引用的时候卸载或者统一都在场景切换的时候使用Unload(true)进行卸载。
关于AssetBundle的常见问题解答就分享到这里,如果还有其它疑问,也可在下方评论区留言,或访问Unity官方中文社区(forum.china.unity3d.com)发帖提问。