Unity 引擎资源管理代码分析( 3 )

前言:

上一篇《Unity引擎资源管理代码分析( 2 )》主要分析了Unity引擎的Object.Instantiate、Object.Destroy、Resources.UnloadUnusedAssets等接口的实现。本篇则着重分析AssetBundle相关的资源加卸载接口,并对所有的资源加卸载API优劣做一个简明的总结和对比。

6. AssetBundle.Load、AssetBundle.LoadAll

前文中提到,使用Resources类的接口来单独卸载一个GameObject及其下子节点和挂接资源已经无望,那如果我们把一个或多个Prefab打包到一个单独的AssetBundle中,然后再通过AssetBundle来管理资源是否就可以达到加/卸载部分资源的目的呢?

假设我们已通过WWW类或AssetBundle.CreateFromFile等接口完成了AssetBundle本身的加载,让我们先来分析下从AssetBundle中加载资源的接口。(由于通过AssetBundle加载资源的代码跟上文联系更加紧密,因此有关AssetBundle加载的接口我们留到后续的章节中再具体讲解。)

AssetBundle.Load的对应C++函数实现

Object* LoadNamedObjectFromAssetBundle (AssetBundle& bundle, const std::string& name, ScriptingSystemTypeObjectPtr type)
{

    string lowerName = ToLower(name);
    AssetBundle::range found = bundle.GetPathRange(lowerName);

    vector<Object*> result;
    ProcessAssetBundleEntries(bundle,found,type,result,true);
    if (result.empty())
        return NULL;

    return result[0];
}

AssetBundle.LoadAll的对应C++函数实现

void LoadAllFromAssetBundle (AssetBundle& assetBundle, ScriptingSystemTypeObjectPtr type, vector<Object* >& output)
{
    AssetBundle::range found = assetBundle.GetAll();
    ProcessAssetBundleEntries(assetBundle,found,type,output,false);
}

由代码可见,这两个函数其实最终都是通过ProcessAssetBundleEntries这个内部函数来加载AssetBundle内的资源对象的。只不过在函数LoadNamedObjectFromAssetBundle中,是先通过GetPathRange函数根据资源名称收集了同名的对象列表。而在函数LoadAllFromAssetBundle中,则是粗暴地获取了所有对象的列表。

注意这个GetPathRange函数的实现很像我们在讲解Resources.Load接口时提到的GetPathRange函数,它会获取所有小写同名的Object对象,而不论类型是否相同。但在对象加载完成后,LoadNamedObjectFromAssetBundle函数却只返回了数组中的第一个Object对象。而此时其它的同名对象其实也已经被加载了,白白浪费了时间。甚至有可能加载上来的对象并不是我们想要的那个对象,从而产生错误。

ProcessAssetBundleEntries函数的内部实现则非常的简单,它只是遍历了下每个AssetBundle对象中包含的PPtr对象列表,然后通过Object.IsValid()函数去强制访问其C++指针,从而调用了PPtr::operatorT* () const这个指针引用重载操作符。接下来的实现就和Resources.Load一样,在InstanceID to Pointer的全局对象列表中没有找到这个对象的C++实例,如果没找到则通过PersistentManager去加载它。只不过在PersistentManager中这个对象对应的SerializedObjectIdentifier文件标识符指向了包含它的AssetBundle文件。

7. AssetBundle.Unload

接下来我们分析下AssetBundle.Unload接口。这个接口并没有用来指定具体需要卸载哪个资源的参数,而是只有一个用来控制是否要卸载AssetBundle内所有对象的参数bool unloadAllLoadedObjects。

AssetBundle.Unload对应的C++函数为UnloadAssetBundle,根据unloadAllLoadedObjects参数的不同,它的执行流程有所不同。

当unloadAllLoadedObjects为true时,这个函数会通过PersistentManager将所有关联到这个AssetBundle文件(SerializedFile)的对象全部删除,无论这些对象还有没有被别的对象所引用。该函数不会删除文件数据,可以用来卸载资源对象。

当unloadAllLoadedObjects为false时,则只会从PersistentManager中删除所有对象到这个AssetBundle文件的关联关系,而不删除对象本身。

之后的流程则无论unloadAllLoadedObjects参数如何都一样,删除AssetBundle对应的SerializedFile对象,清空所有的两进制文件数据流。

除此之外,当我们加载多个存在依赖关系的AssetBundle时会有特殊的情况出现。例如我们打了两个AssetBundle,AB1和AB2,AB1中包含Mesh和Texture,AB2中包含引用这个Mesh和Texture资源的GameObject(Prefab)。但由于此时在PersistentManager中,Mesh和Texture资源是关联到AB1中的,而GameObject是被关联到AB2中的。因此当我们加载时必须先加载AB1、再加载AB2,如果先加载AB2则会找不到对应的加载文件。而当我们卸载时,如果只卸载了AB2,则只会卸载GameObject,Mesh和Texture不会被卸载。如果先卸载了AB1,会发现GameObject下的Mesh和Texture对象已经变为了null。所以在使用AssetBundle时必须严格遵照AssetBundle之间的依赖关系来顺序地执行加载和卸载操作。

