使用Assetbundle需要考虑的问题
Assetbundle资源打包时需要考虑如下几个问题:
1.如何有效地将依赖资源完整打包?如何减少资源重复打包,同时保证加载时不会导致内存超标?
2.如果多个资源打包在一个Assetbundle中,如何通过指定资源名查找定位到对应Assetbundle包名并加载?
3.创建多份相同资源(如副本中刷出多个相同的怪)GameObject时,想做到只加载一次Assetbundle和Prefab,提升加载效率,降低内存,如何设计缓存机制?
4.Assetbundle用完后如何释放?会不会有内存泄漏发生?如何避免?
资源依赖关系及打包规划
在Unity中一个Prefab文件可能会对另外的资源文件(如Prefab,贴图,shader等)有依赖,Unity提供了如下方式处理资源加载的依赖关系。
打包方法BuildPipeline.BuildAssetBundle(Object mainAsset, Object[] assets, string pathname, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform)
中参数assetBundleOptions使用BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets会将对象依赖的资源自动整体打入一个Assetbundle包中,这样保证了资源加载的完整性。
接着考虑到如果在打包时,将资源单独打包,那么多个资源所共用的资源就会被打包成多分,造成了冗余;如果将资源整体打成一个包,那么加载一个资源就需要加载整个Assetbundle包的镜像到内存,造成内存压力过大,对于资源量较大的游戏甚至发生游戏崩溃的情况。Unity提供方法
BuildPipeline.PushAssetDependencies()
BuildPipeline.PopAssetDependencies()
建立资源依赖关系,可以有效减小资源打包冗余。比如当打包shared, A, B三个资源时,伪代码如下:
BuildPipeline.PushAssetDependencies();
BuildPipeline.BuildAssetBundle(AssetDatabase.LoadMainAssetAtPath(“shared.prefab”), …);
BuildPipeline.PushAssetDependencies();
BuildPipeline.BuildAssetBundle(AssetDatabase.LoadMainAssetAtPath(“A.prefab”), …);
BuildPipeline.PopAssetDependencies();
BuildPipeline.PushAssetDependencies();
BuildPipeline.BuildAssetBundle(AssetDatabase.LoadMainAssetAtPath(“B.prefab”), …);
BuildPipeline.PopAssetDependencies();
BuildPipeline.PopAssetDependencies();
如果shared包含了A的资源,A就不会包含这个资源,而是依赖于shared中的;如果shared包含了B的资源,B也是依赖于shared中的;但是如果A包含了B的资源,B则不会依赖于A中的,而是自己包含这个资源。也就是说,在Push和Pop对中的资源可共享浅层级的资源,而无法共享层级更深的资源。
另外,在加载A资源之前,需要先加载shared资源,否则会出现A资源依赖的资源丢失问题。这个就要求开发者自己保证资源的加载顺序。对于资源量较大的实际工程,应当适度使用资源依赖,尽量避免资源加载时对加载顺序的限制,减小资源加载的复杂度,否则资源量大了之后较难管理维护。这里提供一些实际项目中的打包经验以供参考。
比如自定义的所有shader文件可以提取出来打包为公共Assetbundle包,避免出现每个依赖shader的资源都会单独打包shader,而造成冗余打包的情况。由于打包主要是为了支持资源级别的热更新,而shader一般情况下是不太可能被修改的,如果确定资源级别热更新不太可能更新shader,甚至可以将自定义shader文件放在Resources目录下不打Assetbundle包。
下来考虑模型资源,一般模型资源中最占空间的是材质贴图资源,可以要求美术将使用相同材质贴图的模型放在同一文件夹下,按照文件夹粒度打包,这样会有效地减小材质贴图重复打包造成的资源空间占用增长情况。
对于场景资源的加载一般也可按照场景的相似度,将共用大量物件贴图的场景放在一个文件夹中,将其打在一个Assetbundle包中,来减小场景资源空间增长的情况。
总之,在打包特定类型资源时需要按照这类资源依赖关系的特点,同时考虑减小Assetbundle包的粒度和减小依赖资源重复打包两方面使用Untiy提供的打包机制进行合理打包优化,达到加载到内存和资源包两方面冗余都能够满足游戏空间优化要求的目的。
Assetbundle资源命名管理
Unity在打包Assetbundle时没有提供现成的资源名到Assetbundle名映射关系。另外,Unity对于Assetbundle命名的要求是全局唯一。对于单个资源打包生成单个Assetbundle这种一对一的情况。可将Assetbundle名称命名为目录+文件名,以便于索引,这种情况一般用于本地数据表打包。但是对于资源共用比较多的场景和模型,一般会将多个贴图复用度较高的资源打包在一起,这就需要在创建Assetbundle包时建立资源文件名和Assetbundle包名的映射关系,将其序列化写入 xml文件中。游戏启动后先加载初始化资源映射关系表,以便于加载资源时通过资源名查找对应的Assetbundle包。
资源加载缓存机制
一份Assetbundle资源从加载到转化为GameObject需要经过如下三步:
(1)从网络或本地磁盘加载到内存生成Assetbundle文件内存镜像(异步);
(2)Assetbundle.Load接口加载内存镜像生成内存Prefab(阻塞式);
(3)Prefab被初始化复制生成GameObject(阻塞式);
如下图所示:
在移动平台下从网络或本地磁盘加载指定AssetBundle包到内存可以使用Unity提供的如下两个接口:
(1) WWW bundle = new WWW(path);
(2) WWW bundle = WWW.LoadFromCacheOrDownload(path, fileVer);
方法WWW.LoadFromCacheOrDownload会通过传入的下载地址和版本号在内存中查找是否已经有缓存的Assetbundle镜像,如果有,直接返回镜像;没有就会下载并缓存新的Assetbundle镜像。这里需要注意的是,传入的下载地址即使相同,版本号不同也会重新下载,对于没有加载多份相同镜像需求的常规情况,只需要将版本号参数设置为1 就好了。另外,缓存的释放机制由Unity自行管理,不需要使用者考虑,在磁盘空间不足的极端情况下,这个方法只是一个普通的new WWW。这是从Unity引擎层面保证相同的Assetbundle镜像在内存中只有一份,防止内存加载冗余。在实际项目中,开发者还需要自行管理加载缓存机制,不能只依赖于Unity的机制。在这里提供一种加载缓存的方法供大家参考讨论。
由上图可以看出Assetbundle加载后在内存里有Assetbundle镜像,未初始化的Prefab和初始化后复制的GameObject,设计缓存机制时需要从这三处考虑。缓存未初始化的Prefab既可以省略异步加载Assetbundle镜像的过程,又可以得到未修改的GameObject。因此首先定义一个以资源ID为Key值的Prefab字典。
Dictionary<string, GameObject> mPrefabMap;
接着考虑加载一波相同怪物的情况。由于Assetbundle内存镜像的加载是异步的,因此无法按逻辑顺序判断避免重复加载Assetbundle。这里需要再定义一个以Assetbundle文件名为Key值的Assetbundle内存镜像字典。
Dictionary<string, Assetbundle> mAssetMap;
相同资源再次加载时如果发现Assetbundle镜像正在加载中,缓存起来等待镜像加载完成处理。缓存结构如下:
Dictionary<string,List<ABLoader>> mLoadersMap;
这里Key为Assetbundle文件名,Value为待处理的加载器列表。
加载流程如下图所示:
在初始化GameObject后会立刻释放Assetbundle镜像和对应待加载的ABLoader对象列表。这种机制可以保证加载多份相同资源时只异步加载一次Assetbundle镜像(较为耗时),并且在内存中只缓存一份Prefab,再次加载相同资源时只需通过Prefab复制初始化即可。另外Prefab的释放时机可以选择一波怪GameObject销毁时,也可以选择关卡结束时,这个按照项目具体情况选择即可。
Assetbundle使用时需要注意的坑
Assetbundle在实际使用时遇到了很多的问题。这里总结一些典型的问题供大家参考,避免再次掉进坑里。
在打包Assetbundle时需要注意两个坑。一个是不做资源依赖打包的情况下不要添加Push和Pop对,否则会在部分机型(如华为P8)下出现加载资源积累到一定程度时(如加载几十波怪)出现资源贴图丢失的问题。另一个需要注意的是做资源依赖打包下面的写法是错误的。
BuildPipeline.PushAssetDependencies();
BuildPipeline.BuildAssetBundle(AssetDatabase.LoadMainAssetAtPath(“shared.prefab”), …);
BuildPipeline.PushAssetDependencies();
BuildPipeline.BuildAssetBundle(AssetDatabase.LoadMainAssetAtPath(“A.prefab”), …);
BuildPipeline.BuildAssetBundle(AssetDatabase.LoadMainAssetAtPath(“B.prefab”), …);
…
BuildPipeline.PopAssetDependencies();
BuildPipeline.PopAssetDependencies();
这样有可能会导致加载资源出现贴图丢失的情况。正确的写法如下:
BuildPipeline.PushAssetDependencies();
BuildPipeline.BuildAssetBundle(AssetDatabase.LoadMainAssetAtPath(“shared.prefab”), …);
BuildPipeline.PushAssetDependencies();
BuildPipeline.BuildAssetBundle(AssetDatabase.LoadMainAssetAtPath(“A.prefab”), …);
BuildPipeline.PopAssetDependencies();
BuildPipeline.PushAssetDependencies();
BuildPipeline.BuildAssetBundle(AssetDatabase.LoadMainAssetAtPath(“B.prefab”), …);
BuildPipeline.PopAssetDependencies();
…
BuildPipeline.PopAssetDependencies();
每个依赖于Shared的资源打包时都需要用添加单独的Push和Pop对。
如果对自定义的Shader单独打Assetbundle包,需要在Edit->Project Settings->Graphics中添加自定义的shader引用,否则加载会出现Shader丢失的情况。
实际工程中为了减小DLL空间大小,通常会打开Stripping Level设置,剥离没有被使用的库。这样会出现以下情况,当模型骨骼都被做成Assetbundle包,并且模型使用了物理关节后加载模型崩溃,当场景被做成Assetbundle包,并且场景中使用了LightProbe,加载场景后,LightProbe影响的材质都会变黑。这些都是因为对应的物理库和LightProbe库没有被打包到安装包中,出现这种问题时可以在Assets目录下添加link.xml文件来手动加入需要引用的库。
另外,外网有部分玩家反馈,打了一段时间后无法进入场景。定位发现部分手机大文件清理会清除大于10M的文件,单个Assetbundle包大小不要超过10M。
以上是使用Assetbundle打包和加载资源的一些经验和心得,有需要改进的地方欢迎指正探讨。
转自:http://gad.qq.com/article/detail/7151362