本篇文章是为了记录学习了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加载管理
Note:
这里的AssetBundle打包主要是针对Unity 5.X以及以后的版本来实现学习的。
AB打包是把资源打包成assetbundle格式的资源。
AB打包需要注意的问题:
这里说的资源冗余是指同一份资源被打包到多个AB里,这样就造成了存在多份同样资源。
资源冗余造成的问题:
解决方案:
依赖打包
依赖打包
依赖打包是指指定资源之间的依赖关系,打包时不将依赖的资源重复打包到依赖那些资源的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信息可以有多种方式:
除了资源冗余,打包策略也很重要。打包策略是指决定各个Asset如何分配打包到指定AB里的策略。打包策略会决定AB的数量,资源冗余等问题。AB数量过多会增加IO负担。资源冗余会导致包体过大,内存中存在多份同样的Asset,热更新资源大小等。
打包策略:
打包策略遵循几个比较基本的准则:
从上面可以看出,不同的打包策略适用于不同的游戏类型,根据游戏类型选择最优的策略是关键点。
AB压缩不压缩问题,主要考虑的点如下:
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提供了两个主要的接口:
Note:
AssetBundle-Browser也是基于前者的一套打包方案,只不过AssetBundle-Browser实现了高度的Asset资源打包可视化操作与智能分析。
这里本人实现的打包方案是使用的后者。(这一套并不完善(未写完,也有不少问题,可以简单看哈借鉴哈思路。)
主要实现了以下功能:
缺点与问题:
基于上面的很多缺点,导致本人最终也放弃了继续写该方案的打包。但从这一次打包代码编写中加深了对AssetBundle打包的理解。后面说完AB加载管理会一起附上最后的源代码Git链接。
Note:
个人建议读者参考其他开源库或者使用Assetbundle Browser。
实战学习AB加载之前让我们通过一张图先了解下AB与Asset与GameObject之间的关系:AssetBundleFramework
还记得前面说到的依赖的Assest信息打包吗?
这里就需要加载出来并使用进行还原了。
这里接不细说加载还原了,主要就是通过存储的依赖信息把依赖的Asset加载进来并设置回去的过程(可以是手动设置回去也可以是Unity自动还原的方式)。
这里主要要注意的是前面那张大图上给出的各种资源类型在Asset加载还原时采用的方式。
资源加载还原的方式主要有两种:
复制+引用
UI — 复制(GameObject) + 引用(Components,Tranform等)
Material — 复制(材质自身) + 引用(Texture和Shader)
引用
Sprite — 引用
Audio — 引用
Texture — 引用
Shader — 引用
Material — 引用
1 2 3 4 5 6 |
// 加载本地压缩过的AB AssetBundle.LoadFromFile(abfullpath) // 加载AB里的指定Asset AssetBundle.LoadAsset(assetname); // 加载AB里的所有Asset AssetBundle.LoadAllAssets(); |
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回收的方式有两种:
接下来结合这两种方式,实战演练加深理解。
核心思想:
一下以之前打包的CapsuleNormalMappingTexture.prefab进行详细说明:CapsuleNormalMappingPrefab.PNG
可以看出CapsuleNormalMappingTexture.prefab用到了如下Asset:
开始加载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 |
第三步:
还原依赖材质的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的数量回到了最初的数值。
这是本文重点分享的部分
方案1:
核心思想:
优点:
缺点:
方案2:
核心思想:
优点:
缺点:
考虑到希望上层灵活度高一些,个人现在倾向于第二种方案。
接下来基于第二种方案来实战编写资源AB加载的框架。
AB打包这一块打算先使用之前自己写的一套没有增量更新但支持一定打包规则指定的打包方案。
AB加载管理框架
以下实现主要参考了下面两个开源项目:
Unity3D AssetBundle 打包与管理系统
KEngine
框架设计
支持功能:
更多学习参考:
浅谈 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加载方式:
AssetBundle.LoadFromFile()
AssetBundle.LoadFromFileAsync()
加载管理方案:
AB加载管理相关概念:
Note:
Demo
点击加载窗口预制件按钮后:
1 2 3 4 5 6 |
ModuleManager.Singleton.getModule |
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 |
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 |
可以看到上面我们正确的实现了资源加载的管理。详细的内容这里就不再多说,感兴趣的直接去下载源码吧,这里给出Git链接:
TonyTang1990/AssetBundleLoadManager
Note:
资源辅助工具五件套:
AB删除判定工具
资源依赖查看工具
AssetDependenciesBrowser
内置资源依赖统计工具(只统计了.mat和.prefab,场景建议做成Prefab来统计)
内置资源提取工具
BuildInResourceExtraction
Shader变体搜集工具
tangzx/ABSystem
mr-kelly/KEngine
AssetBundles-Browser