上一篇《Unity引擎资源管理代码分析( 2 )》主要分析了Unity引擎的Object.Instantiate、Object.Destroy、Resources.UnloadUnusedAssets等接口的实现。本篇则着重分析AssetBundle相关的资源加卸载接口,并对所有的资源加卸载API优劣做一个简明的总结和对比。
前文中提到,使用Resources类的接口来单独卸载一个GameObject及其下子节点和挂接资源已经无望,那如果我们把一个或多个Prefab打包到一个单独的AssetBundle中,然后再通过AssetBundle来管理资源是否就可以达到加/卸载部分资源的目的呢?
假设我们已通过WWW类或AssetBundle.CreateFromFile等接口完成了AssetBundle本身的加载,让我们先来分析下从AssetBundle中加载资源的接口。(由于通过AssetBundle加载资源的代码跟上文联系更加紧密,因此有关AssetBundle加载的接口我们留到后续的章节中再具体讲解。)
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];
}
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文件。
接下来我们分析下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之间的依赖关系来顺序地执行加载和卸载操作。
在讲解这两个接口之前我们需要先了解下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,则峰值内存占用有可能达到三倍以上。
AssetBundle.CreateFromFile这个接口在Unity引擎内部的实现也是调用ExtractAssetBundle函数,但是不同于AssetBundle.CreateFromMemory接口调用的传入UnityWebStream对象的ExtractAssetBundle函数,它调用的是传入文件路径字符串参数的重载版。这个重载版的ExtractAssetBundle函数会直接通过文件系统API读取AssetBundle文件头,并判断其是否为压缩格式的AssetBundle。如果为压缩格式则直接报错返回。如果是非压缩格式则在PersistentManager中建立映射到磁盘文件的SerializedFile对象,而并非一次性地将全部文件数据读取到内存中。这样做的好处是即用即读,不会造成过大的内存开销。
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
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。