Unity AssetBundles and Resources指引 (四) AssetBundle使用模式

本文内容主要翻译自下面这篇文章

https://unity3d.com/cn/learn/tutorials/topics/best-practices/guide-assetbundles-and-resources?playlist=30089  A guide to AssetBundles and Resources

 

本部分讨论AssetBundle实际应用中一切潜在的问题和解决方案。

1.1    管理加载的资源

在内存敏感的环境里面要严格控制加载Object的大小和数量。当Object从当前激活场景中移除时,Unity不会自动卸载他们。清除Asset只有在特殊的时刻被触发。当然也可以手动触发。

AssetBundle子什么也要仔细管理。从文件加载的AssetBundle占用的内存最小,一般不超过10到40KB。但是当大量AssetBundle出现的时候还是会出现问题。

因为大部分项目运行用户重复体验相同的内容,比如重玩一个关卡,所以知道什么时候加载和卸载AssetBundle很重要。如果一个AssetBundle被不正确的卸载,会造成Object占用双倍内存,在某些情况下还会造成别的不可预期的问题。比如贴图丢失。

理解了AssetBundle.Unload方法的参数true和false的区别对应管理assets和AssetBundle非常重要。这个api卸载掉被调用的AssetBundle的头信息。里面的参数会指定是否同时卸载从AssetBundle里面实例化的Objects。如果参数为true,则从这个AssetBundle里面创建的所有objects都被立即卸载,即使正在被当前的场景使用。

比如下图所示

 

材质M从AssetBundle AB里面加载的,而且M正在当前场景中被使用。如果Unload(true)被调用,则M会从当前场景移除并销毁掉。但是如果调用的是Unload(false),则AB的头信息被卸载了但是M仍然留在场景里面起作用。但是M和AB的联系呗打断了。如果AB重新被加载,新的objects拷贝会重新加载进内存。

 

如果AB后来又被加载的话,新的一份AssetBundle的头信息拷贝到内存。但是M不再是从新的AB里面加载的。Unity不会给M和新的AB建立连接。

 

如果这是再次调用LoadAsset方法重新加载M,Unity不会任务原来的那个M是从AB里面数据的实例化,而是从新加载一份新的M。导致场景里面有两份一样的M。

对很多项目来说,这是不期望的行为。大部分项目都才用AssetBundle.Unload(true)和措施来确保对象不会被复制。两个常用的方案是

  1. 找一个应用程序生命周期某个设计好的时点卸载所有的AssetBundle。比如关卡切换或者在过载场景。这是最简单常用的选项。
  2. 维护单个对象的object的引用计数。当一个AssetBundle的所有对象没有被使用时卸载AssetBundle。

如果一个应用必须采用Unload(false),这些Objects只能用两种方式卸载:

  1. 确保没有对这个对象的引用,无论是代码还是场景里面。然后调用 Resources.UnloadUnusedAssets.
  2. 以非附加的方式加载一个场景。这回自动销毁当前场景里面的objects然后调用1里面的方法。

 

如果项目有设计好的点,用户在这里等待对象的加载和卸载。比如游戏模式或者关卡之间的切换,这个时点就可以用来卸载不需要的Objects并加载新的对象。最简单的方式就是资源以场景的方式分割。然后把每个场景及其里面的依赖打包到AssetBundle里面。程序可以加入一个加载场景,在这里卸载老场景里面的对象,然后加载包含新场景的AssetBundle。显然这是一个简单的流程,一些项目需要更复杂的AssetBundle管理。这里没有统一的设计样式,每个项目都不一样。当考虑怎么样Objects分组打包时,如果对象总是一起加载和更新,就可以考虑打包进一个AssetBundle。

比如一个角色扮演游戏,就可以把地图和过场动画以场景分组打包。但是有些对象所有的场景都需要。比如头像,游戏UI和角色模型以及贴图等,后面的对象可以打包进第二套AssetBundle,并且在游戏开始时加载,然后整个生命周期都存在。

