编辑
2024-09-13
Unity
00

目录

AB包是什么
AB包的作用
打包说明
1.勾选标记资源
2.官方打包工具AssetBundle Browser
面板参数讲解
3.打包后生成的文件
AB包的使用
加载AB包
从AB包中加载资源
异步加载AB包
卸载AB包
AB包的依赖
引用计数
归纳总结——AssetBundelManager

AB包是什么

  • 特定于平台的资产压缩包,有点类似压缩文件
  • 能够压缩的资产包括:模型、贴图、预设体、音效、材质球等等

AB包的作用

  1. 减小安装包体积 —— 将资源按需拆分打包,避免所有资源都打进主包,减少初始安装包的大小;
  2. 动态的按需加载与卸载,减少运行时的内存 —— 通过AssetBundle可以在运行时从本地或服务器加载资源,并在不用时卸载释放内存;
  3. 热更新支持 —— 不需要重新打包整个应用,只需替换或下载新的AB包即可实现资源和脚本的更新

打包说明

1.勾选标记资源

选中资源,在它的Inspector窗口底部,可以将当前资源加入某个标签或新建一个标签,标签的名字就是AB包的名字,这里的包名是强制小写的

image.png

image.png

2.官方打包工具AssetBundle Browser

AB包打包工具可以通过编辑器扩展自己写一个,调用AB包相关的API即可。在这里我直接使用官方的省事;

面板参数讲解

  1. Configure

    显示当前所有的标签和对应有哪些资源,如图,model包中包含一个Cube资源,一目了然 image.png

  2. Build
    image.png

    • BuildTarget:目标平台
    • Output Path:目标输出路径
    • Clear Folders:是否清空文件夹 重新打包
    • Copy To StreamingAssets:是否拷贝到StreamingAssets
    • Compression
    • NoCompression
      • 不压缩,解压快,包较大 不推荐
    • LZMA
      • 压缩最小,解压慢
      • 缺点:用一个资源 要解压所有
    • LZ4
      • 压缩,相对LZMA大一点点
      • 建议使用,用什么解压什么,只解压你所需要的,内存占用低
    • ETI
      • 在资源包中 不包含资源的类型信息
    • FR
      • 重新打包时需要重新构建包,和ClearFolders不同,它不会删除不再存在的包
    • ITTC
      • 增量构建检查时,忽略类型数的更改
    • Append Hash
      • 将文件哈希值附加到资源包名上
    • SM
      • 严格模式,如果打包时报错了,则打包直接失败无法成功
    • DRB
      • 运行时构建
  3. Inspect

    主要是可视化的查看AB包中的详细信息 image.png

3.打包后生成的文件

跳转到打包后的输出路径,查看文件夹下的内容

image.png

  • model:这是AB包文件,也就是我们刚刚定义的AB包,包名字就叫model
  • ios:这个和输出路径的同名文件,是AB包的主包,它是AssetBundle主清单文件,包含了所有被打包的 AssetBundle 名称,以及它们之间的依赖关系
  • .manifest:每个AB包都会生成一个.manifest文件,用于描述对应的AB包的信息,主要记录,可以直接双击打开查看: image.png 主要包含以下几个部分:
    1. 版本校验
      • 包含 Hash 值和 CRC 校验码,用于比对更新和校验下载是否完整
      • ManifestFileVersion:说明 manifest 文件的版本号;
      • CRC:资源包的 校验码(循环冗余校验),运行时可用来检测资源是否损坏;
      • 哈希相关:
        • AssetFileHash:AB 文件整体的哈希,用于版本对比和一致性校验;
        • TypeTreeHash:类型树(TypeTree)的哈希,确保序列化结构一致,避免不同 Unity 版本序列化格式不兼容;
        • IncrementalBuildHash:增量构建时使用的哈希,判断是否需要重新打包
        • HashAppended:标志位,说明是否在 AB 包文件结尾附加了 Hash 信息(一般是 0 或 1);
    2. 类型信息
      • ClassTypes:列出 AB 包里包含的 Unity 对象类型,以 Class ID 表示
      • 比如:Class: 1 → GameObject;Class: 4 → Transform;Class: 33 → MeshRenderer
      • SerializeReferenceClassIdentifiers:用于记录 SerializeReference 序列化的自定义类标识符(如果没有就为空数组)
    3. 资源内容
      • 显示该 AB 包中具体包含的资源路径
    4. 资源依赖
      • Dependencies数组中包含相关资源依赖

