当前位置:   article > 正文

[游戏开发][Unity]Assetbundle打包篇(5)使用Manifest二次构建资源索引_unity manifest

unity manifest

目录

打包与资源加载框架目录

正文

正文开始前,先把打包代码放过来,请注意,前面的代码已省略,自己去对比前面的文章。本篇文章从第一次执行打包代码开始。

  1. public void PostAssetBuild()
  2. {
  3.     //前面的代码省略,和上一篇文章一致
  4. Log($"开始构建......");
  5. BuildAssetBundleOptions opt = MakeBuildOptions();
  6. AssetBundleManifest buildManifest = BuildPipeline.BuildAssetBundles(OutputPath, buildInfoList.ToArray(), opt, BuildTarget);
  7. if (buildManifest == null)
  8. throw new Exception("[BuildPatch] 构建过程中发生错误!");
  9.     //本篇的代码从这开始==============================================
  10. // 清单列表
  11. string[] allAssetBundles = buildManifest.GetAllAssetBundles();
  12. Log($"资产清单里总共有{allAssetBundles.Length}个资产");
  13. //create res manifest
  14. var resManifest = CreateResManifest(buildMap, buildManifest);
  15. var manifestAssetInfo = new AssetInfo(AssetDatabase.GetAssetPath(resManifest));
  16. var label = "Assets/Manifest";
  17. manifestAssetInfo.ReadableLabel = label;
  18. manifestAssetInfo.AssetBundleVariant = PatchDefine.AssetBundleDefaultVariant;
  19. manifestAssetInfo.AssetBundleLabel = HashUtility.BytesMD5(Encoding.UTF8.GetBytes(label));
  20. var manifestBundleName = $"{manifestAssetInfo.AssetBundleLabel}.{manifestAssetInfo.AssetBundleVariant}".ToLower();
  21. _labelToAssets.Add(manifestBundleName, new List<AssetInfo>() { manifestAssetInfo });
  22. //build ResManifest bundle
  23. buildInfoList.Clear();
  24. buildInfoList.Add(new AssetBundleBuild()
  25. {
  26. assetBundleName = manifestAssetInfo.AssetBundleLabel,
  27. assetBundleVariant = manifestAssetInfo.AssetBundleVariant,
  28. assetNames = new[] { manifestAssetInfo.AssetPath }
  29. });
  30. var resbuildManifest = BuildPipeline.BuildAssetBundles(OutputPath, buildInfoList.ToArray(), opt, BuildTarget);
  31.     //加密代码省略,后面文章讲解
  32. }

第一次调用BuildPipeline.BuildAssetBundles打包API后(详见代码第七行),会返回AssetBundleManifest的引用,

【疑问】:BuildPipeline.BuildAssetBundles打包API已经帮我们创建好了AB包之间的依赖关系引用了,为何还要创建AB包的引用关系?

【解答】:BuildPipeline.BuildAssetBundles打包API执行完生成的UnityManifest.manifest文件记录了所有AB包信息以及依赖关系,但是!企业级项目打包是要考虑增量打包的,因此我们想要知道每个AB是哪个版本打出的,需要一个标记,比如记录该AB包是从SVN 某某某阶段打出来的。因此打包接口生成的UnityManifest.manifest文件是个半成品。


下面开始正式介绍对UnityManifest.manifest文件的二次加工

string[] allAssetBundles = buildManifest.GetAllAssetBundles();拿到allAssetBundles再使用CreateResManifest方法创建一个Unity的Asset文件,把UnityManifest.manifest内为数不多的数据都序列化到该asset文件内。asset的序列化脚本是ResManifes,如下图

