文章目錄
  1. 1. 前言
  2. 2. 鸣谢
  3. 3. AssetBundle打包
    1. 3.1. 相关工具
    2. 3.2. AB打包
      1. 3.2.1. 资源冗余
        1. 3.2.1.1. 依赖打包
        2. 3.2.1.2. 增量打包(Unity 5以后)
        3. 3.2.1.3. 依赖Asset信息打包
      2. 3.2.2. 打包策略
      3. 3.2.3. AB压缩格式
      4. 3.2.4. AB打包相关API
      5. 3.2.5. AB打包框架实战
    3. 3.3. AssetBundle加载管理
      1. 3.3.1. 依赖加载还原
      2. 3.3.2. AB加载相关API
      3. 3.3.3. AB回收相关API
      4. 3.3.4. 基于Asset重用的AB加载
      5. 3.3.5. 基于AB引用计数的AB加载管理
    4. 3.4. 新版AssetBundle加载管理,打包以及热更新
      1. 3.4.1. 新版AssetBundle加载管理
        1. 3.4.1.1. 类说明
        2. 3.4.1.2. AB加载管理方案
        3. 3.4.1.3. Demo使用说明
      2. 3.4.2. 新版AssetBundle打包
      3. 3.4.3. 新版资源热更新流程
        1. 3.4.3.1. 类说明
        2. 3.4.3.2. 功能支持
        3. 3.4.3.3. 热更测试说明
        4. 3.4.3.4. 流程图
        5. 3.4.3.5. 热更新辅助工具
        6. 3.4.3.6. 热更包外目录结构
    5. 3.5. 资源辅助工具
    6. 3.6. AB实战总结
  4. 4. Reference

前言

本篇文章是为了记录学习了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以及以后的版本来实现学习的。

相关工具

  1. Unity Profiler(Unity自带的新能分析工具,这里主要用于查看内存Asset加载情况)
  2. Unity Studio(Unity AB以及Asset等资源解析查看工具)
  3. DisUnity(解析AB包的工具)

AB打包

AB打包是把资源打包成assetbundle格式的资源。
AB打包需要注意的问题:

  1. 资源冗余
  2. 打包策略
  3. AB压缩格式

资源冗余

这里说的资源冗余是指同一份资源被打包到多个AB里,这样就造成了存在多份同样资源。

资源冗余造成的问题:

  1. 余造成内存中加载多份同样的资源占用内存。
  2. 同一份资源通过多次IO加载,性能消耗。
  3. 导致包体过大。

解决方案:
依赖打包

依赖打包

依赖打包是指指定资源之间的依赖关系,打包时不将依赖的资源重复打包到依赖那些资源的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信息可以有多种方式:

  1. 通过加载依赖AB,依赖Unity自动还原的机制实现依赖Asset加载还原(这正是本博客实现AssetBundle打包以及加载管理的方案)
  2. 存储挂载相关信息到Prefab上
    在设置好AB名字,打包AB之前,将打包对象上用到的信息通过编写[System.Serializable]可序列化标签抽象数据挂载到该Asset对象上,然后打包AB,打包完AB后在运行时利用打包的依赖信息进行依赖Asset加载还原,从而做到对Asset的生命周期掌控。
    DPInfoMono
  3. 创建打包Asset到同名AB里,加载的时候读取
    通过AssetDatabase.CreateAsset()和AssetImporter.GetAtPath()将依赖的信息写入新的Asset并打包到相同AB里,加载时加载出来使用。
    MaterialAssetInfo

打包策略

除了资源冗余,打包策略也很重要。打包策略是指决定各个Asset如何分配打包到指定AB里的策略。打包策略会决定AB的数量,资源冗余等问题。AB数量过多会增加IO负担。资源冗余会导致包体过大,内存中存在多份同样的Asset,热更新资源大小等。

打包策略:

  1. Logical entities(按逻辑(功能)分类 — 比如按UI,按模型使用,按场景Share等功能分类)
    优点:可以动态只更新特定Entity
  2. Object Types(类型分类 — 主要用于同类型文件需要同时更新的Asset)
    优点:只适用于少部分需要经常变化更新的小文件
  3. Concurrent content(加载时机分类 — 比如按Level分类,主要用于游戏里类容固定(Level Based)不会动态变化加载的游戏类型)
    优点:适合Level based这种一层内容一层不变的游戏类型
    缺点:不适合用于动态创建对象的游戏类型

打包策略遵循几个比较基本的准则:

  1. Split frequently-updated Objects into different AssetBundles than Objects that usually remain unchanged(频繁更新的Asset不要放到不怎么会修改的Asset里而应分别放到不同的AB里)
  2. Group together Objects that are likely to be loaded simultaneously(把需要同时加载的Asset尽量打包到同一个AB里)