AB包的使用

加载AB包

使用StreamingAssets该文件夹,我们在打包的时候已经勾选了复制到StreamingAssets文件夹当中了,使用这个文件夹的目的是:

  1. 跨平台兼容,无论当前是在任何平台,UNITY都会根据不同的平台自动返回正确的路径

    Windows: Application.dataPath + "/StreamingAssets"

    macOS: Application.dataPath + "/Resources/Data/StreamingAssets"

    Android: "jar:file://" + Application.dataPath + "!/assets"

    iOS: Application.dataPath + "/Raw"

  2. StreamingAssets 文件夹中的资源会被原样打包到最终游戏中,不会被 Unity 压缩或处理,保持 AssetBundle 完整性

  3. 这个文件夹是是只读的,确保 AssetBundle 不会被意外修改,保证安全

  4. 性能优势,直接从安装包中加载,速度快,不需要额外的文件复制操作

csharp
string path = Application.streamingAssetsPath + "/" + "model"; AssetBundle ab = AssetBundle.LoadFromFile(path);

从AB包中加载资源

csharp
//建议使用泛型+名字的方式,避免出现同名不同类型的情况 GameObject obj = ab.LoadAsset<GameObject>("Cube"); //也是指定类型的方式来加载AB包中的资源,不同于泛型的是这是通过反射的方式,因为在热更新中如果使用的是Lua脚本,就只能使用这种方式,因为Lua脚本不支持泛型 GameObject obj = ab.LoadAsset("Cube", typeof(GameObject)) as GameObject; //在场景中实例化出加载的对象 Instantiate(obj, Vector3.zero, Quaternion.identity);

AB包不能重复加载,不然会报错

异步加载AB包

csharp
//>异步加载AB包资源-协程 IEnumerator LoadABResAsync(string abName, string resName) { //1.异步加载AB包 AssetBundleCreateRequest abcr = AssetBundle.LoadFromFileAsync(Application.streamingAssetsPath + "/" + abName); yield return abcr; //2.从AB包中异步加载资源 AssetBundleRequest abr = abcr.assetBundle.LoadAssetAsync(resName, typeof(GameObject)); yield return abr; //3.获取异步加载的资源 GameObject obj = abr.asset as GameObject; yield return obj; }

卸载AB包

csharp
//>卸载AB包中的资源,true同时卸载场景中已经从该AB包中加载过的实例,false只卸载AB包自己 ab.Unload(false); AssetBundle.UnloadAllAssetBundles(false);//卸载当前所有

AB包的依赖

加载一个AB包之前先要判断它是否有相关的依赖,先加载依赖包,然后再加载当前AB包

  1. 利用主包来获取依赖信息
csharp
AssetBundle abMain = AssetBundle.LoadFromFile(Application.streamingAssetsPath + "/" + "iOS");
  1. 加载主包中的mainfest文件
csharp
AssetBundleManifest abMainfest = abMain.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
  1. 从主包中的mainfest文件中获取该AB包的Dependencies,它是一个数组,里面包含了所有依赖包的名字
csharp
string[] dependencies = abMainfest.GetAllDependencies("model");
  1. 加载所有的依赖包
csharp
//临时存储依赖包,方便后续能够及时卸载,实际项目中我们应该用一个容器存储已经加载的AB包 List<AssetBundle> dependencyABs = new List<AssetBundle>(dependencies.Length); for (int i = 0; i < dependencies.Length; i++) { AssetBundleCreateRequest dependencyRequest = AssetBundle.LoadFromFileAsync(Application.streamingAssetsPath + "/" + dependencies[i]); yield return dependencyRequest; // 等待异步加载完成 dependencyABs.Add(dependencyRequest.assetBundle); }

引用计数