UnityManifest.manifest文件的二次加工代码如下:

  1. //assetList在前面的打包代码里有
  2. //buildManifest第一次打包API返回的文件
  3. private ResManifest CreateResManifest(List<AssetInfo> assetList , AssetBundleManifest buildManifest)
  4. {
  5. string[] bundles = buildManifest.GetAllAssetBundles();
  6. var bundleToId = new Dictionary<string, int>();
  7. for (int i = 0; i < bundles.Length; i++)
  8. {
  9. bundleToId[bundles[i]] = i;
  10. }
  11. var bundleList = new List<BundleInfo>();
  12. for (int i = 0; i < bundles.Length; i++)
  13. {
  14. var bundle = bundles[i];
  15. var deps = buildManifest.GetAllDependencies(bundle);
  16. var hash = buildManifest.GetAssetBundleHash(bundle).ToString();
  17. var encryptMethod = ResolveEncryptRule(bundle);
  18. bundleList.Add(new BundleInfo()
  19. {
  20. Name = bundle,
  21. Deps = Array.ConvertAll(deps, _ => bundleToId[_]),
  22. Hash = hash,
  23. EncryptMethod = encryptMethod
  24. });
  25. }
  26. var assetRefs = new List<AssetRef>();
  27. var dirs = new List<string>();
  28. foreach (var assetInfo in assetList)
  29. {
  30. if (!assetInfo.IsCollectAsset) continue;
  31. var dir = Path.GetDirectoryName(assetInfo.AssetPath).Replace("\\", "/");
  32. CollectionSettingData.ApplyReplaceRules(ref dir);
  33. var foundIdx = dirs.FindIndex(_ => _.Equals(dir));
  34. if (foundIdx == -1)
  35. {
  36. dirs.Add(dir);
  37. foundIdx = dirs.Count - 1;
  38. }
  39. var nameStr = $"{assetInfo.AssetBundleLabel}.{assetInfo.AssetBundleVariant}".ToLower();
  40. assetRefs.Add(new AssetRef()
  41. {
  42. Name = Path.GetFileNameWithoutExtension(assetInfo.AssetPath),
  43. BundleId = bundleToId[$"{assetInfo.AssetBundleLabel}.{assetInfo.AssetBundleVariant}".ToLower()],
  44. DirIdx = foundIdx
  45. });
  46. }
  47. var resManifest = GetResManifest();
  48. resManifest.Dirs = dirs.ToArray();
  49. resManifest.Bundles = bundleList.ToArray();
  50. resManifest.AssetRefs = assetRefs.ToArray();
  51. EditorUtility.SetDirty(resManifest);
  52. AssetDatabase.SaveAssets();
  53. AssetDatabase.Refresh();
  54. return resManifest;
  55. }

下面是序列化数据的代码:

  1. /// <summary>
  2. /// design based on Google.Android.AppBundle AssetPackDeliveryMode
  3. /// </summary>
  4. [Serializable]
  5. public enum EAssetDeliveryMode
  6. {
  7. // ===> AssetPackDeliveryMode.InstallTime
  8. Main = 1,
  9. // ====> AssetPackDeliveryMode.FastFollow
  10. FastFollow = 2,
  11. // ====> AssetPackDeliveryMode.OnDemand
  12. OnDemand = 3
  13. }
  14. /// <summary>
  15. /// AssetBundle打包位置
  16. /// </summary>
  17. [Serializable]
  18. public enum EBundlePos
  19. {
  20. /// <summary>
  21. /// 普通
  22. /// </summary>
  23. normal,
  24. /// <summary>
  25. /// 在安装包内
  26. /// </summary>
  27. buildin,
  28. /// <summary>
  29. /// 游戏内下载
  30. /// </summary>
  31. ingame,
  32. }
  33. [Serializable]
  34. public enum EEncryptMethod
  35. {
  36. None = 0,
  37. Quick, //padding header
  38. Simple,
  39. X, //xor
  40. QuickX //partial xor
  41. }
  42. [Serializable]
  43. [ReadOnly]
  44. public struct AssetRef
  45. {
  46. [ReadOnly, EnableGUI]
  47. public string Name;
  48. [ReadOnly, EnableGUI]
  49. public int BundleId;
  50. [ReadOnly, EnableGUI]
  51. public int DirIdx;
  52. }
  53. [Serializable]
  54. public enum ELoadMode
  55. {
  56. None,
  57. LoadFromStreaming,
  58. LoadFromCache,
  59. LoadFromRemote,
  60. }
  61. [Serializable]
  62. public struct BundleInfo
  63. {
  64. [ReadOnly, EnableGUI]
  65. public string Name;
  66. [ReadOnly, EnableGUI]
  67. [ListDrawerSettings(Expanded=false)]
  68. public int[] Deps;
  69. [ReadOnly]
  70. public string Hash;
  71. [ReadOnly]
  72. public EEncryptMethod EncryptMethod;
  73. // public ELoadMode LoadMode;
  74. }
  75. public class ResManifest : ScriptableObject
  76. {
  77. [ReadOnly, EnableGUI]
  78. public string[] Dirs = new string[0];
  79. [ListDrawerSettings(IsReadOnly = true)]
  80. public AssetRef[] AssetRefs = new AssetRef[0];
  81. [ListDrawerSettings(IsReadOnly = true)]
  82. public BundleInfo[] Bundles = new BundleInfo[0];
  83. }
  84. }