从上面可以看出,不同的打包策略适用于不同的游戏类型,根据游戏类型选择最优的策略是关键点。

AB压缩格式

AB压缩不压缩问题,主要考虑的点如下:

  1. 加载时间
  2. 加载速度
  3. 资源包体大小
  4. 打包时间
  5. 下载AB时间
    这里就不针对压缩问题做进一步介绍了,主要根据游戏对于各个问题的需求看重点来决定选择(内存与加载性能的抉择)

AB打包相关API

  1. Selection(获取Unity Editor当前勾选对象相关信息的接口)
1
Object[] assetsselections = Selection.GetFiltered(Type, SelectionMode);
  1. AssetDatabase(操作访问Unity Asset的接口)
1
2
3
4
// 获取选中Asset的路径
assetpath = AssetDatabase.GetAssetPath(assetsselections[i]);
// 获取选中Asset的GUID
assetguid = AssetDatabase.AssetPathToGUID(assetpath);
  1. AssetImporter(获取设置Asset的AB名字的接口)
1
2
3
4
5
// 获取指定Asset的Asset设置接口
AssetImporter assetimporter = AssetImporter.GetAtPath(assetpath);
// 设置Asset的AB信息
assetimporter.assetBundleVariant = ABVariantName;
assetimporter.assetBundleName = ABName;
  1. BuildPipeline(AB打包接口)
1
2
3
BuildPipeline.BuildAssetBundles(outputPath, BuildAssetBundleOptions, BuildTarget); 

BuildPipeline.BuildAssetBundles(string outputPath, AssetBundleBuild[] builds, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform);

可以看到AB打包Unity 5.X提供了两个主要的接口:

  1. 前者是依赖于设置每个Asset的AB名字,然后一个接口完成增量打包的方案。
    优点:
    自带增量打包
    缺点:
    需要有一套资源AB命名规则。
    开发者可控度低。
  2. 后者是提供给开发者自定义哪些Asset打包到指定AB里的一个接口。
    优点:
    可自行实现指定需求的打包规则。
    开发者可控度高。
    缺点:
    不自带增量打包需要自己实现。

Note:
AssetBundle-Browser也是基于前者的一套打包方案,只不过AssetBundle-Browser实现了高度的Asset资源打包可视化操作与智能分析。

AB打包框架实战