添加引用计数目的是方便知晓某个AB包的引用次数,使用资源时引用计数+1,释放资源时引用计数-1,便于及时卸载,Addressables中包含了这个功能,我们使用AB包也可以在代码中自己添加这个逻辑

归纳总结——AssetBundelManager

该管理器已包含了上述所描述的知识点

csharp
using System.Collections; using System.Collections.Generic; using OpenCover.Framework.Model; using UnityEngine; using UnityEngine.Events; public class ABMgr : SingletonAutoMono<ABMgr> { //主包 private AssetBundle mainAB = null; //主包依赖获取配置文件 private AssetBundleManifest manifest = null; //选择存储 AB包的容器 //AB包不能够重复加载 否则会报错 //使用引用计数管理AB包对象 private Dictionary<string, ABInfo> abDic = new Dictionary<string, ABInfo>(); private class ABInfo { public AssetBundle assetBundle; //引用计数 public int linkCount; } /// <summary> /// 获取AB包加载路径 /// </summary> private string PathUrl { get { return Application.streamingAssetsPath + "/"; } } /// <summary> /// 主包名 根据平台不同 报名不同 /// </summary> private string MainName { get { #if UNITY_IOS return "IOS"; #elif UNITY_ANDROID return "Android"; #else return "PC"; #endif } } /// <summary> /// 加载主包 和 配置文件 /// 因为加载所有包是 都得判断 通过它才能得到依赖信息 /// 所以写一个方法 /// </summary> private void LoadMainAB() { if (mainAB == null) { mainAB = AssetBundle.LoadFromFile(PathUrl + MainName); manifest = mainAB.LoadAsset<AssetBundleManifest>("AssetBundleManifest"); } } /// <summary> /// 加载指定包的依赖包 /// </summary> /// <param name="abName"></param> private void LoadDependencies(string abName) { //加载主包 LoadMainAB(); //获取依赖包 string[] strs = manifest.GetAllDependencies(abName); for (int i = 0; i < strs.Length; i++) { if (!abDic.ContainsKey(strs[i])) { AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + strs[i]); abDic.Add(strs[i], new ABInfo { assetBundle = ab, linkCount = 1 }); } else { abDic[strs[i]].linkCount++; } } } /// <summary> /// 泛型资源同步加载 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="abName"></param> /// <param name="resName"></param> /// <returns></returns> public T LoadRes<T>(string abName, string resName) where T : Object { //加载依赖包 LoadDependencies(abName); //加载目标包 if (!abDic.ContainsKey(abName)) { AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName); abDic.Add(abName, new ABInfo { assetBundle = ab, linkCount = 1 }); } else { abDic[abName].linkCount++; } //得到加载出来的资源 T obj = abDic[abName].assetBundle.LoadAsset<T>(resName); //如果是GameObject 因为GameObject 100%都是需要实例化的 //所以我们直接实例化 if (obj is GameObject) return Instantiate(obj); else return obj; } /// <summary> /// Type同步加载指定资源 /// </summary> /// <param name="abName"></param> /// <param name="resName"></param> /// <param name="type"></param> /// <returns></returns> public Object LoadRes(string abName, string resName, System.Type type) { //加载依赖包 LoadDependencies(abName); //加载目标包 if (!abDic.ContainsKey(abName)) { AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName); abDic.Add(abName, new ABInfo { assetBundle = ab, linkCount = 1 }); } else { abDic[abName].linkCount++; } //得到加载出来的资源 Object obj = abDic[abName].assetBundle.LoadAsset(resName, type); //如果是GameObject 因为GameObject 100%都是需要实例化的 //所以我们直接实例化 if (obj is GameObject) return Instantiate(obj); else return obj; } /// <summary> /// 名字 同步加载指定资源 /// </summary> /// <param name="abName"></param> /// <param name="resName"></param> /// <returns></returns> public Object LoadRes(string abName, string resName) { //加载依赖包 LoadDependencies(abName); //加载目标包 if (!abDic.ContainsKey(abName)) { AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName); abDic.Add(abName, new ABInfo { assetBundle = ab, linkCount = 1 }); } else { abDic[abName].linkCount++; } //得到加载出来的资源 Object obj = abDic[abName].assetBundle.LoadAsset(resName); //如果是GameObject 因为GameObject 100%都是需要实例化的 //所以我们直接实例化 if (obj is GameObject) return Instantiate(obj); else return obj; } /// <summary> /// 泛型异步加载资源 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="abName"></param> /// <param name="resName"></param> /// <param name="callBack"></param> public void LoadResAsync<T>(string abName, string resName, UnityAction<T> callBack) where T : Object { StartCoroutine(ReallyLoadResAsync<T>(abName, resName, callBack)); } //协程异步加载 private IEnumerator ReallyLoadResAsync<T>(string abName, string resName, UnityAction<T> callBack) where T : Object { //加载依赖包 LoadDependencies(abName); //加载目标包 if (!abDic.ContainsKey(abName)) { AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName); abDic.Add(abName, new ABInfo { assetBundle = ab, linkCount = 1 }); } else { abDic[abName].linkCount++; } //异步加载包中资源 AssetBundleRequest abq = abDic[abName].assetBundle.LoadAssetAsync<T>(resName); yield return abq; if (abq.asset is GameObject) callBack(Instantiate(abq.asset) as T); else callBack(abq.asset as T); } /// <summary> /// Type异步加载资源 /// </summary> /// <param name="abName"></param> /// <param name="resName"></param> /// <param name="type"></param> /// <param name="callBack"></param> public void LoadResAsync(string abName, string resName, System.Type type, UnityAction<Object> callBack) { StartCoroutine(ReallyLoadResAsync(abName, resName, type, callBack)); } private IEnumerator ReallyLoadResAsync(string abName, string resName, System.Type type, UnityAction<Object> callBack) { //加载依赖包 LoadDependencies(abName); //加载目标包 if (!abDic.ContainsKey(abName)) { AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName); abDic.Add(abName, new ABInfo { assetBundle = ab, linkCount = 1 }); } else { abDic[abName].linkCount++; } //异步加载包中资源 AssetBundleRequest abq = abDic[abName].assetBundle.LoadAssetAsync(resName, type); yield return abq; if (abq.asset is GameObject) callBack(Instantiate(abq.asset)); else callBack(abq.asset); } /// <summary> /// 名字 异步加载 指定资源 /// </summary> /// <param name="abName"></param> /// <param name="resName"></param> /// <param name="callBack"></param> public void LoadResAsync(string abName, string resName, UnityAction<Object> callBack) { StartCoroutine(ReallyLoadResAsync(abName, resName, callBack)); } private IEnumerator ReallyLoadResAsync(string abName, string resName, UnityAction<Object> callBack) { //加载依赖包 LoadDependencies(abName); //加载目标包 if (!abDic.ContainsKey(abName)) { AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName); abDic.Add(abName, new ABInfo { assetBundle = ab, linkCount = 1 }); } else { abDic[abName].linkCount++; } //异步加载包中资源 AssetBundleRequest abq = abDic[abName].assetBundle.LoadAssetAsync(resName); yield return abq; if (abq.asset is GameObject) callBack(Instantiate(abq.asset)); else callBack(abq.asset); } //卸载AB包的方法 (引用计数管理) public void UnLoadAB(string name) { if (abDic.ContainsKey(name)) { abDic[name].linkCount--; if (abDic[name].linkCount <= 0) { abDic[name].assetBundle.Unload(false); abDic.Remove(name); } } } //强制卸载指定AB包 (忽略引用计数) public void ForceUnLoadAB(string name) { if (abDic.ContainsKey(name)) { abDic[name].assetBundle.Unload(false); abDic.Remove(name); } } //获取AB包的引用计数 public int GetABReferenceCount(string name) { if (abDic.ContainsKey(name)) { return abDic[name].linkCount; } return 0; } //清空AB包的方法 public void ClearAB() { AssetBundle.UnloadAllAssetBundles(false); abDic.Clear(); //卸载主包 mainAB = null; manifest = null; } }

本文作者:xuxuxuJS

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!