前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Unity 引擎资源管理代码分析( 3 )

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

原创
作者头像
李海辰
修改2017-11-17 18:03:39
3.4K2
修改2017-11-17 18:03:39
举报
文章被收录于专栏:李海辰的专栏李海辰的专栏

前言:

上一篇《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++函数实现

代码语言:javascript
复制
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++函数实现

代码语言:javascript
复制
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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言:
  • 6. AssetBundle.Load、AssetBundle.LoadAll
    • AssetBundle.Load的对应C++函数实现
      • AssetBundle.LoadAll的对应C++函数实现
        • 7. AssetBundle.Unload
          • 8. AssetBundle.CreateFromMemory、AssetBundle.CreateFromMemoryImmediate
            • 9.AssetBundle.CreateFromFile
              • 10. 通过WWW加载AssetBundle
              相关产品与服务
              腾讯云代码分析
              腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,支撑团队传承代码文化。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档