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,一般需要以下工作:

  1. 将想要动态加载的资源添加到AssetBundle(可以有多个bundle)
  2. 要发布时,做打包工作
  3. 自己把bundle上传到服务器,服务器管理版本
  4. 客户端下载bundle,版本不匹配(md5校验啥的),则下载新bundle
  5. 加载指定路径的bundle,提取文件
  6. 卸载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();

这个函数的调用BuildScriptsBuildAssetBundles。具体如下

两个核心工作:

  1. 调用Unity的BuildPipeline.BuildAssetBundles API 打包。
  2. 调用ResKit的AssetBundleExporter.BuildDataTable API,检索每个资源的详细信息,然后存到一个bin文件。客户端在加载资源时,会解析bin文件,拿出资源信息,生成一个table,方便使用。

对于1,比较简单没啥好说的了。对于2,来看一下具体实现:

4.2.1 Reskit生成资源信息表

核心函数BuildDataTable如下

两件事情:

  1. 新建ResDatas,调用AddABInfo2ResDatas,把每个bundle的数据(asset名字,bundle名字等),记录到ResDatas的mAllAssetDataGroup
  2. 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

三个重要的工作:

  1. 调用Unity官方API AssetDatabase.GetAllAssetBundleNames(),得到所有的bundle名字列表,然后遍历。
  2. 调用Unity官方API AssetDatabase.GetAssetPathsFromAssetBundle(abName);,获得一个bundle的所有资源路径(assets变量的类型是string[]),然后遍历。
  3. 遍历得到的资源路径,生成一个AssetData,添加到AssetGroup。 所以一个bundle,将对应一个group。

4.2.2 序列化数据得到bin文件

也就是把mAllAssetDataGroup序列化,存为一个asset_bundle_config.bin文件。
来看ResDatasSave函数:

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文件,区分两种情况:

  1. ab包和bin文件内置在包里。
  2. 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的实现


这个方法,主要包含三件事,都比较关键。

  1. Add2Load的目标,是生成2个IRes对象。
  2. LoadSync,加载IRes对象的Load方法,做实际加载
  3. 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,即ResDatasmAssetDataTable变量。现在派上用场了。

来看一下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的打包 发布 下载与加载的主要内容,如果未能解决你的问题,请参考以下文章

实力封装:Unity打包AssetBundle

unity2018怎么打包assetbundle?

unity 发布webgl怎么加载assetbundle

Unity5版本的AssetBundle打包方案之打包Scene场景

Unity5自动命名Assetbundle并打包

unity打包AssetBundle包的几种压缩方式介绍