这里本人实现的打包方案是使用的后者。(这一套并不完善(未写完,也有不少问题,可以简单看哈借鉴哈思路。)

主要实现了以下功能:

  1. 支持设置四种Asset打包规则(通过设置AB名字)
    • sharerule(作为共享资源,单独打成AB)
    • entirerule(作为一个整体,所有Asset用到的资源打包成一个AB)
    • mutilplerule(作为一个集合,同目录下同类型Asset打包成一个AB)
    • normalrule(作为一个普通资源,谁依赖使用它,他就打包到使用它的Asset所属打包规则的AB里)
  2. 资源划分类型,针对不同资源类型,可以自定义限制打包规则设置以及Unity格式设定检查等。
  3. 可以对内置资源打包做单独处理(比如内置shader,内置default sprite等)
  4. 每次打包生成的依赖文件信息提取出来后最后合并得出最终的依赖信息。
  5. Shader打包资源时自动记录,最后打一次Shader AB即可。

缺点与问题:

  1. 不支持增量打包,每一次打包都是单独分析的。(增量打包这一点很重要)
  2. 依赖分析是根据AssetDatabase.GetDependencies()来做的分析,看网上有提到说
    EditorUtility.CollectDependencies()获取的数据才是更正确的,依赖分析可能有问题。(这个比较好改,直接换成后者即可)
  3. normalrule设计的时候没有考虑到单个normalrule资源被多个同类型资源引用的情况(单个资源单次打包不能指定到多个AB里),这里分析出来的数据会有误(导致normalrule的资源只会被打包到某一个引用他的资源对象AB里)。(比较严重的问题,前期依赖分析设计考虑欠佳导致的)
  4. 因为没有增量打包,每一次都是单独打包,所以只支持同类型或者单个资源分析打包。
  5. 修改某个资源的打包规则会导致需要把所有用到该资源的资源全部重新打包。(很严重的问题)

基于上面的很多缺点,导致本人最终也放弃了继续写该方案的打包。但从这一次打包代码编写中加深了对AssetBundle打包的理解。后面说完AB加载管理会一起附上最后的源代码Git链接。

Note:
个人建议读者参考其他开源库或者使用Assetbundle Browser。

————————————-2021/04/11更新开始——————————————————

参考MotionFramework里的打包思路,这一次准备重新编写AB打包系统,完成之前未完成的完善资源打包系统,且AB加载按路径的形式提供做到编辑器开发走AssetDatabase做到完全无需设定任何AB相关东西,只有AB打包时才需要设定抽象的AB打包策略。

MotionFramework里的AB打包系统,核心是通过单独抽象面向目录设定打包策略的方式来支持AB打包策略的设定。

针对目录首先支持了两种收集规则:

  1. 收集
  2. 忽略

这两种规则的大前提可以支持到我们确保一些无关目录绝对不会被打进AB包。

其次针对目录的AB打包规则支持了三中打包规则设置:

  1. 按文件打包(即文件单个打包)
  2. 按目录打包(即所有文件夹内的都按文件夹名打包)
  3. 固定名字打包(博主个人扩展支持的一个分类)
  4. 不允许打包(确保指定了此打包规则的被引用时报错)

Note:

  1. 单个文件的最终打包策略取决于文件所在最里层设定了打包规则的目录,从而支持了嵌套打包规则设定(Asset的打包规则取决于他上层第一个设定了打包规则的目录)。
  2. AB打包路径和Asset名都是按全路径来定的,完美支持AssetDatabase的编辑器加载模式,从而做到编辑器开发可以完全AB无关化,同时开发期和编辑器资源路径概念一致。
  3. 通过自定义记录AB打包信息(含Asset路径,AB路径,MD5,依赖信息等实现自定义AB加载依赖信息查询比较系统)(这里博主换成了ScriptableObject结合AssetBundle的形式来记录相关信息以及支持热更新)

最后分析结束触发打包时,将前面分析的AB名字设置给BuildPipeline.BuildAssetBundles()指定所有需要打包Asset的AB名字即可。

同时MotionFramework还提供了资源加密AB热更Patch分析等功能,这里暂时以AB打包为核心来实现,后面单独考虑完善后续功能。

接下来开始实战AB打包(中间为了快速拷贝部分MotionFramework代码,大家可以直接去看MotionFramework里的打包系统实现即可):

感谢MotionFramework作者的无私分享,这里给出官方Git连接:

MotionFramework

—————————————2021/04/11更新结束—————————————————

AssetBundle加载管理

实战学习AB加载之前让我们通过一张图先了解下AB与Asset与GameObject之间的关系:
AssetBundleFramework

依赖加载还原

还记得前面说到的依赖的Assest信息打包吗?
这里就需要加载出来并使用进行还原了。
这里接不细说加载还原了,主要就是通过存储的依赖信息把依赖的Asset加载进来并设置回去的过程(可以是手动设置回去也可以是Unity自动还原的方式)。

这里主要要注意的是前面那张大图上给出的各种资源类型在Asset加载还原时采用的方式。
资源加载还原的方式主要有两种:

  1. 复制+引用
    UI — 复制(GameObject) + 引用(Components,Tranform等)
    Material — 复制(材质自身) + 引用(Texture和Shader)

  2. 引用
    Sprite — 引用
    Audio — 引用
    Texture — 引用
    Shader — 引用
    Material — 引用

AB加载相关API

  1. AssetBundle(AB接口)
1
2
3
4
5
6
// 加载本地压缩过的AB
AssetBundle.LoadFromFile(abfullpath)
// 加载AB里的指定Asset
AssetBundle.LoadAsset(assetname);
// 加载AB里的所有Asset
AssetBundle.LoadAllAssets();

AB回收相关API

  1. AssetBundle(AB接口)
1
2
3
4
// 回收AssetBundle并连带加载实例化出来的Asset以及GameObject一起回收
AssetBundle.Unload(true);
// 只回收AssetBundle
AssetBundle.Unload(false);
  1. Resource(资源接口)
1
2
3
4
// 回收指定Asset(这里的Asset不能为GameObject)
Resource.UnloadAsset(asset);
// 回收内存以所有不再有任何引用的Asset
Resources.UnloadUnusedAssets();
  1. GC(内存回收)
1
2
// 内存回收
GC.Collect();

AB回收的方式有两种:

  1. AssetBundle.Unload(false)(基于Asset层面的重用,通过遍历判定的方式去判定Asset是否回收)
  2. AssetBundle.Unload(true)(采取索引技术,基于AB层面的管理,只有AB的引用计数为0时我们直接通过AssetBundle.Unload(true)来卸载AB和Asset资源)

接下来结合这两种方式,实战演练加深理解。

基于Asset重用的AB加载

核心思想:

  1. 打包时存储了依赖的Asset信息,加载时利用存储的依赖信息并还原
  2. 缓存加载进来的Asset的控制权进行重用,卸载AB(AssetBundle.Unload(false))
  3. 加载时通过Clone(复制类型)或者返回缓存Asset(引用类型)进行Asset重用

一下以之前打包的CapsuleNormalMappingTexture.prefab进行详细说明:
CapsuleNormalMappingPrefab.PNG
可以看出CapsuleNormalMappingTexture.prefab用到了如下Asset:

  1. NormalMappingMaterial(材质)
  2. Custom/Texture/NormalMappingShader(Shader)
  3. 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);