看图就可知,CreateResManifest方法就是创建了一套属于我们自己的,资源与AB包索引关系。

ResManifes序列化(代码在下面)文件存储了3类数据,

  1. 所有资源文件夹List

  1. 资源所在的AB包List编号、资源所在文件夹List编号

  1. AB包的Name、依赖包名字、版本号MD5,使用加密类型。


【疑问】:为何要序列化这个asset文件?

回答问题之前,先提出一个问题:资源加载肯定是给开发人员用的,开发人员要如何找到想要的资源在哪个ab包里?

【解答】:项目启动的时候,我们要使用这个asset文件去创建所有资源的一个引用信息,项目启动后是要加载这个asset,加载代码如下。

  1. protected virtual ResManifest LoadResManifest()
  2. {
  3. string label = "Assets/Manifest";
  4. var manifestBundleName = $"{HashUtility.BytesMD5(Encoding.UTF8.GetBytes(label))}.unity3d";
  5. string loadPath = GetAssetBundleLoadPath(manifestBundleName);
  6. var offset = AssetSystem.DecryptServices.GetDecryptOffset(manifestBundleName);
  7. var usingFileSystem = GetLocation(loadPath) == AssetLocation.App
  8. ? FileSystemManagerBase.Instance.MainVFS
  9. : FileSystemManagerBase.Instance.GetSandboxFileSystem(PatchDefine.MainPackKey);
  10. if (usingFileSystem != null)
  11. {
  12. offset += usingFileSystem.GetBundleContentOffset(manifestBundleName);
  13. }
  14. AssetBundle bundle = AssetBundle.LoadFromFile(loadPath, 0, offset);
  15. if (bundle == null)
  16. throw new Exception("Cannot load ResManifest bundle");
  17. var manifest = bundle.LoadAsset<ResManifest>("Assets/Manifest.asset");
  18. if (manifest == null)
  19. throw new Exception("Cannot load Assets/Manifest.asset asset");
  20. for (var i = 0; i < manifest.Dirs.Length; i++)
  21. {
  22. var dir = manifest.Dirs[i];
  23. _dirToIds[dir] = i;
  24. }
  25. for (var i = 0; i < manifest.Bundles.Length; i++)
  26. {
  27. var info = manifest.Bundles[i];
  28. _bundleMap[info.Name] = i;
  29. }
  30. foreach (var assetRef in manifest.AssetRefs)
  31. {
  32. var path = StringFormat.Format("{0}/{1}", manifest.Dirs[assetRef.DirIdx], assetRef.Name);
  33. // MotionLog.Log(ELogLevel.Log, $"path is {path}");
  34. if (!_assetToBundleMap.TryGetValue(assetRef.DirIdx, out var assetNameToBundleId))
  35. {
  36. assetNameToBundleId = new Dictionary<string, int>();
  37. _assetToBundleMap.Add(assetRef.DirIdx, assetNameToBundleId);
  38. }
  39. assetNameToBundleId.Add(assetRef.Name, assetRef.BundleId);
  40. }
  41. bundle.Unload(false);
  42. return manifest;
  43. }

看上面代码就知道,这个asset文件也是被打进了bundle里,并且单独一个ab包。再看一下本篇文章的标题:《使用Manifest二次构建资源索引》,那么,这个asset所在的bundle就是本篇文章的核心!!!

讲述一下在项目中开发人员是如何加载资源的,首先,开发人员会调用一个Loader去加载资源,如果是使用AB包加载模式(本地资源加载不讨论),那么一定会传入一个资源路径,和加载成功回调

  1. Loader.Load("Assets/Works/Resource/Sprite/UIBG/bg_lihui",callbackFunction)
  2. //成功后回调
  3. void callbackFunction(资源文件)
  4. {
  5.     //使用资源文件
  6. }