8. AssetBundle.CreateFromMemory、AssetBundle.CreateFromMemoryImmediate

在讲解这两个接口之前我们需要先了解下UnityWebStream这个Unity引擎内部的C++类,它有两个主要功能:

A. 当我们使用网页平台的Unity引擎客户端时,(也就是通过UnityWebPlayer呈现游戏内容)UnityWebSream负责从网上下载AssetBundle的原始数据。(通过Unity引擎自己实现的下载代码)

B. 使用单独的线程将AssetBundle的原始数据解压缩,并保存在其中。(如果输入是压缩格式的AssetBundle。)

在Android和iOS平台上,实际上只有UnityWebPlayer的AssetBundle解压缩功能是发挥作用的。而AssetBundle.CreateFromMemory和AssetBundle.CreateFromMemoryImmediate这两个接口就是通过UnityWebPlayer这个类来完成AssetBundle数据的加载和解压缩的。

AssetBundle.CreateFromMemoryImmediate的C++函数执行流程如下:

1) 直接在主线程中new了一个UnityWebPlayer对象,并将传入的AssetBundle内存数据填充到其中。

2) 启动UnityWebPlayer类自己创建的异步解压缩线程,然后在主线程中等待其解压完成。

3)解压完成后,调用ExtractAssetBundle这个函数,将包含已解压数据的UnityWebPlayer对象传入其中,并使用其已解压的数据在PersistentManager中建立对应的SerializedFile内存流对象。

4)完成AssetBundle对象的初始化,建立其中Object和SerializedFile对象的数据映射关系。

AssetBundle.CreateFromMemory的C++函数执行流程如下:

1) 创建了一个继承于PreloadManagerOperation基类的AssetBundleCreateRequest异步操作执行对象,并加入异步操作执行队列。(忘记PreloadManagerOperation实现原理的读者请参阅前文中关于Resources.UnloadUnusedAssets接口的相关说明。)

2) 在AssetBundleCreateRequest的构造函数中new一个UnityWebPlayer成员对象,然后将内存数据复制到其中。

3)开启UnityWebPlayer对象的AssetBundle数据异步解压缩线程。(如果需要解压缩)

4)在PreloadManager创建的异步处理线程中调用AssetBundleCreateRequest对象的Perform函数,并在Perform函数中等待UnityWebPlayer的异步解压缩线程完成其解压工作。

5)Perform函数执行完毕后,PreloadManager会在主线程中再次调用AssetBundleCreateRequest的IntegrateWithMainThread函数,并在其中调用ExtractAssetBundle函数。

6)调用ExtractAssetBundle函数,其内部执行步骤与AssetBundle.CreateFromMemoryImmediate相同。

由此可以看出,和AssetBundle.CreateFromMemoryImmediate相比,AssetBundle.CreateFromMemory函数只是没有在当前帧阻塞式地等待AssetBundle数据的解压缩过程,其它的实现是基本相同的。

由于这两个函数都会在UnityWebStream对象内复制一份原始的AssetBundle数据,因此算上传入数据的原始空间占用,它们的峰值内存占用都至少在AssetBundle原始数据容量的两倍以上。如果是压缩的AssetBundle,则还要分配解压缩Buffer,则峰值内存占用有可能达到三倍以上。

9.AssetBundle.CreateFromFile

AssetBundle.CreateFromFile这个接口在Unity引擎内部的实现也是调用ExtractAssetBundle函数,但是不同于AssetBundle.CreateFromMemory接口调用的传入UnityWebStream对象的ExtractAssetBundle函数,它调用的是传入文件路径字符串参数的重载版。这个重载版的ExtractAssetBundle函数会直接通过文件系统API读取AssetBundle文件头,并判断其是否为压缩格式的AssetBundle。如果为压缩格式则直接报错返回。如果是非压缩格式则在PersistentManager中建立映射到磁盘文件的SerializedFile对象,而并非一次性地将全部文件数据读取到内存中。这样做的好处是即用即读,不会造成过大的内存开销。

10. 通过WWW加载AssetBundle

