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

相关文章

来自专栏有趣的django

面试题目及答案

1 Python的函数参数传递 看两个例子: a = 1 def fun(a): a = 2 fun(a) print a # 1 a = [] de...

9979
来自专栏xingoo, 一个梦想做发明家的程序员

Elasticsearch聚合 之 Terms

之前总结过metric聚合的内容,本篇来说一下bucket聚合的知识。Bucket可以理解为一个桶,他会遍历文档中的内容,凡是符合要求的就放入按照要求创建的桶...

3356
来自专栏张俊红

python数据分析笔记——数据加载与整理

Python数据分析——数据加载与整理 总第47篇 ▼ ? (本文框架) 数据加载 导入文本数据 ? 1、导入文本格式数据(CSV)的方法: 方法一:使用pd....

3618
来自专栏wblearn

面向切面编程AOP

AOP(Aspect-Oriented Programming):面向切面的编程。OOP(Object-Oriented Programming)面向对象的编程...

602
来自专栏Java Edge

高性能队列——Disruptor总论1 背景2 Java内置队列3 ArrayBlockingQueue的问题4 Disruptor的设计方案代码样例性能等待策略Log4j 2应用场景

这里所说的队列是系统内部的内存队列,而不是Kafka这样的分布式队列 Disruptor特性限于3.3.4

1173
来自专栏绿巨人专栏

Scala on Visual Studio Code

3698
来自专栏分布式系统和大数据处理

C#网络编程(同步传输字符串) - Part.2

在与服务端的连接建立以后,我们就可以通过此连接来发送和接收数据。端口与端口之间以流(Stream)的形式传输数据,因为几乎任何对象都可以保存到流中,所以实际上可...

1133
来自专栏fixzd

redis系列:通过队列案例学习list命令

这一篇文章将讲述Redis中的list类型命令,同样也是通过demo来讲述,其他部分这里就不在赘述了。

982
来自专栏刘笑江的专栏

iOS 私有 API 调用检测机制探讨

最近发现部分 App 以字符串拼接的方法调用私有 API,在提交 AppStore 审核后被发现打回修改的案例。

3364
来自专栏草根专栏

使用xUnit为.net core程序进行单元测试(1)

一. 导读 为什么要编写自动化测试程序(Automated Tests)? 可以频繁的进行测试 可以在任何时间进行测试,也可以按计划定时进行,例如:可以在半夜进...

3175

扫码关注云+社区