码字不易,转载请注明出处哦 https://blog.csdn.net/newchenxf/article/details/124738469
1 前文
都2022了,为什么还讨论AB包?不是有Addressables 了嘛! 本文之所以讨论,是为了梳理一下AssetBundle的优缺点,方便跟Addressables做对比。
本文分两部分,一是AssetBundle的介绍,以及不使用任何第三方插件,如何使用AssetBundle的API。二是,介绍第三方插件,即QFramework的ResKit如何封装AssetBundle API,做资源加载。
2 AssetBundle介绍
AssetBundle (简称AB包)是一种资源压缩包,可以包含模型、贴图、预制体、声音、甚至整个场景,可以在游戏运行的时候被加载;
它存在的意义 : 对资源压缩,上传到服务器,app开启后再下载/加载,可以有效的减少包大小 ,还可以热更新 (资源不对可以换)
AssetBundle一般有多个,彼此之间可以有依赖 关系;例如,一个 AssetBundle 中的材质可以引用另一个 AssetBundle 中的纹理。当然了,没依赖最好,不容易出错。
2.1 AB包的文件结构
和很多文件一样,它包含文件头和数据区。 文件头: 该AB包的关键信息,例如压缩算法,数据清单(每个资源在bundle中的索引值) 数据区: 压缩过的资源数据。
要使用AssetBundle,一般需要以下工作:
- 将想要动态加载的资源添加到AssetBundle(可以有多个bundle)
- 要发布时,做打包工作
- 自己把bundle上传到服务器,服务器管理版本
- 客户端下载bundle,版本不匹配(md5校验啥的),则下载新bundle
- 加载指定路径的bundle,提取文件
- 卸载bundle, 节约内存
下面根据每一条说一下,不依赖任何框架,打AB包,都靠unity的啥东西。
2.2 AB包的使用流程
2.2.1 将资源加载到AB包
你可以选中任何一个资源,在对应的Inspector 面板的最下面,就有一个AB包的添加入口: 默认None 表示,该资源不加入到AB包。点击箭头,可以新增一个AB包的名字,然后选中,就会添加到这个AB包。
可以通过如下方法,获得所有的AB包:
string[] assetBundleNames = AssetDatabase.GetAllAssetBundleNames();
可以通过如下的方法,获得某个AB包所包含的资源路径列表:
string[] aFiles = AssetDatabase.GetAssetPathsFromAssetBundle(assetBundleName);
2.2.2 要发布时,做打包工作
Unity有API做打包,一个常用的API如下:
public class BuildPipeline {
public static AssetBundleManifest BuildAssetBundles(string outputPath, AssetBundleBuild[] builds, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform);
}
outputPath : AB包打出来放到哪个路径 builds : AB包的数据列表,来看一下AssetBundleBuild 定义:
public struct AssetBundleBuild
{
public string assetBundleName;
public string assetBundleVariant;
public string[] assetNames;
[NativeName("nameOverrides")]
public string[] addressableNames;
}
很简单,主要就是AB包的名字,和ab包内的各种资源名字列表。
targetPlatform : 编译平台
返回值 :编译出AB包后,还会生成一个manifest文件,例如Android.manifest,包含所有的ab包名字和依赖 关系。
例如我这里打包好后,manifest文件如下:
ManifestFileVersion: 0
CRC: 3548016675
AssetBundleManifest:
AssetBundleInfos:
Info_0:
Name: sceneres_unity
Dependencies: {}
Info_1:
Name: a_png
Dependencies: {}
Info_2:
Name: textureexample1_png
Dependencies: {}
2.2.3 自己把bundle上传到服务器,服务器管理版本
这一块,就可以各家公司自己操心了,Unity的AB包方案,不关心这一块,不像Addressable方案。
2.2.4 客户端下载bundle
同上,各家公司自己操心。 当然了,Unity也提供了下载方法:
public static UnityWebRequest GetAssetBundle(string uri)
UnityWebRequestAssetBundle.GetAssetBundle(*)
2.2.5 加载指定路径的bundle
Unity提供了如下的API,来读取bundle数据,有同步或者异步的方法。
public class AssetBundle {
public static AssetBundle LoadFromFile(string path);
public static AssetBundleCreateRequest LoadFromFileAsync(string path);
public static AssetBundle LoadFromMemory(byte[] binary);
public static AssetBundleCreateRequest LoadFromMemoryAsync(byte[] binary);
}
一般不推荐用LoadFromMemory,因为参数就是把整个bundle都读取到内存了,浪费内存资源。 LoadFromFile则比较实惠,会先读取文件头信息,等到实际加载资源某个资源时再读取具体数据段。
上面是读取bundle包,得到的是AssetBundle 类型的对象。得到该对象后,就可以调用LoadAsset 方法,读取某个资源。(注意,早些年的版本,是Load 方法,这个已经废弃啦,至少2022年已经废弃啦)
public T LoadAsset<T>(string name) where T : Object;
public Object LoadAsset(string name);
3 第三方的AB包工具
上文说的是Unity的官方的API,打包呀,加载呀,都可以自己开发,提供了官方API,但这些其实是很通用的工作,所以不少公司/开发者开发了插件,提供UI界面,帮你去打包,提供上层函数,帮你加载。
本文就基于一个插件QFramework @ ResKit, 来逐步说明原理。其他公司的插件,其实都差不多。
4 QFramework ResKit 工具
QFramework 是一个开源的Unity开发框架和工具集。开发框架有UIKit, ResKit等。 其中ResKit 模块是用于帮助打AB包和加载AB包。我们就专门看一下ResKit是如何实现的。
ps: 本文下载的版本是v0.14.68 ,2022年下载的,所以如果哪里有说明代码不匹配的,轻拍哦
4.1 Reskit的标记资源原理
打包前,需要标记资源到AssetBundle。
4.1.1 如何标记
选中某一个资源,右键,选择@ResKit。如此,在右边的AssetLabels,就会新建一个AB包名字,名字以该资源的名字来取,全部为小写,“. ”改为“_ ”。 接着会把资源归属到该AB包。
4.1.2 查看标记
选择菜单栏的QFramework -> Tool Kit -> Res Kit 界面简单,就是哪个资源加入到AB包了。 缺点: 标记一个资源,就新增一个AB包,且看不到AB包的名字!比如上面新增的samplescene_unity
优点 任何一个资源都有单独的ab包,可以针对每个文件热更新。
4.1.3 标记时的代码分析
右键选择时,调用如下函数。 AssetImporter.GetAtPath 会新增一个AB包,并把当前的资源加到该AB包。AB包的名字和资源名字类似。例如资源是AssetObj.prefab,则AB包就是assetobj_prefab
4.2 ResKit的打包原理
在4.1.2 节的图中,选择【打AB包】,就会生成bundle包,放到StreamingAssets目录下。
打包的代码入口如下:
ResKitEditorAPI.BuildAssetBundles();
这个函数的调用BuildScripts 的BuildAssetBundles 。具体如下 两个核心工作:
- 调用Unity的
BuildPipeline.BuildAssetBundles API 打包。 - 调用ResKit的
AssetBundleExporter.BuildDataTable API,检索每个资源的详细信息,然后存到一个bin文件。客户端在加载资源时,会解析bin文件,拿出资源信息,生成一个table,方便使用。
对于1,比较简单没啥好说的了。对于2,来看一下具体实现:
4.2.1 Reskit生成资源信息表
核心函数BuildDataTable 如下 两件事情:
- 新建
ResDatas ,调用AddABInfo2ResDatas ,把每个bundle的数据(asset名字,bundle名字等),记录到ResDatas的mAllAssetDataGroup 。 - 把
mAllAssetDataGroup 序列化,存为一个asset_bundle_config.bin 文件。
4.2.1.1 类定义说明
函数首先创建了一个ResDatas ,这是一个全局的变量,编译一次,对应一个,定义如下:
public sealed class ResDatas : IResDatas
{
public static string FileName = "asset_bundle_config.bin";
private readonly List<AssetDataGroup> mAllAssetDataGroup = new List<AssetDataGroup>();
private AssetDataTable mAssetDataTable = null;
[Serializable]
public class SerializeData
{
private AssetDataGroup.SerializeData[] mAssetDataGroup;
}
}
有两个很重要的变量:
一个是 mAllAssetDataGroup ,一个bundle对应一个AssetDataGroup,每个Group就是记录该bundle所管理的asset。这个Group列表,将会被序列化,存到bin文件,依靠的是SerializeData类 。 一个是AssetDataTable ,这里生成bin文件用不到。但是,在客户端加载的时候,就需要使用了!会遍历mAllAssetDataGroup数据,生成一个查找表,方便查询使用。 这个表的key为AssetData.AssetName , value为AssetData
AssetDataTable 类的核心定义如下:
public class AssetDataTable : Table<AssetData>
{
public TableIndex<string, AssetData> NameIndex = new TableIndex<string, AssetData>(data => data.AssetName);
protected override void OnAdd(AssetData item)
{
NameIndex.Add(item);
}
}
说完定义,看一下具体的AddABInfo2ResDatas 都干啥了
4.2.1.2 AddABInfo2ResDatas 添加数据到group
三个重要的工作:
- 调用Unity官方API
AssetDatabase.GetAllAssetBundleNames() ,得到所有的bundle名字列表,然后遍历。 - 调用Unity官方API
AssetDatabase.GetAssetPathsFromAssetBundle(abName); ,获得一个bundle的所有资源路径(assets变量的类型是string[]),然后遍历。 - 遍历得到的资源路径,生成一个AssetData,添加到AssetGroup。 所以一个bundle,将对应一个group。
4.2.2 序列化数据得到bin文件
也就是把mAllAssetDataGroup 序列化,存为一个asset_bundle_config.bin 文件。 来看ResDatas 的Save 函数: outpath是bin文件的输出路径,比如我这里是
D:/work/unity/ChenxfTest/Assets/StreamingAssets//AssetBundles/Android/asset_bundle_config.bin
4.3 ResKit的加载原理
咱们先看一下ResKit的使用例子,然后再根据例子展开。
public class ResKitExample : MonoBehaviour
{
private ResLoader mResLoader = ResLoader.Allocate();
private void Start()
{
ResKit.Init();
prefab = mResLoader.LoadSync<GameObject>("AssetObj");
gameObj = Instantiate(prefab);
gameObj.name = "这是使用通过 AssetName 加载的对象";
prefab = mResLoader.LoadSync<GameObject>("assetobj_prefab", "AssetObj");
gameObj = Instantiate(prefab);
gameObj.name = "这是使用通过 AssetName 和 AssetBundle 加载的对象";
}
private void OnDestroy()
{
mResLoader.Recycle2Cache();
mResLoader = null;
}
}
很简单,ResKit.Init() 做初始化。mResLoader.LoadSync 做加载。 所以要分析原理,从这2个函数入手即可。
4.3.1 ResKit.Init 生成资源关系表
先总结:这个方法的主要目的,是找到bin文件,反序列化,得到所有的bundle数据,进而生成table(资源名字和资源的位置关系表)
接着说一下具体实现,ResKit封装了同步或异步的方法:
public partial class ResKit
public static void Init()
{
ResMgr.Init();
}
public static IEnumerator InitAsync()
{
yield return ResMgr.InitAsync();
}
}
两种方法类似,我们以同步为例子,来看ResMgr.Init干啥了
public static void Init()
{
SafeObjectPool<AssetBundleRes>.Instance.Init(40, 20);
SafeObjectPool<AssetRes>.Instance.Init(40, 20);
SafeObjectPool<ResourcesRes>.Instance.Init(40, 20);
SafeObjectPool<NetImageRes>.Instance.Init(40, 20);
SafeObjectPool<ResSearchKeys>.Instance.Init(40, 20);
SafeObjectPool<ResLoader>.Instance.Init(40, 20);
Instance.InitResMgr();
}
初始化对象池,方便后续加载每个资源。 接着InitResMgr。来看一下这个函数: 还算简单。 ResKit分为两种模式: 在开发阶段,叫模拟模式,AssetBundlePathHelper.SimulationMode = true ,意味着是Unity编辑器直接运行,此时没有打AB包,直接根据现有数据生成table表(每个资源在哪里的表)。
在发布阶段,就是非模拟模式了,程序是在真机运行了。 此时编译代码,需要把编辑窗口的【模拟模式】取消勾线,程序才会走到上面的else 那一段。
else主要目的,是得到bin文件并解析,生成table 。要得到bin文件,区分两种情况:
- ab包和bin文件内置在包里。
- ab包和bin文件动态下载。
对于1,程序走到ResKit....GetFileInInner ,具体而言,是从Application.streamingAssetsPath 目录或子目录读取bin文件。 对于2,程序走到AssetBundlePathHelper.GetFileInFolder ,是从Application.PersistentDataPath 目录或子目录 读取bin文件。
Application.streamingAssetsPath :是app的安装目录。在Android端,是在app的安装目录,只能读,不能写。例如: /data/app/com.DefaultCompany.ChenxfTest-FhvI5ggT3YjhkPWP9_zAHw==/base.apk/!/assets/ Application.PersistentDataPath : 是app运行期间的数据存储目录。在Android端,就是SD卡的外部存储目录,例如:/storage/emulated/0/Android/data/com.DefaultCompany.ChenxfTest/files
4.3.2 ResLoader.LoadSync 加载具体资源
该函数传入资源名字,作为key,然后去上一节说的资源关系表table查找资源在哪里,然后做加载。
加载的目标 ,是得到IRes 接口的具体对象,这个很重要,所以先提出: 有几个类会继承该接口,对于从AB包加载的情况,涉及如下2个:
AssetRes AssetBundleRes
接下来讨论如何做具体加载: ResLoader同样提供了同步和异步的加载方法,都差不多,本文仅针对同步方法LoadSync展开。
public class ResLoader
public Object LoadSync(string name)
{
var resSearchRule = ResSearchKeys.Allocate(name);
IRes retAsset = LoadResSync(resSearchRule);
resSearchRule.Recycle2Cache();
tempDepends.Clear();
return retAsset.Asset;
}
}
很简单,根据名字,生成查找key,然后调用LoadResSync 加载。 key是多种参数的组合,不只是一个string。例如
public class ResSearchKeys : IPoolable,IPoolType
{
public string AssetName { get; set; }
public string OwnerBundle { get; set; }
public Type AssetType { get; set; }
public string OriginalAssetName { get; set; }
接着就调用LoadResSync 了
4.3.2.1 LoadResSync的实现
这个方法,主要包含三件事,都比较关键。
- Add2Load的目标,是生成2个IRes对象。
- LoadSync,加载IRes对象的Load方法,做实际加载
- ResMgr.Instance.GetRes 得到数据
下面分别展开
4.3.2.2 Add2Load生成2个IRes对象
如上,红色注释,先调用ResMgr.Instance.GetRes(resSearchKeys, true) 得到一个AssetRes 对象, 然后,因为资源肯定在某个bundle中,而且资源可能还依赖其他bundle的加载,所以,又生成了一个类型为AssetBundle 的key,循环调用Add2Load方法,得到一个AssetBundleRes 对象。
也就是,把bundle文件本身,也当作一个资源了!
最后都添加到一个mWaitLoadList 列表里。
如果想深究2个对象怎么生成的,那就看ResMgr.Instance.GetRes 的实现了。不关心可以跳过了。
4.3.2.3 ResMgr.Instance.GetRes的实现
这个方法主要调用ResFactory.Create 方法。 先说明mResCreators 有多种类型:
public static List<IResCreator> mResCreators = new List<IResCreator>()
{
new ResourcesResCreator(),
new AssetBundleResCreator(),
new AssetResCreator(),
AssetBundleSceneResCreator,
new NetImageResCreator(),
new LocalImageResCreator()
};
对于AB包的加载,用的是AssetBundleResCreator和AssetResCreator。
上面的方法,很精简清晰,根据Match 方法,找到和search key匹配的creator,然后调用对应的Create 。
4.3.2.3.1 Match方法如何确定
AssetResCreator对应的Match方法
public class AssetResCreator : IResCreator
{
public bool Match(ResSearchKeys resSearchKeys)
{
var assetData = AssetBundleSettings.AssetBundleConfigFile.GetAssetData(resSearchKeys);
if (assetData != null)
{
return assetData.AssetType == ResLoadType.ABAsset;
}
....
AssetBundleSettings.AssetBundleConfigFile 就是4.2.1 节说创建的ResDatas 对象! 我们说过从bin文件反序列化,就是为了得到一个table,即ResDatas 的mAssetDataTable 变量。现在派上用场了。
来看一下GetAssetData干啥了。
简单,得到我们打包时,就用到了Asset对象! 再回顾一下@4.2.1.2节,我们曾经针对每个资源这样添加到group: 创建AssetData,只区分两种类型,要么是ResLoadType.ABScene,要么是ResLoadType.ABAsset。
所以,AssetResCreator的Match方法的这一段:
return assetData.AssetType == ResLoadType.ABAsset;
也就返回true了,意味着工厂模式,命中该对象!
AssetBundleResCreator的match方法
回到@4.3.2.2节,有这样一段:
var searchRule = ResSearchKeys.Allocate(depend, null, typeof(AssetBundle));
即类型为AssetBundle 。再看一下
public class AssetBundleResCreator : IResCreator
{
public bool Match(ResSearchKeys resSearchKeys)
{
return resSearchKeys.AssetType == typeof(AssetBundle);
}
类型匹配,对上了!
4.3.2.3.2 Create方法如何生成
其中AssetBundleResCreator 会创建AssetBundleRes AssetResCreator 会创建AssetRes
来看一下这两个Create方法:
public IRes Create(ResSearchKeys resSearchKeys)
{
return AssetBundleRes.Allocate(resSearchKeys.AssetName);
}
public IRes Create(ResSearchKeys resSearchKeys)
{
return AssetRes.Allocate(resSearchKeys.AssetName, resSearchKeys.OwnerBundle, resSearchKeys.AssetType);
}
都很简单,创建对象而已,不再展开咯
4.3.2.4 LoadSync 做实际资源加载
上面的Add2Load已经把2个对象加到了mWaitLoadList,现在循环取出来,调用对应的LoadSync对象。 因为是AssetBundleRes先添加,所以它先调用。这也合理,因为AssetRes其实依赖AssetBundleRes先加载。
4.3.2.4.1 AssetBundleRes LoadSync得到资源对应的bundle对象
来看下AssetBundleRes的LoadSync方法
public override bool LoadSync()
{
var url = AssetBundleSettings.AssetBundleName2Url(mAssetName);
bundle = AssetBundle.LoadFromFile(url);
}
根据bundle的名字,找到存储路径。AssetBundleName2Url方法比较重要,贴一下:
public static string AssetBundleName2Url(string name)
{
string retUrl = AssetBundlePathHelper.PersistentDataPath + "AssetBundles/" +
AssetBundlePathHelper.GetPlatformName() + "/" + name;
if (File.Exists(retUrl))
{
return retUrl;
}
return AssetBundlePathHelper.StreamingAssetsPath + "AssetBundles/" +
AssetBundlePathHelper.GetPlatformName() + "/" + name;
}
先从app的外部存储目录下寻找,没找到,则从安装目录找。
注意,这里写死了必须是具体目录下的子目录,即AssetBundles/[平台]/ ,这有点不够灵活,可以根据你的需求修改一下。
anyway,我这里编辑器下运行,得到的路径是: D:/work/unity/ChenxfTest/Assets/StreamingAssets/AssetBundles/Android/assetobj_prefab Android手机运行,得到的路径是 /storage/emulated/0/Android/data/com.DefaultCompany.ChenxfTest/files/AssetBundles/Android/assetobj_prefab
有了AB包的路径,就可以调用Unity的官方API做加载了: 即
AssetBundle.LoadFromFile
如此,得到了一个AB包对象。
4.3.2.4.2 AssetRes.LoadSync 得到具体资源
整体上也比较简单,上一步其实得到了AssetBundleRes,其中有个变量是AssetBundle,代表bundle包的实际数据。 调用该对象的LoadAsset 方法,即可得到实际的资源了!这也呼应了@2.2.5节。
5 总结
个人认为,ResKit在资源打包界面这一块,不太清晰。基本上一个资源就对应一个AB包,而且界面没法看一个资源对应哪个AB包。 而Addressable就很清晰,从下图可见,ChenxfGroup是一个AB包,所管理的资源key(名字或路径都可以),资源路径,很清晰。
当然了,ResKit在资源加载这一块,就做的比较好,逻辑清晰。
后面将基于ResKit,提出一种改进方案,敬请期待。
|