WWW类的功能是根据URL地址下载原始AssetBundle数据,它的内部实现为libcurl,(http://curl.haxx.se/libcurl/) 一个第三方的基于URL的数据传输库。当我们通过new WWW(“Your URL address”);这行代码创建一个WWW对象时,Unity底层就会创建一个WWWCurl类的C++对象,并开启一个单独的线程调用libcurl的API进行AssetBundle数据的传输。(如果URL地址为“file:// ”开头的本地文件地址libcurl会自动进行磁盘文件的读取。)当所有数据传输完毕后,WWWCurl类会创建一个UnityWebStream对象,传入AssetBundle的内存数据,并启动UnityWebStream的解压缩线程开始进行解压缩操作。

这里我们需要注意的是,如果在new完WWW对象后不对www.assetBundle 属性进行任何访问,Unity引擎则不会等待WWW对象传输完AssetBundle数据,更不会等待UnityWebStream对象的解压缩线程结束。只有在第一次尝试访问www.assetbundle 属性时,Unity引擎才会调用C++底层的WWW_Get_Custom_PropAssetBundle函数,开始阻塞式地等待UnityWebStream解压完成,之后再通过ExtractAssetBundle函数创建真正的AssetBundle对象。因此我强烈建议大家在游戏场景资源加载完成之前,对所有的www.assetbundle 对象进行一次显式的访问,(例如 var forceToLoadAssetBundle = www.assetBundle;) 以完成AssetBundle的加载。

最后提醒大家,由于Unity的WWWCurl类只有在它的析构函数中才会真正释放掉为AssetBundle分配的数据内存。而在Mono的C#实现中,如果不显式调用WWW的Dispose接口,则只有在自动执行垃圾回收时才会真正删除C++的WWWCurl对象,并调用其析构释放掉分配的内存。所以建议大家不要同时创建多个WWW对象进行AssetBundle的加载,而应该通过队列把加载工作都放到一个协程内来进行。

##四、总结

前文中对Unity 4.x版本引擎中常用的资源加卸载接口实现逐一进行了分析,下面我们从应用角度对各类API的优劣及适用性进行一个简明的概括,以方便大家对比和使用。

最后感谢大家耐心地阅读完了本文,作者我表示感激涕零。由于时间紧张还没有深入地对Unity源码的每一处实现细节都做出完整的分析,如有疏漏敬请提出!@cobyli

原创声明,本文系作者授权云+社区-专栏发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏大数据人工智能

ZStack--工作流引擎

在IaaS软件中的任务通常有很长的执行路径,一个错误可能发生在任意一个给定的步骤。为了保持系统的完整性,一个IaaS软件必须提供一套机制用于回滚先前的操作步骤。...

4184
来自专栏jouypub

HTTP和RPC的优缺点

在HTTP和RPC的选择上,可能有些人是迷惑的,主要是因为,有些RPC框架配置复杂,如果走HTTP也能完成同样的功能,那么为什么要选择RPC,而不是更容易上手的...

1093
来自专栏aCloudDeveloper

vhost:一种 virtio 高性能的后端驱动实现

什么是 vhost vhost 是 virtio 的一种后端实现方案,在 virtio 简介中,我们已经提到 virtio 是一种半虚拟化的实现方案,需要虚拟机...

4826
来自专栏Rainbond开源「容器云平台」

微服务架构的设计模式

1336
来自专栏A周立SpringCloud

Spring Cloud各组件配置属性总结

我们知道,Spring Cloud是个工具集,整合了各种组件。有的组件Spring Cloud是拿来主义,有的组件Spring Cloud又进行了一些增强(例如...

3355
来自专栏Java技术栈

Redis 的 4 大法宝,2018 必学中间件!

Redis是什么? 全称:REmote DIctionary Server Redis是一种key-value形式的NoSQL内存数据库,由ANSI C编写,遵...

3555
来自专栏zhangdd.com

亿级请求下多级缓存那些事 转载

摘要: 什么是多级缓存 所谓多级缓存,即在整个系统架构的不同系统层级进行数据缓存,以提升访问效率,这也是应用最广的方案之一。我们应用的整体架构如图1所示: 图1...

653
来自专栏Java架构师历程

3、进程间通信

本书主要介绍如何使用微服务架构构建应用程序,这是本书的第三章。第一章介绍了微服务架构模式,将其与单体架构模式进行对比,并讨论了使用微服务的优点与缺点。第二章描述...

442
来自专栏嵌入式程序猿

ARM cortex 内核编程模式

ARM cortexM4 内核的编程模式,处理器模式和软件执行的特权级别简介 处理器模式 处理器模式包含: 线程模式:常用来执行应用软件,处理器复位后,进入线...

3649
来自专栏原创

安卓推送技术手册——使用透传消息的正确姿势

目前的消息推送方式主要有两种:通知和透传。 什么是透传?透传即是透明传送,即传送网络无论传输业务如何,只负责将需要传送的业务传送到目的节点,同时保证传输的质量即...

3856

扫码关注云+社区