【转载】AssetBundle资源打包加载管理
前言
本篇文章是为了记录学习了Unity资源加载(Resource & AssetBundle)相关知识后,对于AssetBundle打包加载框架实战的学习记录。因为前一篇学习Unity资源相关知识的文章太长,所以AssetBundle实战这一块单独提出来写一篇。
基础知识学习回顾,参考:
Unity Resource Manager
鸣谢
这里AssetBundle加载管理的框架思路借鉴了Git上的开源库:
tangzx/ABSystem
同时也学习了KEngine相关部分的代码:
mr-kelly/KEngine
AssetBundle打包这一套主要是自己基于对新版(5.X以上)的AssetBundle打包机制自行编写的一套,这一套相对来说很不完善有很多缺点,个人建议读者参考Git上的其他一些开源库或者直接使用Unity官方推出的高度可视化和高度*度打包方案:
AssetBundles-Browser
本文重点分享,参考ABSystem编写的一套AB加载管理方案实现:
基于AB引用计数的AB加载管理
AssetBundle打包
Note:
这里的AssetBundle打包主要是针对Unity 5.X以及以后的版本来实现学习的。
相关工具
- Unity Profiler(Unity自带的新能分析工具,这里主要用于查看内存Asset加载情况)
- Unity Studio(Unity AB以及Asset等资源解析查看工具)
- DisUnity(解析AB包的工具)
AB打包
AB打包是把资源打包成assetbundle格式的资源。
AB打包需要注意的问题:
- 资源冗余
- 打包策略
- AB压缩格式
资源冗余
这里说的资源冗余是指同一份资源被打包到多个AB里,这样就造成了存在多份同样资源。
资源冗余造成的问题:
- 余造成内存中加载多份同样的资源占用内存。
- 同一份资源通过多次IO加载,性能消耗。
- 导致包体过大。
解决方案:
依赖打包
依赖打包
依赖打包是指指定资源之间的依赖关系,打包时不将依赖的资源重复打包到依赖那些资源的AB里(避免资源冗余)。
在老版(Unity 5之前),官方提供的API接口是通过BuildPipeline.PushAssetDependencies和BuildPipeline.PopAssetDependencies来指定资源依赖来解决资源冗余打包的问题。
在新版(Unity 5以后),官方提供了针对每个Asset在面板上设置AssetBundle Name的形式指定每个Asset需要打包到的最终AB。然后通过 API接口BuildPipeline.BuildAssetBundles()触发AB打包。Unity自己会根据设置AB的名字以及Asset之间的使用依赖决定是否将依赖的资源打包到最终AB里。
这里值得一提的是Unity 5以后提供的增量打包功能。
增量打包(Unity 5以后)
增量打包是指Unity自己维护了一个叫manifest的文件(前面提到过的记录AB包含的Asset以及依赖的AB关系的文件),每次触发AB打包,Unity只会修改有变化的部分,并将最新的依赖关系写入manifest文件。
*.manifest记录所有AB打包依赖信息的文件,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
ManifestFileVersion: 0 CRC: 961902239 AssetBundleManifest: AssetBundleInfos: Info_0: Name: nonuiprefabs/nui_capsulesingletexture Dependencies: Dependency_0: materials/mt_singletexturematerial Info_1: Name: shaders/sd_shaderlist Dependencies: {} Info_2: Name: materials/mt_singletexturematerial Dependencies: Dependency_0: shaders/sd_shaderlist Dependency_1: textures/tx_brick_diffuse Info_3: Name: textures/tx_brick_diffuse Dependencies: {} Info_4: Name: nonuiprefabs/nui_capsulenormalmappingtexture Dependencies: {} Info_5: Name: textures/tx_brick_normal Dependencies: {} Info_6: Name: materials/mt_normalmappingmaterial Dependencies: Dependency_0: shaders/sd_shaderlist Dependency_1: textures/tx_brick_diffuse Dependency_2: textures/tx_brick_normal |
问题:
虽然Unity5提供了增量打包并记录了依赖关系,但从上面的*.manifest可以看出,依赖关系只记录了依赖的AB的名字没有具体到特定的Asset。
最好的证明就是上面我把用到的Shader都打包到sd_shaderlist里。在我打包的资源里,有两个shader被用到了(SingleTextShader和NormalMappingShader),这一点可以通过UnityStudio解压查看sd_shaderlist看到:
sd_shaderlist
从上面可以看出Unity的增量打包只是解决了打包时更新哪些AB的判定,而打包出来的*.manifest文件并不能让我们得知用到了具体哪一个Asset而是AssetBundle。
解决用到哪些Asset这一环节依然是需要我们自己解决的问题,只有存储了用到哪些Asset的信息,我们才能在加载AB的时候对特定Asset做操作(缓存,释放等)。
Note:
每一个AB下面都对应一个.manifest文件,这个文件记录了该AB的asset包含情况以及依赖的AB情况,但这些manifest文件最终不会不打包到游戏里的,只有最外层生成的*.manifest文件(记录了所有AB的打包信息)才会被打包到游戏里,所以才有像前面提到的通过读取.manifest文件获取对应AB所依赖的所有AB信息进行加载依赖并最终加载出所需Asset的例子
依赖Asset信息打包
存储依赖的Asset信息可以有多种方式:
- 通过加载依赖AB,依赖Unity自动还原的机制实现依赖Asset加载还原(这正是本博客实现AssetBundle打包以及加载管理的方案)
- 存储挂载相关信息到Prefab上
在设置好AB名字,打包AB之前,将打包对象上用到的信息通过编写[System.Serializable]可序列化标签抽象数据挂载到该Asset对象上,然后打包AB,打包完AB后在运行时利用打包的依赖信息进行依赖Asset加载还原,从而做到对Asset的生命周期掌控。
DPInfoMono - 创建打包Asset到同名AB里,加载的时候读取
通过AssetDatabase.CreateAsset()和AssetImporter.GetAtPath()将依赖的信息写入新的Asset并打包到相同AB里,加载时加载出来使用。
MaterialAssetInfo
打包策略
除了资源冗余,打包策略也很重要。打包策略是指决定各个Asset如何分配打包到指定AB里的策略。打包策略会决定AB的数量,资源冗余等问题。AB数量过多会增加IO负担。资源冗余会导致包体过大,内存中存在多份同样的Asset,热更新资源大小等。
打包策略:
- Logical entities(按逻辑(功能)分类 — 比如按UI,按模型使用,按场景Share等功能分类)
优点:可以动态只更新特定Entity - Object Types(类型分类 — 主要用于同类型文件需要同时更新的Asset)
优点:只适用于少部分需要经常变化更新的小文件 - Concurrent content(加载时机分类 — 比如按Level分类,主要用于游戏里类容固定(Level Based)不会动态变化加载的游戏类型)
优点:适合Level based这种一层内容一层不变的游戏类型
缺点:不适合用于动态创建对象的游戏类型
打包策略遵循几个比较基本的准则:
- Split frequently-updated Objects into different AssetBundles than Objects that usually remain unchanged(频繁更新的Asset不要放到不怎么会修改的Asset里而应分别放到不同的AB里)
- Group together Objects that are likely to be loaded simultaneously(把需要同时加载的Asset尽量打包到同一个AB里)
从上面可以看出,不同的打包策略适用于不同的游戏类型,根据游戏类型选择最优的策略是关键点。
AB压缩格式
AB压缩不压缩问题,主要考虑的点如下:
- 加载时间
- 加载速度
- 资源包体大小
- 打包时间
- 下载AB时间
这里就不针对压缩问题做进一步介绍了,主要根据游戏对于各个问题的需求看重点来决定选择(内存与加载性能的抉择)
AB打包相关API
-
Selection(获取Unity Editor当前勾选对象相关信息的接口)
1
Object[] assetsselections = Selection.GetFiltered(Type, SelectionMode);
-
AssetDatabase(操作访问Unity Asset的接口)
1 2 3 4
// 获取选中Asset的路径 assetpath = AssetDatabase.GetAssetPath(assetsselections[i]); // 获取选中Asset的GUID assetguid = AssetDatabase.AssetPathToGUID(assetpath);
-
AssetImporter(获取设置Asset的AB名字的接口)
1 2 3 4 5
// 获取指定Asset的Asset设置接口 AssetImporter assetimporter = AssetImporter.GetAtPath(assetpath); // 设置Asset的AB信息 assetimporter.assetBundleVariant = ABVariantName; assetimporter.assetBundleName = ABName;
-
BuildPipeline(AB打包接口)
1 2 3
BuildPipeline.BuildAssetBundles(outputPath, BuildAssetBundleOptions, BuildTarget); BuildPipeline.BuildAssetBundles(string outputPath, AssetBundleBuild[] builds, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform);
可以看到AB打包Unity 5.X提供了两个主要的接口:
- 前者是依赖于设置每个Asset的AB名字,然后一个接口完成增量打包的方案。
优点:
自带增量打包
缺点:
需要有一套资源AB命名规则。
开发者可控度低。 - 后者是提供给开发者自定义哪些Asset打包到指定AB里的一个接口。
优点:
可自行实现指定需求的打包规则。
开发者可控度高。
缺点:
不自带增量打包需要自己实现。
Note:
AssetBundle-Browser也是基于前者的一套打包方案,只不过AssetBundle-Browser实现了高度的Asset资源打包可视化操作与智能分析。
AB打包框架实战
这里本人实现的打包方案是使用的后者。(这一套并不完善(未写完,也有不少问题,可以简单看哈借鉴哈思路。)
主要实现了以下功能:
- 支持设置四种Asset打包规则(通过设置AB名字)
- sharerule(作为共享资源,单独打成AB)
- entirerule(作为一个整体,所有Asset用到的资源打包成一个AB)
- mutilplerule(作为一个集合,同目录下同类型Asset打包成一个AB)
- normalrule(作为一个普通资源,谁依赖使用它,他就打包到使用它的Asset所属打包规则的AB里)
- 资源划分类型,针对不同资源类型,可以自定义限制打包规则设置以及Unity格式设定检查等。
- 可以对内置资源打包做单独处理(比如内置shader,内置default sprite等)
- 每次打包生成的依赖文件信息提取出来后最后合并得出最终的依赖信息。
- Shader打包资源时自动记录,最后打一次Shader AB即可。
缺点与问题:
- 不支持增量打包,每一次打包都是单独分析的。(增量打包这一点很重要)
- 依赖分析是根据AssetDatabase.GetDependencies()来做的分析,看网上有提到说
EditorUtility.CollectDependencies()获取的数据才是更正确的,依赖分析可能有问题。(这个比较好改,直接换成后者即可) - normalrule设计的时候没有考虑到单个normalrule资源被多个同类型资源引用的情况(单个资源单次打包不能指定到多个AB里),这里分析出来的数据会有误(导致normalrule的资源只会被打包到某一个引用他的资源对象AB里)。(比较严重的问题,前期依赖分析设计考虑欠佳导致的)
- 因为没有增量打包,每一次都是单独打包,所以只支持同类型或者单个资源分析打包。
- 修改某个资源的打包规则会导致需要把所有用到该资源的资源全部重新打包。(很严重的问题)
基于上面的很多缺点,导致本人最终也放弃了继续写该方案的打包。但从这一次打包代码编写中加深了对AssetBundle打包的理解。后面说完AB加载管理会一起附上最后的源代码Git链接。
Note:
个人建议读者参考其他开源库或者使用Assetbundle Browser。
AssetBundle加载管理
实战学习AB加载之前让我们通过一张图先了解下AB与Asset与GameObject之间的关系:
AssetBundleFramework
依赖加载还原
还记得前面说到的依赖的Assest信息打包吗?
这里就需要加载出来并使用进行还原了。
这里接不细说加载还原了,主要就是通过存储的依赖信息把依赖的Asset加载进来并设置回去的过程(可以是手动设置回去也可以是Unity自动还原的方式)。
这里主要要注意的是前面那张大图上给出的各种资源类型在Asset加载还原时采用的方式。
资源加载还原的方式主要有两种:
-
复制+引用
UI — 复制(GameObject) + 引用(Components,Tranform等)
Material — 复制(材质自身) + 引用(Texture和Shader) -
引用
Sprite — 引用
Audio — 引用
Texture — 引用
Shader — 引用
Material — 引用
AB加载相关API
- AssetBundle(AB接口)
1 2 3 4 5 6
// 加载本地压缩过的AB AssetBundle.LoadFromFile(abfullpath) // 加载AB里的指定Asset AssetBundle.LoadAsset(assetname); // 加载AB里的所有Asset AssetBundle.LoadAllAssets();
AB回收相关API
-
AssetBundle(AB接口)
1 2 3 4
// 回收AssetBundle并连带加载实例化出来的Asset以及GameObject一起回收 AssetBundle.Unload(true); // 只回收AssetBundle AssetBundle.Unload(false);
-
Resource(资源接口)
1 2 3 4
// 回收指定Asset(这里的Asset不能为GameObject) Resource.UnloadAsset(asset); // 回收内存以所有不再有任何引用的Asset Resources.UnloadUnusedAssets();
-
GC(内存回收)
1 2
// 内存回收 GC.Collect();
AB回收的方式有两种:
- AssetBundle.Unload(false)(基于Asset层面的重用,通过遍历判定的方式去判定Asset是否回收)
- AssetBundle.Unload(true)(采取索引技术,基于AB层面的管理,只有AB的引用计数为0时我们直接通过AssetBundle.Unload(true)来卸载AB和Asset资源)
接下来结合这两种方式,实战演练加深理解。
基于Asset重用的AB加载
核心思想:
- 打包时存储了依赖的Asset信息,加载时利用存储的依赖信息并还原
- 缓存加载进来的Asset的控制权进行重用,卸载AB(AssetBundle.Unload(false))
- 加载时通过Clone(复制类型)或者返回缓存Asset(引用类型)进行Asset重用
一下以之前打包的CapsuleNormalMappingTexture.prefab进行详细说明:
CapsuleNormalMappingPrefab.PNG
可以看出CapsuleNormalMappingTexture.prefab用到了如下Asset:
- NormalMappingMaterial(材质)
- Custom/Texture/NormalMappingShader(Shader)
- Brick_Diffuse和Brick_Normal(纹理)
开始加载CapsuleNormalMappingTexture.prefab:
首先让我们看看加载了CapsuleNormalMappingTexture.prefab前的Asset加载情况:
NonUIPrefabLoadedBeforeProfiler
可以看出只有Shader Asset被预先加载进来了(因为我预先把所有Shader都加载进来了)
第一步:
加载CapsuleNormalMappingTexture.prefab对应的AB,因为Prefab是采用复制加引用所以这里需要返回一个通过加载Asset后Clone的一份对象。
1 2 3 4 5 6 |
nonuiprefabab = loadAssetBundle(abfullname); var nonuiprefabasset = loadMainAsset(nonuiprefabab); nonuigo = GameObject.Instantiate(nonuiprefabasset) as GameObject; // Asest一旦加载进来,我们就可以进行缓存,相应的AB就可以释放掉了 // 后续会讲到相关AB和Asset释放API nonuiprefabab.Unload(false); |
1 2 3 4 5 6 7 8 9 |
NonUIPrefabDepInfo nonuidpinfo = nonuigo.GetComponent<NonUIPrefabDepInfo>(); var dpmaterials = nonuidpinfo.mDPMaterialInfoList; for (int i = 0; i < dpmaterials.Count; i++) { for(int j = 0; j < dpmaterials[i].mMaterialNameList.Count; j++) { MaterialResourceLoader.getInstance().addMaterial(dpmaterials[i].mRenderer, dpmaterials[i].mMaterialNameList[j]); } } |
第三步:
还原依赖材质的Shader和Texture依赖引用
MaterialAssetInfo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 根据材质_InfoAsset.asset进行还原材质原始Shader以及Texture信息 var materialinfoasseetname = string.Format("{0}_{1}InfoAsset.asset", materialname, ResourceHelper.CurrentPlatformPostfix); var materialassetinfo = loadSpecificAsset(materialab, materialinfoasseetname.ToLower()) as MaterialAssetInfo; // 加载材质依赖的Shader var materialshader = materialassetinfo.mShaderName; var shader = ShaderResourceLoader.getInstance().getSpecificShader(materialshader); material.shader = shader; // 获取Shader使用的Texture信息进行还原 var materialdptextureinfo = materialassetinfo.mTextureInfoList; for (int i = 0; i < materialdptextureinfo.Count; i++) { // 加载指定依赖纹理 Texture shadertexture = TextureResourceLoader.getInstance().loadTexture(materialdptextureinfo[i].Value); //设置材质的对应Texture material.SetTexture(materialdptextureinfo[i].Key, shadertexture); } |
接下让我们看看加载了CapsuleNormalMappingTexture.prefab后的Asset加载情况:
NonUIPrefabLoadedAfterProfiler
可以看出引用的材质和纹理Asest都被加载到内存里了(Shader因为我预先把所有Shader都加载进来了所以就直接重用了没有被重复加载)
第四步:
对缓存的Asset进行判定是否回收(这里以材质为例,判定方式可能多种多样,我这里是通过判定是否有有效引用)
启动一个携程判定特定Material是否不再有有效组件(所有引用组件为空或者都不再使用任何材质)时回收Material Asset
1 |
Resources.UnloadAsset(materialasset); |
接下来让我们看看卸载实例对象后,材质被回收的情况:
MaterialRecycleAssetNumer
MaterialRecycle
可以看到没有被引用的材质Asset被回收了,但内存里的Asset数量却明显增加了。
这里多出来的是我们还没有回收的Texture以及Prefab的GameObject以及Components Asset依然还在内存里。
AfterMaterialRecyleTextureStatus
AfterMaterialRecyleGameObjectStatus
AfterMaterialRecyleTrasformStatus
第五步:
通过切换场景触发置空所有引用将还未回收的Asset变成UnsedAsset或者直接触发Texture Asset回收,然后通过Resources.UnloadUnusedAssets()回收所有未使用的Asset
1 2 3 4 5 6 7 8 9 |
mNonUIPrefabAssetMap = null; foreach(var texture in mTexturesUsingMap) { unloadAsset(texture.Value); } mTexturesUsingMap = null; Resources.UnloadUnusedAssets(); |
AfterAssetsRecyleAssetNumber
AfterAssetsRecyleTextureStatus
AfterAssetsRecyleGameObjectStatus
AfterAssetsRecyleTransformStatus
可以看到所有的Texture, GameObject, Transform都被回收了,并且Asset的数量回到了最初的数值。
基于AB引用计数的AB加载管理
这是本文重点分享的部分
方案1:
核心思想:
- 基于AB的引用计数 + AssetBundle.Unload(true)
- 给每一种资源(e.g. 特效,模型,图片,窗口等)加载都编写统一的资源加载接口(父类抽象)进行自身加载使用的AB引用计数,每个资源负责自身的资源加载管理和返还(主动调用)
- 由一个单例管理者统一管理所有加载AB的引用计数信息,负责判定是否可回收
优点:
- 严格的AB引用计数加载管理和释放
- 可以做到对资源对象的重用减少GC
- 对资源对象的重用可以减少AB的重复加载
缺点:
- 底层管理的内容比较多(比如基于资源对象的重用),上层灵活度欠缺(相当于对象池已经写在了最底层)
- 需要主动去调用返还接口(针对不同资源加载释放时机都需要编写一套对应的代码)
方案2:
核心思想:
- 基于AB的引用计数 + AssetBundle.Unload(true)
- 绑定加载AB的生命周期判定到Object上(e.g. GameObject,Image,Material等),上层无需关心AB的引用计数,只需绑定AB到对应的对象上即可
- 通过单例管理者统一管理判定依赖使用AB的Object列表是否都为空来判定是否可以回收,无需上层管理AB引用计数返还
优点:
- 上层只需关心AB加载绑定,无需关心AB引用计数返还问题,上层使用灵活度高
缺点:
- AB的返还判定跟绑定的Object有关,Object被回收后,AB容易出现重复加载(可以在上层写部分对象池来减少AB的重复加载)
考虑到希望上层灵活度高一些,个人现在倾向于第二种方案。
接下来基于第二种方案来实战编写资源AB加载的框架。
AB打包这一块打算先使用之前自己写的一套没有增量更新但支持一定打包规则指定的打包方案。
AB加载管理框架
以下实现主要参考了下面两个开源项目:
Unity3D AssetBundle 打包与管理系统
KEngine
框架设计
支持功能:
- 支持AB和AssetDatabase(限编辑器)资源加载(方便快速迭代开发,开发时用AssetDatabase(也支持快速切换AB模式))
- 支持AB同步和异步加载(统一采用callback风格)
- 支持三种基本的资源加载类型(NormalLoad — 正常加载(可通过Tick检测判定正常卸载) Preload — 预加载(切场景才会卸载) PermanentLoad — 永久加载(常驻内存永不卸载))
- 已加载资源允许变更资源加载类型,但只允许从低往高变(NormalLoad -> Preload -> PermanentLoad),不允许从高往低(PermanentLoad -> Preload -> NormalLoad)
- 基于UnityEngine.Object的AB索引生命周期绑定
- 底层统一管理AB索引计数,管理资源加载释放
- 支持卸载频率,卸载帧率门槛,单次卸载数量等设置。采用Long Time Unused First Unload(越久没用越先卸载)原则卸载。
- 上层编写对应资源类型Manager资源加载接口(负责提供各资源类型的加载)
- Manager of Manager架构设计,面向接口设计Manager,通过中介者统一注册获取(ModuleManager.GetManager的形式)
- 底层模块之间的解耦暂时还没想好如何设计(TODO)
更多学习参考:
浅谈 Unity 开发中的分层设计
类设计:
中介者解耦Manager的类:
ModuleManager(单例类)
ModuleInterface(模块接口类)
ModuleType(模块枚举类型)
资源加载类:
ABLoadMethod(资源加载方式枚举类型 — 同步 or 异步)
ABLoadState(资源加载状态 — 错误,加载中,完成之类的)
ABLoadType(资源加载类型 — 正常加载,预加载,永久加载)
ResourceModuleManager(资源加载模块统一管理类)
AssetBundleLoader(AB资源加载任务类)
AssetBundleInfo(AB信息以及加载状态类,AB访问,索引计数以及AB依赖关系抽象都在这一层)
AssetBundlePath(AB资源路径相关 — 处理多平台路径问题)
ABDebugWindow.cs(Editor运行模式下可视化查看AB加载详细信息的辅助工具窗口)
AssetBundle管理方式是索引计数+检查有效使用对象,然后AssetBundle.Unload(true)的方式。
这里的引用计数+检查有效使用对象是指:
- AB被依赖加载的引用计数
- AB被加载后绑定到特定Object上的owner对象有效性检查
包内AB加载方式:
AssetBundle.LoadFromFile()
AssetBundle.LoadFromFileAsync()
加载管理方案:
- 加载指定资源
- 加载自身AB(自身AB加载完通知资源加载层移除该AB加载任务避免重复的加载任务被创建),自身AB加载完判定是否有依赖AB
- 有则加载依赖AB(增加依赖AB的引用计数)(依赖AB采用和自身AB相同的加载方式(ABLoadMethod),但依赖AB统一采用ABLoadType.NormalLoad加载类型)
- 自身AB和所有依赖AB加载完回调通知逻辑层可以开始加载Asset资源(AB绑定对象在这一步)
- 判定AB是否满足引用计数为0,绑定对象为空,且为NormalLoad加载方式则卸载该AB(并释放依赖AB的计数减一)(通知资源管理层AB卸载,重用AssetBundleInfo对象)
- 切场景,递归判定卸载PreloadLoad加载类型AB资源
AB加载管理相关概念:
- 依赖AB与被依赖者采用同样的加载方式(ABLoadMethod),但加载方式依赖AB统一采用ABLoadType.NormalLoad
- 依赖AB通过索引计数管理,只要原始AB不被卸载,依赖AB就不会被卸载
- 已加载的AB资源加载类型只允许从低往高变(NormalLoad -> Preload -> PermanentLoad),不允许从高往低(PermanentLoad -> Preload -> NormalLoad)
Note:
- 一个AB加载任务对应一个AssetBundleLoader
- 一个已加载AB信息对应一个AssetBundleInfo
Demo
-
点击加载窗口预制件按钮后:
1 2 3 4 5 6
ModuleManager.Singleton.getModule<ResourceModuleManager>().requstResource("MainWindow", (abi) => { mMainWindow = abi.instantiateAsset("MainWindow"); mMainWindow.transform.SetParent(UIRootCanvas.transform, false); });
AssetBundleLoadManagerUIAfterLoadWindow
可以看到窗口mainwindow依赖于loadingscreen,导致我们加载窗口资源时,loadingscreen作为依赖AB被加载进来了(引用计数为1),窗口资源被绑定到实例出来的窗口对象上(绑定对象MainWindow) -
点击测试异步和同步加载按钮后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
if(mMainWindow == null) { onLoadWindowPrefab(); } // 测试大批量异步加载资源后立刻同步加载其中一个该源 var image = mMainWindow.transform.Find("imgBG").GetComponent<Image>(); ModuleManager.Singleton.getModule<ResourceModuleManager>().requstResource("tutorialcellspritesheet", (abi) => { var sp = abi.getAsset<Sprite>(image, "TextureShader"); image.sprite = sp; }, ABLoadType.NormalLoad, ABLoadMethod.Async); ModuleManager.Singleton.getModule<ResourceModuleManager>().requstResource("Ambient", (abi) => { var sp = abi.getAsset<Sprite>(image, "Ambient"); image.sprite = sp; }, ABLoadType.NormalLoad, ABLoadMethod.Async); ModuleManager.Singleton.getModule<ResourceModuleManager>().requstResource("BasicTexture", (abi) => { var sp = abi.getAsset<Sprite>(image, "BasicTexture"); image.sprite = sp; }, ABLoadType.NormalLoad, ABLoadMethod.Async); ModuleManager.Singleton.getModule<ResourceModuleManager>().requstResource("Diffuse", (abi) => { var sp = abi.getAsset<Sprite>(image, "Diffuse"); image.sprite = sp; }, ABLoadType.NormalLoad, ABLoadMethod.Async);
AssetBundleLoadManagerUIAfterLoadSprites
可以看到我们切换的所有Sprite资源都被绑定到了imgBG对象上,因为不是作为依赖AB加载进来的所以每一个sprite所在的AB引用计数依然为0. -
点击销毁窗口实例对象后:
1
GameObject.Destroy(mMainWindow);
AssetBundleLoadManagerUIAfterDestroyWindow
窗口销毁后可以看到之前加载的资源所有绑定对象都为空了,因为被销毁了(MainWindow和imgBG都被销毁了)
o -
等待回收检测回收后:
AssetBundleLoadManagerUIAfterUnloadAB
上述资源在窗口销毁后,满足了可回收的三大条件(1. 索引计数为0 2. 绑定对象为空 3. NormalLoad加载方式),最终被成功回收。Note:
读者可能注意到shaderlist索引计数为0,也没绑定对象,但没有被卸载,这是因为shaderlist是被我预加载以常驻资源的形式加载进来的(PermanentLoad),所以永远不会被卸载。
1 2 3 4 5 6
ModuleManager.Singleton.getModule<ResourceModuleManager>().requstResource("shaderlist", (abi) => { abi.loadAllAsset<UnityEngine.Object>(); }, ABLoadType.PermanentLoad); // Shader常驻
可以看到上面我们正确的实现了资源加载的管理。详细的内容这里就不再多说,感兴趣的直接去下载源码吧,这里给出Git链接:
TonyTang1990/AssetBundleLoadManagerNote:
- Git上后续实现和更新了更多的内容,这里就不一一介绍了,详情参见上面的Git链接
资源辅助工具
资源辅助工具五件套:
-
AB删除判定工具
-
资源依赖查看工具
-
AssetDependenciesBrowser
-
内置资源依赖统计工具(只统计了.mat和.prefab,场景建议做成Prefab来统计)
-
内置资源提取工具
-
BuildInResourceExtraction
-
Shader变体搜集工具
AB实战总结
- AB打包和加载是一个相互相存的过程。
- Unity 5提供的增量打包只是提供了更新部分AB的打包机制,*.manifest文件里也只提供依赖的AB信息并非Asset,所以想基于Asset的重用还需要我们自己处理。
- Unity并非所有的Asset都是采用复制而是部分采用复制,部分采用引用的形式,只有正确掌握了这一点,我们才能确保Asset真确的重用和回收。
- 打包策略是根据游戏类型,根据实际情况而定。
- AB压缩格式选择取决于内存和加载性能以及包体大小等方面的抉择。
- 资源管理策略而言,主要分为基于Asset管理(AssetBundle.Unload(false))还是基于AB管理(AssetBundle.Unload(true)),前者容易出现内存中多份重复资源,后者需要确保严格的管理机制(比如索引计数))