Unity 打AssetBundle和加载方案

一、如何组织assetBundle:

unity5以前,打包需要自己去找依赖,然后需要按照拓扑图顺序压入AB栈,这样在最后打AB时才能有效利用依赖(栈内已有的AB才能作为依赖)。

unity5.x后,打包变得简单,但如何组织assetBundle依然需要琢磨和规划。
  首先我们需要知道:AB、asset和资源的关系:一个AB包括1个或多个asset,一个asset可能没有依赖其他asset(即包括了其需要的所有资源),也可能依赖其他asset(包括一些资源和指向其他asset所在AB的引用)。
  其次,使用asset和AB:要使用一个asset,需要先加载该asset的所有依赖的asset(迭代下去,把所有相关的AB都加载出来),然后加载自己的AB,最后取出asset来使用。
  所以,要使用一个asset,都需要加载所有相关的AssetBundle镜像进入内存。这就需要我们要研究打ab的方案了。并且打ab的方式也影响ab的总大小,即使在Unity5下。
  文档提供了3种打ab方案:
    1.逻辑实体分类法,即根据功能模块分类,(大体上意思是不同功能不同ab,这样就可以开一个功能下载一个ab)比如先大层次分:UI模块,角色模块,场景模块等,
     UI模块又根据具体功能模块分:比如各个副本的UI是不同的,那每个副本的UI可以打成单独ab;
     角色模块则是每个角色包括其依赖的东西,如mesh,model,animation,texture等打成一个AB;
     场景模块可以一个场景分成多个片,每个片相当于一个“子模块”,单独成ab,多个场景共享片。
    2.根据资源类型打ab:比如音效打成一个ab,国际化文本打成1个ab等,文档说这种方法是打多平台ab的一种好方法,比如音效ab在各个平台下都一样则可以复用,而shader平台区分则不能复用
     并且在此方法下,因为原始资源(比如Texture)很少改动,所以利于版本更新(增量少)。这种方法我也不是很理解,感觉就是把ab粒度细化到原始资源层次(纹理,音效,网格等),他们有的在不同平台保持一样,有的平台区分,但基本不会发生改变。
    3.根据“同时加载”来打ab,即把需要同时加载的资源打入同一个ab.比如每个场景包括其依赖的所有东西打成一个AB。
  无论使用什么方案(一般是都使用一点点,比如第3种用来打场景,第1种用来打各个系统(即功能)的资源,等),为了减少AB大小,增量AB大小,加载时间和运行时内存消耗,都需要注意下面细节:
    1.频繁变动的asset不应该和稳定的asset打入一个AssetBundle;小增量AB
    2.如果稳定的大/多asset中含有频繁变动的资源,应该把资源单独打AB;小增量AB
    3.使用时一般同时加载的东西如Model和它的texture,material,animation等应该打入一个AssetBundle中,所以角色一般每个一个AB;减少加载时间
    4.如果多个AB都依赖某个asset或资源,应该把该asset或资源单独打AB;小AB,省内存
    5.如果2个asset极少同时加载,那应该分开打AB;省内存
    6.把经常同时加载的小asset的AB合并成大AB;减少加载时间

 

二、打AB的参数选择和加载AB的方案:

  1.清单:

    1)打AB时,每个AB都会额外带一个.manifest文件,用来描述该AB相关信息,包括
      CRC码,(file hash)a single hash for all assets in AB,(type tree hash)a single hash for all types in AB,(class types)all class types in AB,(asset names)all asset paths in AB.
      此文件只用于生成增量AB, not necessary for runtime,所以不需要打入安装包,依赖关系可以在2)那里统一查找。
    2)并且在打AB的输入参数中的“输出文件夹”那里生成一个.manifest文件,里面包括所有AB路径及其依赖,这个在运行时加载资源时使用,因为加载资源需先加载依赖。

  2.AB的压缩方式

  AB有3种压缩方式(当然你也可以自定义压缩算法,不过没必要),一种是无压缩UncompressedAssetBundle,一种是块压缩LZ4(ChunkBasedCompression),一种是最大压缩LZMA(None)