我们知道,项目启动时会加载这个资源索引文件,所以框架当然知道所有资源路径和它引用的AB包名称,因此加载资源时会自然而然的找到对应的AB包,同时资源索引文件还记录了AB包的互相依赖关系,加载目标AB包时,递归加载所有依赖包就好啦。

项目里如何使用这个二次构建的资源索引文件上面已经讲清楚了,下面开始讲如何在项目启动时热更下载所有AB包。


CreatePatchManifestFile方法是创建AB包下载清单,请注意,创建新清单前会先加载老清单,并且对比AB包生成的MD5有没有发生变化,如果没变化,则继续沿用老清单的版本号,举个例子:假设UI_Login预设是在版本1生成的,这次打包时版本2,由于UI_Login在本次打包中对比发现MD5没变化,则UI_Login所在的AB包版本依然写1,其他变化的、以及新添加的资源版本号写2。

  1. /// <summary>
  2. /// 1. 创建补丁清单文件到输出目录
  3. /// params: isInit 创建的是否是包内的补丁清单
  4. /// useAAB 创建的是否是aab包使用的补丁清单
  5. /// </summary>
  6. private void CreatePatchManifestFile(string[] allAssetBundles, bool isInit = false, bool useAAB = false)
  7. {
  8. // 加载旧文件
  9. PatchManifest patchManifest = LoadPatchManifestFile(isInit);
  10. // 删除旧文件
  11. string filePath = OutputPath + $"/{PatchDefine.PatchManifestFileName}";
  12. if (isInit)
  13. filePath = OutputPath + $"/{PatchDefine.InitManifestFileName}";
  14. if (File.Exists(filePath))
  15. File.Delete(filePath);
  16. // 创建新文件
  17. Log($"创建补丁清单文件:{filePath}");
  18. var sb = new StringBuilder();
  19. using (FileStream fs = File.OpenWrite(filePath))
  20. {
  21. using (var bw = new BinaryWriter(fs))
  22. {
  23. // 写入强更版本信息
  24. //bw.Write(GameVersion.Version);
  25. //sb.AppendLine(GameVersion.Version.ToString());
  26. int ver = BuildVersion;
  27. // 写入版本信息
  28. // if (isReview)
  29. // {
  30. // ver = ver * 10;
  31. // }
  32. bw.Write(ver);
  33. sb.AppendLine(ver.ToString());
  34. // 写入所有AssetBundle文件的信息
  35. var fileCount = allAssetBundles.Length;
  36. bw.Write(fileCount);
  37. for (var i = 0; i < fileCount; i++)
  38. {
  39. var assetName = allAssetBundles[i];
  40. string path = $"{OutputPath}/{assetName}";
  41. string md5 = HashUtility.FileMD5(path);
  42. long sizeKB = EditorTools.GetFileSize(path) / 1024;
  43. int version = BuildVersion;
  44. EBundlePos tag = EBundlePos.buildin;
  45. string readableLabel = "undefined";
  46. if (_labelToAssets.TryGetValue(assetName, out var list))
  47. {
  48. readableLabel = list[0].ReadableLabel;
  49. if (useAAB)
  50. tag = list[0].bundlePos;
  51. }
  52. // 注意:如果文件没有变化使用旧版本号
  53. PatchElement element;
  54. if (patchManifest.Elements.TryGetValue(assetName, out element))
  55. {
  56. if (element.MD5 == md5)
  57. version = element.Version;
  58. }
  59. var curEle = new PatchElement(assetName, md5, version, sizeKB, tag.ToString(), isInit);
  60. curEle.Serialize(bw);
  61. if (isInit)
  62. sb.AppendLine($"{assetName}={readableLabel}={md5}={sizeKB}={version}={tag.ToString()}");
  63. else
  64. sb.AppendLine($"{assetName}={readableLabel}={md5}={sizeKB}={version}");
  65. }
  66. }
  67. string txtName = "PatchManifest.txt";
  68. if (isInit)
  69. txtName = "InitManifest.txt";
  70. File.WriteAllText(OutputPath + "/" + txtName, sb.ToString());
  71. Debug.Log($"{OutputPath}/{txtName} OK");
  72. }
  73. }