另外一个问题就是如果从一个已经卸载的AssetBundle里面加载Object,就会加载失败,在unity的编辑器的Hierarchy面板里面显示一个丢失的对象。这个问题经常出在线unity丢失了绘图上下文而需要重新获取时。比如一个移动应用被挂载了或者用户锁定了他的pc。这时候,unity会重新加载贴图和着色器到GPU里面。如果AssetBundle已经失效了(被卸载),这个应用程序就会用洋红色渲染这个对象。

4.2分发

有两种方式分发AssetBundle到客户端:一种直接放到安装包里面另外一种是安装后下载。那种方式根据项目类型来选用。移动项目一般采用安装后下载。控制台和PC项目一般把AssetBundle随安装包一些分发。

正确的架构使得安装后仍可以打补丁或者修正内容而不管开始AssetBundle时如何分发的。

1.2.1        同项目一起分发

这是最简单的方式,因为不需要附加的下载代码。采用这种方式有两个主要理由:

  1. 减少项目编译次数同时可以开发迭代也简单。因为如果AssetBundle不需要同应用隔离开来的话,可以直接把他们存储到streamingassets目录。
  2. 开始就分发可更新资源,就节约了终端用户的时间。Streamingassets不适合这种需求。如果不想自己写一个下载和缓存管理器,那么初次可更新内容可以从streaming assets加载到Cache里面。

1.2.1.1  Streaming Assets

如果想在安装后就包含内容的最简单方式就是把该内存放到/Assets/StreamingAssets/目录。在编译的时候,任何在该目录的内容都会被拷贝到最终的应用程序里面。这个目录可以用来存储任何内容而不仅仅是AssetBundle。

运行时,完整路径可以通过Application.streamingAssetsPath属性来访问。在大部分的平台上面他可以通过LoadFromFile来加载里面的AssetBundle。但是在安卓平台上面,这个属性指向一个压缩的jar文件。甚至AssetBundles都是被压缩了。这种情况下,WWW.LoadFromCacheOrDwonLoad可以用来加载AssetBundle。当然也可以自己写代码来解压jar报,然后抽取AssetBundle到可读的本地目录。

注意Streaming Assets在某些平台是不写的。如果AssetBundle安装后需要更新。需要使用WWW. LoadFromCacheOrDwonLoad或者自己写下载器。

1.2.2        安装后下载

在移动平台上常用该方法。这允许安装后内容可以更新或优化而不用用户重新下载整个应用。在移动平台上面,应用审批流程总是很麻烦的。因此更新系统是必不可少的。

最简单的方式就是讲AssetBundle放到web服务器上面。然后通过WWW或者UnityWebRequest下载。unity会自动存储下载好的AssetBundles。如果下载的是LZMA压缩的内容。unity会解压存放。如果是LZ4压缩的。则下载后也是压缩存储的。如果缓存不足,unity会移除最少使用的AssetBundle。

但是WWW. LoadFromCacheOrDwonLoad是有瑕疵的,正如前面所述,这个在下载时候会消耗AssetBundle数据大小的内存,可能会导致内存问题。有三种方式来避免

  1. 保证AssetBundle够小。
  2. 5.3或者更新的版本,用UnityWebRequest。
  3. 自定义下载器。

一般建议尽可能使用UnityWebRequest,或者5.2以及前面的版本使用WWW,只有内建的系统在内存消耗缓存行为或者性能不可接受的情况下才建议使用自定义下载系统。

下面是不适合使用WWW者UnityWebRequest的情况:需要更好地控制AssetBundle缓存。项目需要自定义压缩策略。项目需要平台相关的API。比如离线下载比如IOS后台任务可以下载。或者AssetBundle需要通过SSL协议下载。

1.2.3        内建的AssetBundle的缓存

在使用WWW和UnityWebRequest时下载的AssetBundle被存储在内建的缓存里面。两个API都有一个重载的版本,接收一个AssetBundle版本号,这个数字不是存储在AssetBundle里面。而且不是AssetBundle系统生成的。