第二步:
还原依赖材质
DPInfoMono

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:
核心思想:

  1. 基于AB的引用计数 + AssetBundle.Unload(true)
  2. 给每一种资源(e.g. 特效,模型,图片,窗口等)加载都编写统一的资源加载接口(父类抽象)进行自身加载使用的AB引用计数,每个资源负责自身的资源加载管理和返还(主动调用)
  3. 由一个单例管理者统一管理所有加载AB的引用计数信息,负责判定是否可回收

优点:

  1. 严格的AB引用计数加载管理和释放
  2. 可以做到对资源对象的重用减少GC
  3. 对资源对象的重用可以减少AB的重复加载

缺点:

  1. 底层管理的内容比较多(比如基于资源对象的重用),上层灵活度欠缺(相当于对象池已经写在了最底层)
  2. 需要主动去调用返还接口(针对不同资源加载释放时机都需要编写一套对应的代码)

方案2:
核心思想:

  1. 基于AB的引用计数 + AssetBundle.Unload(true)
  2. 绑定加载AB的生命周期判定到Object上(e.g. GameObject,Image,Material等),上层无需关心AB的引用计数,只需绑定AB到对应的对象上即可
  3. 通过单例管理者统一管理判定依赖使用AB的Object列表是否都为空来判定是否可以回收,无需上层管理AB引用计数返还

优点:

  1. 上层只需关心AB加载绑定,无需关心AB引用计数返还问题,上层使用灵活度高

缺点:

  1. AB的返还判定跟绑定的Object有关,Object被回收后,AB容易出现重复加载(可以在上层写部分对象池来减少AB的重复加载)

考虑到希望上层灵活度高一些,个人现在倾向于第二种方案。

接下来基于第二种方案来实战编写资源AB加载的框架。
AB打包这一块采用最新的可视化指定打包策略的方式。

新版AssetBundle加载管理,打包以及热更新

为了弥补以前设计和实现上不足的地方,从而有了新版AssetBundle加载管理和打包的编写。

新版AssetBundle加载管理

老版的资源加载管理缺点:

  1. 面向AssetBundle级别,没有面向Asset级别的加载管理,无法做到Asset级别的加载异步以及Asset级别加载取消的。
  2. 老版AssetDatabase模式要求资源必须在设置AB名字后才能正确使用(因为依赖了AB名字作为加载参数而非资源全路径),无法做到资源导入即可快速使用的迭代开发
  3. 资源加载类型分类(普通,预加载,常驻)设计过于面向切场景的游戏设计,不通用到所有游戏类型
  4. 老版AssetBundle异步加载采用了开携程的方式,代码流程看起来会比较混乱
  5. 老版异步加载没有考虑设计上支持逻辑层加载打断
  6. 老版代码没有涉及考虑动态AB下载的设计(边玩边下)
  7. 资源加载代码设计还比较混乱,不容易让人看懂看明白

综合上面4个问题,新版资源加载管理将支持:

  1. 面向Asset级别加载管理,支持Asset和AssetBundle级别的同步异步加载。
  2. 支持资源导入后AssetDatabase模式马上就能配置全路径加载
  3. 资源加载类型只提供普通和常驻两种(且不支持运行时切换相同Asset或AssetBundle的加载类型,意味着一旦第一次加载设定了类型,就再也不能改变,同时第一次因为加载Asset而加载某个AssetBundle的加载类型和Asset一致),同时提供统一的加载管理策略,细节管理策略由上层自己设计(比如对象池,预加载)
  4. 新版异步加载准备采用监听回调的方式来实现,保证流程清晰易懂
  5. 新版设计请求UID的概念来支持加载打断设计(仅逻辑层面的打断,资源加载不会打断,当所有逻辑回调都取消时,加载完成时会返还索引计数确保资源正确卸载)
  6. 设计上支持动态AB下载(未来填坑)
  7. 加载流程重新设计,让代码更清晰
  8. 保留索引计数(Asset和AssetBundle级别)+对象绑定的设计(Asset和AssetBundle级别)+按AssetBundle级别卸载(依赖还原的Asset无法准确得知所以无法直接卸载Asset)+加载触发就提前计数(避免异步加载或异步加载打断情况下资源管理异常)
  9. 支持非回调式的同步加载返回(通过抽象Loader支持LoadImmediately的方式实现)

Note:

  1. 一直以来设计上都是加载完成后才添加索引计数和对象绑定,这样对于异步加载以及异步打断的资源管理来说是有漏洞的,新版资源加载管理准备设计成提前添加索引计数,等加载完成后再考虑是否返还计数的方式确保异步加载以及异步加载打断的正确资源管理