生成的AB包清单长下面这个样子。

第一行是SVN版本号

第二行是AB包数量

从第三行开始是资源包信息,以=号分割开有效数据,分别是

MD5.unity3d = 资源路径 = 资源路径的HashId = 包体KB大小 = SVN版本号 = 启动热更模式

最终把这个InitManifest.txt写成bytes,传到服务器就可以对比数据包了

本系列文章加载篇我会正式的讲解AB包的加载,本文只是简单介绍一下。

第一步:

当客户端启动后,进入下载清单状态机, Http先下载InitManifest.txt或者InitManifest.bytes文件,并解析AB包清单。

下面是解析AB包清单的代码。

  1. public class PatchElement
  2. {
  3. /// <summary>
  4. /// 文件名称
  5. /// </summary>
  6. public string Name { private set; get; }
  7. /// <summary>
  8. /// 文件MD5
  9. /// </summary>
  10. public string MD5 { private set; get; }
  11. /// <summary>
  12. /// 文件版本
  13. /// </summary>
  14. public int Version { private set; get; }
  15. /// <summary>
  16. /// 文件大小
  17. /// </summary>
  18. public long SizeKB { private set; get; }
  19. /// <summary>
  20. /// 构建类型
  21. /// buildin 在安装包中
  22. /// ingame 游戏中下载
  23. /// </summary>
  24. public string Tag { private set; get; }
  25. /// <summary>
  26. /// 是否是安装包内的Patch
  27. /// </summary>
  28. public bool IsInit { private set; get; }
  29. /// <summary>
  30. /// 下载文件的保存路径
  31. /// </summary>
  32. public string SavePath;
  33. /// <summary>
  34. /// 每次更新都会先下载到Sandbox_Temp目录,防止下到一半重启导致逻辑不一致报错
  35. /// temp目录下的文件在重新进入更新流程时先校验md5看是否要跳过下载
  36. /// </summary>
  37. public bool SkipDownload { get; set; }
  38. public PatchElement(string name, string md5, int version, long sizeKB, string tag, bool isInit = false)
  39. {
  40. Name = name;
  41. MD5 = md5;
  42. Version = version;
  43. SizeKB = sizeKB;
  44. Tag = tag;
  45. IsInit = isInit;
  46. SkipDownload = false;
  47. }
  48. public void Serialize(BinaryWriter bw)
  49. {
  50. bw.Write(Name);
  51. bw.Write(MD5);
  52. bw.Write(SizeKB);
  53. bw.Write(Version);
  54. if (IsInit)
  55. bw.Write(Tag);
  56. }
  57. public static PatchElement Deserialize(BinaryReader br, bool isInit = false)
  58. {
  59. var name = br.ReadString();
  60. var md5 = br.ReadString();
  61. var sizeKb = br.ReadInt64();
  62. var version = br.ReadInt32();
  63. var tag = EBundlePos.buildin.ToString();
  64. if (isInit)
  65. tag = br.ReadString();
  66. return new PatchElement(name, md5, version, sizeKb, tag, isInit);
  67. }
  68. }

第二步:

请注意,中断续传也是个很重要的功能,AB包清单记录了每个AB包的大小,当项目启动时,优先遍历Temp文件夹内的AB包,如果大小和清单内的不一致,则开启Http的下载功能,Http是支持断点续传的,Http的Header里定义要下载的数据段。如果你觉得这样不保险,可以直接删掉这个AB包重新下载。

AB包清单解析完后,切换到下载清单状态机,开启清单的每一个文件下载,请注意,热更下载文件时,我们可以先创建一个Temp文件夹,未全部下载成功前的AB包都在这里,全部下载成功后,再全部剪切到PersistentData文件夹内,PersistentData文件夹是Unity内置的沙盒目录,Unity有读写权限。

全部下载完成后,完成PersistentData文件夹剪切工作。

第三步:

全部资源已就绪,启动正式业务框架。

疑问:为何在热更完后再启动正式业务框架?

目前大多数商业项目都是Tolua、Xlua框架,很多框架层代码都是写到Lua中去的,Lua代码属于AB包的一部分,因此只能等热更完后启动。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/82170
推荐阅读
相关标签
  

闽ICP备14008679号