缓存系统跟踪这个版本号。调用api的时候,缓存系统首先检查资源十分存在,如果有,则比较上一次缓存的版本号。如果匹配,则从缓存加载,如果不匹配,或者没有缓存,则unity去下载一个新版本,并同这个版本号关联起来。

AssetBundle使用名字来认证的而不是下载的URL。这意味着相同名字的AssetBundle可以存在不同的位置。比如一个AssetBundle可以存储在多个服务器上面或者CDN上。只要这个名字是一样的。缓存系统就认为他们是相同的AssetBundle。

给AssetBundle分配版本号是开发者的责任。大部分应用使用unity5的assetbundleManifestApi。这个API生成通过AssetBundle的内容MD5哈希值为该AssetBundle生成一个版本号。当AssetBundle改变的时候,他的哈希值也变化了。这就意味着AssetBundle需要下载。注意unity缓存系统并没有删除就得AssetBundle,直到cache被填满。unity后续版本可能会修复这个问题。

1.2.4        自定义下载(略)

1.3          Asset 分配策略

怎么分配项目的资源到不同的AssetBundle不是那么简单。一个简单的诱人的方案就是每个对象放进自己的AssetBundle或者只用一个单独的AssetBundle。但是这些方案都有缺点:

有过少的AssetBundle:增加了运行时内存消耗。增加了加载时间,需要很大的下载。

如果是过多的AssetBundle呢?增加了编译时间,增加了开发的复杂性,增加了总共的下载时间。

怎么把对象分组到AssetBundle的关键思考点在于逻辑实体对象类型和和并发的内容。

要知道一个项目可能会混合这些分类策略。比如一个项目会把不同平台的ui打包到各自的AssetBundle。把不交互的内容按场景打包。厦门是一些好的建议:

  1. 把经常更新的内容和不变的内容打包到不同的AssetBundle。
  2. 把同时加载的内容打包到一起。比如一个模型和他的纹理以及动画。
  3. 如果一个对象依赖多个不同的AssetBundle里面的对象,把这些资源移到一个单独的AssetBundle。
  4. 把子对象和父对象分组到一起。
  5. 如果两个对象不会一起加载。则打包到不同的AssetBundle。
  6. 如果对象是同一个对象的不同的导入设置版本。考虑使用AssetBundle变量替代。

如果上述规则遵守了,然后把一个AssetBundle每次只有少于50%的内容被加载的继续分割。把一些小的AssetBundle(少于5到10个assets)进行重组一个AssetBundle。

1.3.1        逻辑实体分组

这个策略是按照功能分组。应用的不同部分被打包进不同的AssetBundle。

比如打包所有UI的纹理和布局在一起。打包所有角色的贴图模型和动画在一起。

1.3.2        类型分组

这是最简单的策略。相似类型的打包进相同的AssetBundle。比如声音文件打包到一个AssetBundle。不同的语言文件打包进一个AssetBundle。

1.3.3        并发内容分组

同时加载的内容打包一起。

1.4          用AssetBundles打补丁

AssetBundle打补丁简单的只要重新下载一个新的AssetBundle替代旧有的就可以了。只要传递一个不同的版本号给API。更困难问题是检测什么时候需要打补丁。一个打补丁系统需要2个信息。

  1. 当前下载的AssetBundle和版本信息列表。
  2. 服务器上面的AssetBundle和版本信息列表。

补丁系统需要下载服务端列表,同本地比较,丢失的或者版本不匹配的都要重新下载。Unity5 AssetBundle系统在编译AssetBundle完成之后生成一个附加的AssetBundle。这个AssetBundle包含 AssetBundleManifest对象。这对象包含了AssetBundle和哈希列表。也可以自己定制检测AssetBundle改变系统,比如用Json系统和标准的C#校验和类MD5.

1.5          常见陷阱