加载时无压缩最快,其次是块压缩(只需要解压AB的一部分就能加载,与无压缩具有可比性),最后是LZMA(需要完全解压AB),当然打出的AB大小就反过来了(LZMA:50-60%,LZ4:70%)。
一般本地AB建议以LZ4格式存储,较小且快,需要网络下载的用LZMA并使用LoadFromCacheOrDownload对其加载,这样到了本地就变成LZ4了(LoadFromCacheOrDownload在接受数据流时就直接解压,无额外耗时)。

  3.AB加载的几个接口

    1)加载AB文件(这里是指build出来后的AB文件,如果build后还进行加密等则不能直接加载)有几种方式:
    WWW:输入URL,AB在www.assetBundle里;PS:如果ab在本地,url用file:///前缀
    WWW.LoadFromCacheOrDownload:输入URL,AB在www.assetBundle里;PS:如果ab在本地,url用file:///前缀
    LoadFromMemory(Async):输入是bytes[],返回的就是AB;PS:bytes的获取一般用WWW,也可以直接File.ReadAllBytes,反正获得文件的字节流即可。
    LoadFromFile(Async):输入是path,AB在AssetBundleCreateRequest.assetBundle里;

    UnityWebRequest:文档说此方法比WWW好,用法也类似,但因为WWW比较成熟,而且WWW我们基本只是用来加载本地bytes资源,而且此方法在5.4以后版本才有,我们没有使用。
在网络资源下载方面我们项目用的是C#提供的HttpWebRequest
    2)加载非.assetBundle文件例如.bytes,.jpg,.png,.txt,.ogg,.mp3等文件一般使用WWW加载:
    WWW.bytes,WWW.texture,WWW.text,WWW.audioClip里分别存加载后的数据。

    如果原先是.assetBundle,加密成.bytes,先www加出bytes,然后loadfrommemory;

    如果原先是.bytes或者其他自定义数据类型,如xml,json等,打ab成.assetBundle,则先加出assetBundle,然后加载出Asset,最后强转为TextAsset类型,然后你的数据就可以从中取出来了,Unity经常把这些数据看成TextAsset类型;

    简而言之,对特殊文件格式,我们使用之前先测试一下其加载方式,基本在WWW和TextAsset这块考虑,看数据在它们哪个变量里,一测便知。

  4.针对2和3介绍的多种方式,按情况选择压缩方式+加载接口

  文档根据各个函数对内存和CPU消耗的特点做出以下建议:
  1.对于安装包内的AB,尽量用LoadFromFile(Async)+ChunkBasedCompression,因为它和ChunkBasedCompression配合起来很快(可以压缩30%+而且加载时不需要额外解压),如果AB的压缩格式是LZMA(即Option=None),LoadFromFile(Async)需要额外解压和重压缩(压为LZ4);
  2.对于网络资源,尽量用WWW.LoadFromCacheOrDownload,并且资源尽量用默认的LZMA压缩法,因为这样可以省流量,并且此函数在接收网络流时就对流进行解压,为LZ4(即AB压不压对其无区别),而加载LZ4不需要额外解压。
  3.对于加密的AB,需要用WWW把文件加载成bytes,然后解密,然后用LoadFromMemory[Async]加载出AB(这个大概是LoadFromMemory[Async]使用的唯一场景了)
  4.如果使用自定义压缩算法,选UncompressedAssetBundle不压缩来build AB,然后加载时先对数据解压,然后LoadFromFile(Async)
  总结:本人当前项目是用LZMA build AB(不使用LZ4的原因可能是没深入了解,也可能是贪图LZMA的小包体,也可能是加载的多半是小ab,LZMA解压再压LZ4的消耗可以忍受),用LoadFromFile(Async)加载.assetbundle,WWW+LoadFromMemory(Async)加载加密过的ab.bytes,用WWW加载原始图片。

 

