浅谈Unity的资源管理部分
作者:张昊
前言
Unity资源管理部分的功能很强大,会帮你做好很多东西,但是由于Unity设计目标是超级通用性,所以很多时候你要更多去了解其原理,才能保证使用的正确性和高效性,否则很容易掉坑里。
这里我结合以前看过的Unity4.2的源代码和部分文档,浅谈下Unity的资源管理部分的一些东西。常言道:“庖丁解牛,恢恢乎”,个人认为源代码是唯一没有二义性的文档。
3种资源读取方式
1. Resources:支持编辑器模式和发布模式
2. AssetDatabase :仅仅支持编辑器模式
3. AssetBundle: 支持编辑器模式和发布模式
接下来,让我们看下这3种资源获取方式的优缺点。
Resources
你可以认为,unity给你提供了一个特定的包Resources。在编辑器模式下,它以文件夹的形式存在,你可以随意放置里边的散文件。在打包过程中,unity则自动帮你把Resources文件夹的所有东西打包。
优点
开发时绝对的方便。
缺点
Resources文件夹下的任何一点点修改,都会导致Resources包的重打。
AssetDatabase
优点
AssetDatabase 在编辑器模式下超级方便。
缺点
在正式发布版不能用,要用AssetBundle来替换。
AssetBundle
优点
你可以根据自己的项目实际情况自定义打包粒度,很好的支持热更新,就连unity 4.0里被大家吐的最多的槽点——资源依赖问题在unity 5.0以上也被完美的解决了。
缺点
AssetBundle从第一天出生就不是给开发环境用的,如果要修改贴图,每一张都得打包再测试。(信不信愤怒的美术同学过来找你,呵呵。)
3种资源获取方式的优缺点总结
1. Resources :适合开发模式,不适合热更。
2. AssetDatabase:仅仅支持编辑器模式,在发布版本要用AssetBundle 取代。
3. AssetBundle:很好的支持热更,适合发布模式,但开发模式千万别用。
资源读取异步的支持
unity引擎内部加载异步管理器(C++类: PreloadManager )负责处理所有的异步加载请求(C++类:PreloadLevelOperation,继承自C++ 类AsyncOperation )。
注意
1. ResourceRequest 和 AssetBundleRequest 继承自C#的 AsyncOperation,此 AsyncOperation非彼AsyncOperation也。
2. C#的 AsyncOperation和 C++ 的 AsyncOpera tion名字相同,但这是两个类,C#的单向关联C++。
Application.backgroundLoadingPriority 就是操作的 PreloadManager。(网络上有些文章说unity的异步机制是伪多线程,这是不实的。)PreloadManager的确是多线程的。
建议
包装下unity的异步操作,用一个优先级桶排队来push引擎异步指令。
每次按照优先级从上述的指令桶中pop,1个或几个指令,按照指令创建AsyncOperation给引擎,当这些AsyncOperation都执行完,再重复操作。这样既可以抑制创建AsyncOperation风暴,又可以做到自定义优先级。
不建议
不要短时间push给unity太多的异步操作,这样会加重unity的负担,同时也不利于我们控制资源的加载优先级。
AssetBundle 爬坑记
这里我重点说下AssetBundle的对外提供的api以及原理。 (AssetBundle的坑点很多,好多人都掉入过。)
Asset是一堆文件的集合,我们可以选择把几个文件(Asset)打包成一个Bundle。Bundle的文件内容包括 所有Asset文件信息索引列表和所有Asset 的data。
索引列表 (伪代码)
你可认为,一个 AssetBundle文件的格式形如(伪代码):
加载一个 Asset:
那如何加载一个AssetBundle ?
Unity 给你提供了4种方式( 注意,坑点来了)
第 1 种方式
第 1 种方式,看着很土。但我告诉你,它只加载文件索引部分到内存,很小的内存啊!你实际加载asset的时候,它才从文件中IO对应的数据块。
第 2 种方式
第 2 种方式,看着很爽(还有个从内存流创建的异步接口)。因为灵活,你给它传递个数据流就可以了,你可以做自定义bundle 加密等等。(你想怎么折腾怎么折腾)
但是没有文件依托,所有的bundle数据是展到内存里的(哈哈)。等于用内存做了文件的事情!用这种方式再去加载asset的时候会比第一种方式快,代价是牺牲了大量的内存。
第 3 种方式
第 3 种方式,利用http从网络上下载bundle数据,然后将其展到内存里,这和方式二处理文件内存方式类似。
这种方式比较适合网页游戏的开发,或用来做下载。如果用来做游戏运行时的bundle,问题和二一样。
第 4 种方式
第 4 种方式,非常不灵活,它集成了三的下载部分。加载bundle也是只加载文件列表(和一类似),注意版本号大于上次的版本号才会下载更新。url可以是网络地址,也可以是本机地址。
它在使用内存上和一类似,但是函数不够原子,集成了太多的东西。很难用,所以一般被一取代。
4.2的源码,前边两个为二、三服务,最后一个为一、四服务。(嘿嘿...)
做手游开发,内存是金贵的。(使用二和三的同学,小心进坑阿。)如果你自定义加密资源的需求强烈过内存的占用,你可以把bundle的粒度打得足够小,调用方式二,然后及时的卸载bundle。
有人说,使用第一种方式固然省,但如何做资源压缩和文件内容加密呢?
Unity 5.4可以帮你不压缩文件列表,只压缩Asset的数据。强烈建议Unity在LoadAsset提供口子,允许我们这群码农能够自定义数据过滤器。这样我就可以作自定义加解密等。
使用 AssetBundle 还要注意Bundle的加载依赖,否则会有资源丢失的问题。这块Unity 5.0改进的非常好,并且网上文章很多,这里就不详细介绍了。
资源卸载
卸载第 1 种
当我第一次看见这玩意的时候非常兴奋,因为觉得终于可以单点式释放资源了,无限大地图要求的平滑释放资源的接口有了。
此函数只能释放资源文件,非GameObject文件,否则会报“UnloadAsset may only be used on individual assets and can not be used on GameObject's / Components or AssetBundles”。
另外,我之前在unity5.2.1里测试,发现用Resources.load 加进来的Texture,如果你调用了此函数卸载,然后想通过load再加载回来,是加载不回来的了,编辑器模式下还要重启Unity编辑器才能重新加载此Texture,后来我就再也没用过它了。
大家可以在最新的unity版本测试下此函数。
卸载第 2 种
这个函数才是unity希望你调用的卸载资源的函数。因为unity是基于资源引用的卸载策略。(越过引用的暴力卸载函数显然有些任性了)
Resources.UnloadUnusedAssets( ) 这个函数返回的AsyncOperation,你可能觉得是在异步处理。(开始我也觉得很不可思议,因为这玩意在多线程里怎么写,各种状态都会乱啊!我还很有兴致的扒了下4.2的源码,发现这真的是很奇特的事情。)
多线程部分的函数为空函数, 主线程同步的函数里做了所有的事情,然后进度从0瞬间跳到1。这和主线程调了个函数不是一样吗?
是不一样的。AsyncOperation是在PreloadManager线程中排队执行的,所以UnloadUnusedAssets放在异步的主线程同步函数里执行,是为了之前队列中的AsyncOperation全部执行完毕再执行UnloadUnusedAssets。
这样防止同时加载和删除的情况,unity在这点做的十分巧妙。也所以说UnloadUnusedAssets不是立即执行的。
这函数很耗,主要开销在两方面,标记资源依赖(MarkAllDependencies)和资源释放。之前我在手机上作过测试,在cache所有资源的情况下,每帧调用UnloadUnusedAssets,所有开销100%是GC.MarkDependencies,红米note2 的 fps只有7~8。所以Resources.UnloadUnusedAssets的调用频率不要太高,而且过一段时间必须要调一次,否则资源放不掉,内存会爆。
尾声
很多人说,unity对新手友好。这话没错,但想用好unity却那么容易的。
侯捷先生在《深入浅出MFC》的书里写到:“勿在浮沙筑高台”,末了我想引用这句话。真正要想把东西做精,基础、内功、 掌控原理才是最重要的。