1.5.1        Asset复制

当一个Object打包进一个AssetBundle时,unity5 会自动检查所有的依赖Objects。依赖信息被用来决死你个这些Objects是否包含到一个AssetBundle。

显式指定一个对象打包到一个AssetBundle时,这个对象只被打包到指定的AssetBundle。显式指定是指在编辑器里面AssetBundle属性栏设置了一个非空值。

如果一个Objects没有显式指定一个AssetBundle,则会被打包进所有依赖他的AssetBundle里面。

如果两个Object被打到不停地AssetBundle,但是这两个都引用一个共同的Object,这个Object却没有设置打包信息,那么这个Object会被拷贝到两个AssetBundle里面。这复制的两个也会单独实例化。即会增加应用的AssetBundle的尺寸。如果程序加载两个父对象,也有有2份这个子对象的单独内存拷贝。

解决方法:

  1. 确保打包到不同的AssetBundle没有共享的依赖。有共享依赖的Objects打包到同一个AssetBundle。但是如果项目有非常多的共享依赖,这就变得不可行了。他会造成一个单一的AssetBundle。带来下载和效率问题。
  2. 保证有共享依赖的AssetBundle不会同时加载。但仍然会加大程序尺寸。
  3. 把依赖的Objects打到自己的AssetBundle里面。这消除了复制问题,但是带来了复杂性。应用程序必须跟踪依赖。

Unity5用AssetDataBase来跟踪依赖。他位于UnityEditor命名空间。这个Api只能在Editor环境而不能在运行时使用。AssetImporter API可用来查询某个Object被赋予哪个AssetBundle。联合使用这两个API。可以写一个编辑器脚本确保所有的直接和间接依赖都赋予AssetBundles或者共享的依赖都被赋值到某个AssetBundle。建议项目使用这两个API。

1.5.2        图集精灵复制

接下来讨论unity 5在计算自动生成的图集精灵的依赖时的奇怪行为。Unity5.2.2p4修复了这个行为。

Unity5.2.2p4,5.3或更新

任何自动生成的精灵图集会被赋给包含这个生成sprite对象的AssetBundle。如果这个精灵Objects打包到多个AssetBundle,那么这个图集就不会打包到一个AssetBundle,而是被复制。如果精灵对象没有赋值给一个AssetBundle,那么就不会被打包。为了保证不复制,需要检查这个图集下所有的精灵都会被打包到同一个AssetBundle。

Unity5.2.2p4或者更老的版本

自动生成的图集不会赋值到某个AssetBundle。因此他们会包含到任何引用他们精灵的AssetBundle中。因为这个问题建议升级到更高的unity版本。对于不能升级的项目,建议:

1简单的方式就是不要用内建的精灵打包器。

2难一点的方式就是把所有的使用这个图集的对象打包到同一个AssetBundle。

1.6          AssetBundle Variants

1.7          压缩与否?

是否压缩需要仔细考虑:

加载时间是否是关键因素?未压缩的加载时间会快很多。但是从远程下载压缩的资源也会比未压缩的的块。

编译时间是否是关键因素?LZMA和LZ4编译时非常慢。如果项目有很多AssetBundle,则要花掉大量的时间。

应用程序尺寸是否关键因素?

内存消耗是否是关键因素?

下载时间是否是关键因素?

1.8          AssetBundle和WEBGL

unity强烈建议WEBGL项目不要使用压缩的AssetBundle。因为unity5.3在主线程上解压AssetBundle。(下载AssetBundle会通过XMLHttpRequest委托给浏览器。不在主线程上)这意味着在WEBGL里面加载压缩的资源非常消耗。

记住这些,你就会避免LZMA格式,而采用LZ4格式。因为后者的解压很高效。如果你在意资源大小,那就采用lz4来分发,并且配置服务端支持gzip压缩。

 

posted @ 2016-10-13 19:04  qzzlw  阅读(2633)  评论(0编辑  收藏  举报