三、如果管理AssetBundle

  1.AssetBundle.unload(true/false)的用法:
  true会把AB,由AB加出来的Asset,由Asset实例化出来的GameObj(也可能只是指向Asset的引用)都销毁,内存一干二净;
  false只是把AB卸载,官方文档描述为把AB和Asset,Gobj的link断开,即后者们自己删除自己,如果再次加载Asset则是新的Asset,与原来的Asset独立共存于内存;
  这里需要明确两点:
  1.AssetBundle加载出来后没有Unload则其镜像一直在内存,再次加载会报错,可以unload掉重新加载或者hold住它,不重新加载;
  2.false时要卸掉Asset需要把由它实例化出来的Gobj都销毁(引用则置空),然后Resource.unloadunusedassets卸载,Gobj则destroy即可;
  官方文档建议选择true,true调用的时机可以是切地图或者Asset引用为0时,如果选择false,那Asset只有在2的情况下才能卸掉。
  但官方文档没说的一点是在调用unload(true)前如果再次加载应该怎么办。
    1.其实只需在上层存个Pool即可,Pool里的元素对应一个AssetBundle,加载AB时先看Pool里有没有,有则直接使用它来GetAsset和实例化,并把引用+1,当实例化的东西Destroy时把该AB的引用减一,当引用变为0时unload(true)即可。
    2.但这种方式主要用来处理实例化的GameObject资源,对某些资源比如AudioClip,Text,场景,自定义数据等一般很少频繁加载,这样我们在加载AB出来后直接unload(false),然后当它不使用时,对应的Asset引用就空了,就可以在UnloadUnusedAsset时卸载(切场景时默认调用一次,一般在程序中定时调用一次)
    3.官方文档之所以建议使用unload(true),是想避免内存存多份Asset和Asset不能及时卸载,如果资源是很少加载如2中那些,用unload(false)较好,至于unloadunsedAsset的调用可以在该资源使用完后手动调用一下(当资源消耗时刻明确时),也可以交给游戏卸载系统(定时调用unloadunusedasset)或切场景。

  我的一种方案:

  A.对不频繁加载的资源(大部分都是),我们加载完后就unload(false),这样可以迅速释放内存镜像,而当加载出来的资源不再被使用(引用为空)时,Asset就能被识别为unsedassets而被卸载;

  B.对频繁加载的资源(小部分),我们在上层弄一个pool,里面存着stringtoasset的字典,字典里只存asset,即即使是频繁加载的资源我们也要在加载出asset后用unload(false)把内存镜像卸载掉,只保留加载出来的资源以便实例化/引用,这里我们需要搞个引用计数,因为字典里永远引用着该资源,当引用计数为0时我们需要从字典中删除。

  在A,B的指导下+定时调用Resources.UnLoadUnUsedAssets,GC.Collect应该能保持内存的尽量干净了。那哪些资源是频繁加载哪些是不频繁加载,标准是咋样的呢?这个需要具体测试得到相关标准和性能的关系才能做决定,和游戏逻辑有关,和资源有关,和体验有关,需要写工具来确定。
  总结:AssetBundle的管理主要抓住几个接口即可AssetBundle.unload(true/false),Resources.UnLoadUnUsedAssets,GC.Collect,Destroy,但要真正了解他们,需要深入理解Unity对AssetBundle,Asset,资源等的内存管理机制,可以参考这篇文章:    

  http://www.cnblogs.com/88999660/archive/2013/03/15/2961663.html

 

四、Ps(存疑与补充)

  AssetBundle Variant:ab变种,一般用于解决“多态性”问题,比如资源有hd,ld,图片要适应各种设备需要不同压缩格式等,有空可以研究一下。

posted @ 2017-11-08 08:55  BigTreee  阅读(836)  评论(0编辑  收藏  举报