加载流程设计主要参考:

XAsset

对象绑定加索引计数设计主要参考:

tangzx/ABSystem

类说明

Manager统一管理:

- ModuleManager(单例类 Manager of Manager的管理类)
- ModuleInterface(模块接口类)

资源加载类:

- ResourceLoadMethod(资源加载方式枚举类型 -- 同步 or 异步)
- ResourceLoadMode(资源加载模式 -- AssetBundle or AssetDatabase(**限Editor模式下可切换,支持同步和异步(异步是本地模拟延迟加载来实现的)加载方式**))
- ResourceLoadState(资源加载状态 -- 错误,等待加载, 加载中,完成,取消之类的)
- ResourceLoadType(资源加载类型 -- 正常加载,常驻加载)
- ResourceModuleManager(资源加载模块统一入口管理类)
- AbstractResourceModule(资源加载模块抽象)
- AssetBundleModule(AssetBundle模式下的实际加载管理模块)
- AssetDatabaseModule(AssetDatabase模式下的实际加载管理模块)
- AbstractResourceInfo(资源加载使用信息抽象)
- AssetBundleInfo(AssetBundle资源使用信息)
- AssetInfo(Asset资源使用信息)
- LoaderManager(加载器管理单例类)
- Loadable(资源加载器基类--抽象加载流程)
- AssetLoader(Asset加载器基类抽象)
- BundleAssetLoader(AssetBundle模式下的Asset加载器)
- AssetDatabaseLoader(AssetDatabase模式下的Asset加载器)
- BundleLoader(AssetBundle加载器基类抽象)
- AssetBundleLoader(本地AssetBundle加载器)
- DownloadAssetBundleLoader(动态资源AsserBundle加载器)
- AssetDatabaseAsyncRequest(AssetDatabase模式下异步加载模拟)
- AssetBundlePath(AB资源路径相关 -- 处理多平台以及热更资源加载路径问题)
- ResourceDebugWindow.cs(Editor运行模式下可视化查看资源加载(AssetBundle和AssetDatabase两种都支持)详细信息的辅助工具窗口)
- ResourceConstData(资源打包加载相关常量数据)
- ResourceLoadAnalyse(资源加载统计分析工具)

AB加载管理方案

加载管理方案:

  1. 加载指定资源
  2. 加载自身AB(自身AB加载完通知资源加载层移除该AB加载任务避免重复的加载任务被创建),自身AB加载完判定是否有依赖AB
  3. 有则加载依赖AB(增加依赖AB的引用计数)(依赖AB采用和自身AB相同的加载方式(ResourceLoadMethod),但依赖AB统一采用ResourceLoadType.NormalLoad加载类型)
  4. 自身AB和所有依赖AB加载完回调通知逻辑层可以开始加载Asset资源(AB绑定对象在这一步)
  5. 判定AB是否满足引用计数为0,绑定对象为空,且为NormalLoad加载方式则卸载该AB(并释放依赖AB的计数减一)(通知资源管理层AB卸载,重用AssetBundleInfo对象)
  6. 切场景,递归判定卸载PreloadLoad加载类型AB资源

相关设计:

  1. 依赖AB与被依赖者采用同样的加载方式(ResourceLoadMethod),但加载方式依赖AB统一采用ResourceLoadType.NormalLoad
  2. 依赖AB通过索引计数管理,只要原始AB不被卸载,依赖AB就不会被卸载
  3. 已加载的AB资源加载类型只允许从低往高变(NormalLoad -> Preload -> PermanentLoad),不允许从高往低(PermanentLoad -> Preload -> NormalLoad)

Demo使用说明

先打开资源调试工具

Tools->Debug->资源调试工具

  1. AssetBundle和AssetDatabase资源加载模式切换AssetDatabaseModuleSwitch

  2. AB依赖信息查看界面

    AssetBundleDepInfoUI

  3. AB运行时加载管理详细信息界面

    AssetBundleLoadManagerUI

  4. 加载器信息查看界面

    AssetBundleAsyncUI

  5. 测试界面

    AssetBundleTestUI

  6. 点击加载窗口预制件按钮后:

    1
    2
    3
    4
    5
    6
    7
    8
    ResourceManager.Singleton.getPrefabInstance(
    "Assets/Res/windows/MainWindow.prefab",
    (prefabInstance, requestUid) =>
    {
    mMainWindow = prefabInstance;
    mMainWindow.transform.SetParent(UIRootCanvas.transform, false);
    }
    );

    AssetBundleLoadManagerUIAfterLoadWindow
    可以看到窗口mainwindow依赖于loadingscreen,导致我们加载窗口资源时,loadingscreen作为依赖AB被加载进来了(引用计数为1),窗口资源被绑定到实例出来的窗口对象上(绑定对象MainWindow)

  7. 点击测试异步转同步加载窗口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/// <summary>
