Unity AssetBundle的打包 发布 下载与加载
Posted newchenxf
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity AssetBundle的打包 发布 下载与加载相关的知识,希望对你有一定的参考价值。
码字不易,转载请注明出处哦
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
定义:
//
// 摘要:
// AssetBundle building map entry.
public struct AssetBundleBuild
//
// 摘要:
// AssetBundle name.
public string assetBundleName;
//
// 摘要:
// AssetBundle variant.
public string assetBundleVariant;
//
// 摘要:
// Asset names which belong to the given AssetBundle.
public string[] assetNames;
//
// 摘要:
// Addressable name used to load an asset.
[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;
//
// 摘要:
// Loads asset with name of type T from the bundle.
//
// 参数:
// name:
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;
//序列化的类,用于把AssetDataGroup列表序列化到bin文件
[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>
//定义TableIndex, key为AssetData.AssetName, value为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,提出一种改进方案,敬请期待。
以上是关于Unity AssetBundle的打包 发布 下载与加载的主要内容,如果未能解决你的问题,请参考以下文章