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 条评论
登录 后参与评论

相关文章

来自专栏向治洪

Volley请求

1. Volley简介 我们平时在开发Android应用的时候不可避免地都需要用到网络技术,而多数情况下应用程序都会使用HTTP协议来发送和接收网络数据。A...

1857
来自专栏iKcamp

如何优雅的设计 React 组件

如今的 Web 前端已被 React、Vue 和 Angular 三分天下,一统江山十几年的 jQuery 显然已经很难满足现在的开发模式。那么,为什么大家会觉...

800
来自专栏码洞

Github上最受欢迎的Python框架Flask入门

flask最近终于发布了它的1.0版本更新,从项目开源到最近的1.0版本flask已经走过了8个年头。

2922
来自专栏云飞学编程

怎么让代码更Pythonic?光有技巧可不行,你还需要看这些

写代码如同写文章,好的文章是反复修改出来的,代码也同样是反复的重构出来的。今天给大家分享下,怎么从一个编程学习者变为一个程序猿(程序媛)!起码不要让别人一看你的...

973
来自专栏前端小吉米

Android 和 Webview 如何相互 sayHello(一)

在移动时代 Web 的开发方式逐渐从 PC 适配时代转向 Hybird 的 Webview。以前,我们只需要了解一下 PC Chrome 提供的几个操作行为,比...

1063
来自专栏章鱼的慢慢技术路

牛客网_Go语言相关练习_p判断&选择题(5)

使用第三方库时,先将源码编译成.a文件放到临时目录下,然后去链接这个.a文件,而不是go install安装的那个.a文件;

572
来自专栏大魏分享(微信公众号:david-share)

如何使用模拟框架测试微服务? | 微服务系列第八篇

作为开发人员尝试创建集成测试时,会遇到许多复杂问题。出现的两个最常见的问题包括与:

1942
来自专栏大内老A

.NET的资源并不限于.resx文件,你可以采用任意存储形式[上篇]

为了构建一个轻量级的资源管理框架以满足简单的本地化(Localization)的需求,我试图直接对现有的Resource编程模型进行扩展。虽然最终没能满足我们的...

2107
来自专栏向治洪

React Native运行原理解析

Facebook 于2015年9月15日推出react native for Android 版本, 加上2014年底已经开源的IOS版本,至此RN (reac...

5658
来自专栏你不就像风一样

[原创]一款小巧、灵活的Java多线程爬虫框架(AiPa)

AiPa 只需要使用者提供网址集合,即可在多线程下自动爬取,并对一些异常进行处理。

543

扫码关注云+社区