/// 测试异步转同步窗口加载
/// </summary>
public void onAsynToSyncLoadWindow()
{

DIYLog.Log("onAsynToSyncLoadWindow()");
if (mMainWindow == null)
{
onDestroyWindowInstance();
}
AssetLoader assetLoader;
var requestUID = ResourceManager.Singleton.getPrefabInstanceAsync(
"Assets/Res/windows/MainWindow.prefab",
out assetLoader,
(prefabInstance, requestUid) =>
{
mMainWindow = prefabInstance;
mMainWindow.transform.SetParent(UIRootCanvas.transform, false);
}
);
// 将异步转同步加载
assetLoader.loadImmediately();
}
  1. 点击销毁窗口实例对象后
1
2
3
4
5
6
7
8
9
/// <summary>
/// 销毁窗口实例对象
/// </summary>
public void onDestroyWindowInstance()
{

DIYLog.Log("onDestroyWindowInstance()");
GameObject.Destroy(mMainWindow);
}
窗口销毁后可以看到之前加载的资源所有绑定对象都为空了,因为被销毁了(MainWindow被销毁了)

AssetBundleLoadManagerUIAfterDestroyWindow

  1. 等待回收检测回收后
    AssetBundleLoadManagerUIAfterUnloadAB
    上述资源在窗口销毁后,满足了可回收的三大条件(1. 索引计数为0 2. 绑定对象为空 3. NormalLoad加载方式),最终被成功回收。

Note:

读者可能注意到shaderlist索引计数为0,也没绑定对象,但没有被卸载,这是因为shaderlist是被我预加载以常驻资源的形式加载进来的(PermanentLoad),所以永远不会被卸载。

1
2
3
4
5
6
7
8
9
10
11
/// <summary>
/// 加载常驻Shader
/// </summary>
public void onLoadPermanentShaderList()
{

DIYLog.Log("onLoadPermanentShaderList()");
ResourceManager.Singleton.loadAllShader("shaderlist", () =>
{
},
ResourceLoadType.PermanentLoad);
}

新版AssetBundle打包

老版的资源加载管理缺点:

  1. 采用AssetBundleBrowser需要自己一个一个的设置导入资源的AB名字不够灵活,流程过于繁琐只适合个人小项目
  2. 老版打包是没有保留资源目录结构,都是单个名字在统一目录下,这样无法做到统一处理AssetDatabase模式和AssetBundle模式面向Asset级别加载

综合上面2个问题,新版资源打包将支持:

  1. 打包AB的策略由抽象的目录打包策略设定决定

  2. 打包后的AB保留目录结构,确保AB模式和AssetDatabase模式加载都面向Asset路径保持一致性

设计主要参考:

MotionFramework

核心AB打包思想和流程:

  1. 通过抽象纯虚拟的打包策略设置(即AB收集打包策略设置界面—设置指定目录的打包策略),做到AB打包策略设置完全抽象AB名字设置无关化(这样一来无需设置AB或清除AB名字,自己完全掌控AB打包策略和打包结论)
  2. 打包时分析打包策略设置的所有有效资源信息,统计出所有有效资源的是否参与打包以及依赖相关等信息,然后结合所有的打包策略设置分析出所有有效Asset的AB打包名字(如果Asset满足多个打包策略设置,默认采用最里层的打包策略设置,找不到符合的采用默认收集打包规则)(这一步是分析关键,下面细说一下详细步骤)
    • 通过自定义设置的打包策略得到所有的有效参与打包路径列表
    • 通过AssetDatabase.FindAssets()结合打包路径列表得出所有需要分析的Asset
    • 通过AssetDatabase.GetDependencies(*, true)遍历所有需要分析的Asset列表得出所有Asset的使用信息(是否有依赖使用,是否是需要参与打包的灯)
    • 通过遍历得到的所有Asset使用信息的路径信息分析设置Asset的AB打包变体信息
    • 通过遍历得到的所有Asset信息列表结合AssetDatabase.GetDependencies(*, false)接口排除依赖未设置参与打包的Asset资源并得出最终的打包信息列表List
    • 在最后分析得出最后的打包结论之前,这里我个人将AB的依赖信息文件(AssetBuildInfo.asset)的生成和打包信息单独插入在这里,方便AB依赖信息可以跟AB资源一起构建参与热更
    • 最后根据分析得出的打包信息列表List构建真真的打包信息List进行打包
    • AB打包完成后进行一些后续的特殊资源处理(比如视频单独打包。AB打包的依赖文件删除(个人采用自定义生成的AssetBuildInfo.Asset作为依赖加载信息文件)。循环依赖检查。创建打包说明文件等。)
  3. 不同的打包规则通过反射创建每样一个来实现获取对应打包规则的打包AB名字结论获取(采用全路径AB名的方式,方便快速查看资源打包分布)
  4. 然后根据所有有效Asset的所有AB名字打包结论来分析得出自定义的打包结论(即哪些Asset打包到哪个AB名里)
  5. 接着根据Asset的AB打包结论来生成最新的AssetBuildInfo(Asset打包信息,可以理解成我们自己分析得出的Manifest文件,用于运行时加载作为资源加载的基础信息数来源)(手动将AssetBuildInfo添加到打包信息里打包成AB,方便热更新走统一流程)
  6. 最后采用BuildPipeline.BuildAssetBundles(输出目录, 打包信息列表, ……)的接口来手动指定打包结论的方式触发AB打包。

AB打包策略支持了如下几种:

  1. 按目录打包(打包策略递归子目录判定)
  2. 按文件打包(打包策略递归子目录判定)
  3. 按固定名字打包(扩展支持固定名字打包—比如所有Shader打包到shaderlist)(打包策略递归子目录判定)
  4. 按文件或子目录打包(打包策略递归子目录判定,设定目录按文件打包,其他下层目录按目录打包)
  5. 不参与打包(打包策略递归子目录判定)

这里先简单的看下新的AB搜集和打包界面:

AssetBundleCollectWindow

AssetBundleBuildWindow

关于Asset路径与AB路径关联信息以及AB依赖信息最终都存在一个叫assetbundlebuildinfo.asset的ScriptableObejct里(单独打包到assetbuildinfo的AB里),通过Asset路径如何加载到对应AB以及依赖AB的关键就在这里。(这里和MotionFramework自定义Manifest文件输出不一样,采用打包AB的方式,为了和热更新AB走一套机制)

让我们先来看下大致数据信息结构:

AssetBundleBuildInfoView1

AssetBundleBuildInfoView2

新版资源热更新流程

类说明

热更类:

1
2
- HotUpdateModuleManager.cs(热更新管理模块单例类)
- TWebRequest.cs(资源下载http抽象类)

版本信息类:

1
2
- VersionConfigModuleManager.cs(版本管理模块单例类)
- VersionConfig.cs(版本信息抽象类)

功能支持

  1. 支持游戏内版本强更(完成 — 暂时限Android,IOS待测试)
  2. 支持游戏内资源热更(完成 — 暂时限Android, IOS待测试)
  3. 支持游戏内代码热更(未做)

热更测试说明

之前是使用的HFS快速搭建的一个资源本地资源服务器,后来使用阿里的ISS静态资源服务器做了一个网络端的资源服务器。

版本强更流程:

  1. 比较包内版本信息和包外版本信息检查是否强更过版本
  2. 如果强更过版本清空包外相关信息目录
  3. 通过资源服务器下载最新服务器版本信息(ServerVersionConfig.json)和本地版本号作对比,决定是否强更版本
  4. 结合最新版本号和资源服务器地址(Json配置)拼接出最终热更版本所在的资源服务器地址
  5. 下载对应版本号下的强更包并安装
  6. 安装完成,退出游戏重进

资源热更流程:

  1. 初始化本地热更过的资源列表信息(暂时存储在:Application.persistentDataPath + “/ResourceUpdateList/ResourceUpdateList.txt”里)

  2. 通过资源服务器下载最新服务器版本信息(ServerVersionConfig.json)和本地资源版本号作对比,决定是否资源热更

  1. 结合最新版本号,最新资源版本号和资源服务器地址(Json配置)拼接出最终资源热更所在的资源服务器地址

  2. 下载对应地址下的AssetBundleMD5.txt(里面包含了对应详细资源MD5信息)

    AssetBundleMD5.txt

    1
    2
    3
    assetbuildinfo.bundle|ca830d174533e87efad18f1640e5301d
    shaderlist.bundle|2ac2d75f7d91fda7880f447e21b2e289
    ******
  3. 根据比较对应地址下的AssetBundleMD5.txt里的资源MD5信息和本地资源MD5信息(包外的MD5信息优先)得出需要更新下载的资源列表

  4. 根据得出的需要更新的资源列表下载对应资源地址下的资源并存储在包外(Application.persistentDataPath + “/Android/“),同时写入最新的资源MD5信息文件(本地AssetBundleMD5.txt)到本地

  5. 直到所有资源热更完成,退出重进游戏

流程图

HotUpdateFlowChat

热更新辅助工具

Tools->HotUpdate->热更新操作工具

HotUpdateToolsUI

主要分为以下2个阶段:

  • 热更新准备阶段:

    1. 每次资源打包会在包内Resource目录生成一个AssetBundleMd5.txt文件用于记录和对比哪些资源需要热更

    AssetBundleMD5File

    1. 执行热更新准备操作,生成热更新所需服务器最新版本信息文件(ServerVersionConfig.json)并将包内对应平台资源拷贝到热更新准备目录

    HotUpdatePreparationFolder

  • 热更新判定阶段

    1. 初始化包内(AssetBundleMd5.txt)和包外(AssetBundleMd5.txt)热更新的AssetBundle MD5信息(先读包内后读包外以包外为准)

    2. 游戏运行拉去服务器版本和资源版本信息进行比较是否需要版本强更或资源热更新

    3. 需要资源热更新则拉去对应最新资源版本的资源MD5信息文件(AssetBundleMD5.txt)进行和本地资源MD5信息进行比较判定哪些资源需要热更新
    4. 拉去所有需要热更新的资源,完成后进入游戏

Note:

  1. 每次打包版本时会拷贝一份AssetBundleMD5.txt到打包输出目录(保存一份方便查看每个版本的资源MD5信息)

热更包外目录结构

PersistentAsset -> HotUpdate -> Platform(资源热更新目录)
PersistentAsset -> HotUpdate -> AssetBundleMd5.txt(记录热更新的AssetBundle路径和MD5信息—兼顾进游戏前资源热更和动态资源热更)(格式:热更AB路径:热更AB的MD5/n热更AB路径:热更AB的MD5**)
PersistentAsset -> Config -> VersionConfig.json(包外版本信息—用于进游戏前强更和热更判定)

PersistentAsset -> HotUpdate -> 版本强更包

资源辅助工具

资源辅助工具五件套:

  • AB删除判定工具

    DeleteRemovedAssetBundle

  • 资源依赖查看工具

    AssetDependenciesBrowser

  • 内置资源依赖统计工具(只统计了.mat和.prefab,场景建议做成Prefab来统计)

    BuildInResourceReferenceAnalyze

  • 内置资源提取工具

    BuildInResourceExtraction

  • Shader变体搜集工具

    ShaderVariantsCollection

AB实战总结

  1. AB打包和加载是一个相互相存的过程。
  2. Unity 5提供的增量打包只是提供了更新部分AB的打包机制,*.manifest文件里也只提供依赖的AB信息并非Asset,所以想基于Asset的重用还需要我们自己处理。
  3. Unity并非所有的Asset都是采用复制而是部分采用复制,部分采用引用的形式,只有正确掌握了这一点,我们才能确保Asset真确的重用和回收。
  4. 打包策略是根据游戏类型,根据实际情况而定。
  5. AB压缩格式选择取决于内存和加载性能以及包体大小等方面的抉择。
  6. 资源管理策略而言,主要分为基于Asset管理(AssetBundle.Unload(false))还是基于AB管理(AssetBundle.Unload(true)),前者容易出现内存中多份重复资源,后者需要确保严格的管理机制(比如索引计数))

Reference

tangzx/ABSystem
mr-kelly/KEngine
AssetBundles-Browser

文章目錄
  1. 1. 前言
  2. 2. 鸣谢
  3. 3. AssetBundle打包
    1. 3.1. 相关工具
    2. 3.2. AB打包
      1. 3.2.1. 资源冗余
        1. 3.2.1.1. 依赖打包
        2. 3.2.1.2. 增量打包(Unity 5以后)
        3. 3.2.1.3. 依赖Asset信息打包
      2. 3.2.2. 打包策略
      3. 3.2.3. AB压缩格式
      4. 3.2.4. AB打包相关API
      5. 3.2.5. AB打包框架实战
    3. 3.3. AssetBundle加载管理
      1. 3.3.1. 依赖加载还原
      2. 3.3.2. AB加载相关API
      3. 3.3.3. AB回收相关API
      4. 3.3.4. 基于Asset重用的AB加载
      5. 3.3.5. 基于AB引用计数的AB加载管理
    4. 3.4. 新版AssetBundle加载管理,打包以及热更新
      1. 3.4.1. 新版AssetBundle加载管理
        1. 3.4.1.1. 类说明
        2. 3.4.1.2. AB加载管理方案
        3. 3.4.1.3. Demo使用说明
      2. 3.4.2. 新版AssetBundle打包
      3. 3.4.3. 新版资源热更新流程
        1. 3.4.3.1. 类说明
        2. 3.4.3.2. 功能支持
        3. 3.4.3.3. 热更测试说明
        4. 3.4.3.4. 流程图
        5. 3.4.3.5. 热更新辅助工具
        6. 3.4.3.6. 热更包外目录结构
    5. 3.5. 资源辅助工具
    6. 3.6. AB实战总结
  4. 4. Reference