赞
踩
正文开始前,先把打包代码放过来,请注意,前面的代码已省略,自己去对比前面的文章。本篇文章从第一次执行打包代码开始。
- public void PostAssetBuild()
- {
- //前面的代码省略,和上一篇文章一致
-
- Log($"开始构建......");
- BuildAssetBundleOptions opt = MakeBuildOptions();
- AssetBundleManifest buildManifest = BuildPipeline.BuildAssetBundles(OutputPath, buildInfoList.ToArray(), opt, BuildTarget);
- if (buildManifest == null)
- throw new Exception("[BuildPatch] 构建过程中发生错误!");
-
- //本篇的代码从这开始==============================================
- // 清单列表
- string[] allAssetBundles = buildManifest.GetAllAssetBundles();
- Log($"资产清单里总共有{allAssetBundles.Length}个资产");
-
- //create res manifest
- var resManifest = CreateResManifest(buildMap, buildManifest);
- var manifestAssetInfo = new AssetInfo(AssetDatabase.GetAssetPath(resManifest));
- var label = "Assets/Manifest";
- manifestAssetInfo.ReadableLabel = label;
- manifestAssetInfo.AssetBundleVariant = PatchDefine.AssetBundleDefaultVariant;
- manifestAssetInfo.AssetBundleLabel = HashUtility.BytesMD5(Encoding.UTF8.GetBytes(label));
- var manifestBundleName = $"{manifestAssetInfo.AssetBundleLabel}.{manifestAssetInfo.AssetBundleVariant}".ToLower();
- _labelToAssets.Add(manifestBundleName, new List<AssetInfo>() { manifestAssetInfo });
-
- //build ResManifest bundle
- buildInfoList.Clear();
- buildInfoList.Add(new AssetBundleBuild()
- {
- assetBundleName = manifestAssetInfo.AssetBundleLabel,
- assetBundleVariant = manifestAssetInfo.AssetBundleVariant,
- assetNames = new[] { manifestAssetInfo.AssetPath }
- });
- var resbuildManifest = BuildPipeline.BuildAssetBundles(OutputPath, buildInfoList.ToArray(), opt, BuildTarget);
- //加密代码省略,后面文章讲解
- }
第一次调用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文件的二次加工代码如下:
- //assetList在前面的打包代码里有
- //buildManifest第一次打包API返回的文件
- private ResManifest CreateResManifest(List<AssetInfo> assetList , AssetBundleManifest buildManifest)
- {
- string[] bundles = buildManifest.GetAllAssetBundles();
- var bundleToId = new Dictionary<string, int>();
- for (int i = 0; i < bundles.Length; i++)
- {
- bundleToId[bundles[i]] = i;
- }
-
- var bundleList = new List<BundleInfo>();
- for (int i = 0; i < bundles.Length; i++)
- {
- var bundle = bundles[i];
- var deps = buildManifest.GetAllDependencies(bundle);
- var hash = buildManifest.GetAssetBundleHash(bundle).ToString();
-
- var encryptMethod = ResolveEncryptRule(bundle);
- bundleList.Add(new BundleInfo()
- {
- Name = bundle,
- Deps = Array.ConvertAll(deps, _ => bundleToId[_]),
- Hash = hash,
- EncryptMethod = encryptMethod
- });
- }
-
- var assetRefs = new List<AssetRef>();
- var dirs = new List<string>();
- foreach (var assetInfo in assetList)
- {
- if (!assetInfo.IsCollectAsset) continue;
- var dir = Path.GetDirectoryName(assetInfo.AssetPath).Replace("\\", "/");
- CollectionSettingData.ApplyReplaceRules(ref dir);
- var foundIdx = dirs.FindIndex(_ => _.Equals(dir));
- if (foundIdx == -1)
- {
- dirs.Add(dir);
- foundIdx = dirs.Count - 1;
- }
-
- var nameStr = $"{assetInfo.AssetBundleLabel}.{assetInfo.AssetBundleVariant}".ToLower();
- assetRefs.Add(new AssetRef()
- {
- Name = Path.GetFileNameWithoutExtension(assetInfo.AssetPath),
- BundleId = bundleToId[$"{assetInfo.AssetBundleLabel}.{assetInfo.AssetBundleVariant}".ToLower()],
- DirIdx = foundIdx
- });
- }
-
- var resManifest = GetResManifest();
- resManifest.Dirs = dirs.ToArray();
- resManifest.Bundles = bundleList.ToArray();
- resManifest.AssetRefs = assetRefs.ToArray();
- EditorUtility.SetDirty(resManifest);
- AssetDatabase.SaveAssets();
- AssetDatabase.Refresh();
-
- return resManifest;
- }
下面是序列化数据的代码:
- /// <summary>
- /// design based on Google.Android.AppBundle AssetPackDeliveryMode
- /// </summary>
- [Serializable]
- public enum EAssetDeliveryMode
- {
- // ===> AssetPackDeliveryMode.InstallTime
- Main = 1,
- // ====> AssetPackDeliveryMode.FastFollow
- FastFollow = 2,
- // ====> AssetPackDeliveryMode.OnDemand
- OnDemand = 3
- }
-
- /// <summary>
- /// AssetBundle打包位置
- /// </summary>
- [Serializable]
- public enum EBundlePos
- {
- /// <summary>
- /// 普通
- /// </summary>
- normal,
-
- /// <summary>
- /// 在安装包内
- /// </summary>
- buildin,
-
- /// <summary>
- /// 游戏内下载
- /// </summary>
- ingame,
- }
-
- [Serializable]
- public enum EEncryptMethod
- {
- None = 0,
- Quick, //padding header
- Simple,
- X, //xor
- QuickX //partial xor
- }
-
- [Serializable]
- [ReadOnly]
- public struct AssetRef
- {
- [ReadOnly, EnableGUI]
- public string Name;
-
- [ReadOnly, EnableGUI]
- public int BundleId;
-
- [ReadOnly, EnableGUI]
- public int DirIdx;
- }
-
- [Serializable]
- public enum ELoadMode
- {
- None,
- LoadFromStreaming,
- LoadFromCache,
- LoadFromRemote,
- }
-
-
- [Serializable]
- public struct BundleInfo
- {
- [ReadOnly, EnableGUI]
- public string Name;
-
- [ReadOnly, EnableGUI]
- [ListDrawerSettings(Expanded=false)]
- public int[] Deps;
-
- [ReadOnly]
- public string Hash;
-
- [ReadOnly]
- public EEncryptMethod EncryptMethod;
-
- // public ELoadMode LoadMode;
- }
-
- public class ResManifest : ScriptableObject
- {
- [ReadOnly, EnableGUI]
- public string[] Dirs = new string[0];
- [ListDrawerSettings(IsReadOnly = true)]
- public AssetRef[] AssetRefs = new AssetRef[0];
- [ListDrawerSettings(IsReadOnly = true)]
- public BundleInfo[] Bundles = new BundleInfo[0];
- }
- }
看图就可知,CreateResManifest方法就是创建了一套属于我们自己的,资源与AB包索引关系。
ResManifes序列化(代码在下面)文件存储了3类数据,
所有资源文件夹List
资源所在的AB包List编号、资源所在文件夹List编号
AB包的Name、依赖包名字、版本号MD5,使用加密类型。
【疑问】:为何要序列化这个asset文件?
回答问题之前,先提出一个问题:资源加载肯定是给开发人员用的,开发人员要如何找到想要的资源在哪个ab包里?
【解答】:项目启动的时候,我们要使用这个asset文件去创建所有资源的一个引用信息,项目启动后是要加载这个asset,加载代码如下。
- protected virtual ResManifest LoadResManifest()
- {
- string label = "Assets/Manifest";
- var manifestBundleName = $"{HashUtility.BytesMD5(Encoding.UTF8.GetBytes(label))}.unity3d";
- string loadPath = GetAssetBundleLoadPath(manifestBundleName);
- var offset = AssetSystem.DecryptServices.GetDecryptOffset(manifestBundleName);
- var usingFileSystem = GetLocation(loadPath) == AssetLocation.App
- ? FileSystemManagerBase.Instance.MainVFS
- : FileSystemManagerBase.Instance.GetSandboxFileSystem(PatchDefine.MainPackKey);
- if (usingFileSystem != null)
- {
- offset += usingFileSystem.GetBundleContentOffset(manifestBundleName);
- }
-
- AssetBundle bundle = AssetBundle.LoadFromFile(loadPath, 0, offset);
- if (bundle == null)
- throw new Exception("Cannot load ResManifest bundle");
-
- var manifest = bundle.LoadAsset<ResManifest>("Assets/Manifest.asset");
- if (manifest == null)
- throw new Exception("Cannot load Assets/Manifest.asset asset");
-
- for (var i = 0; i < manifest.Dirs.Length; i++)
- {
- var dir = manifest.Dirs[i];
- _dirToIds[dir] = i;
- }
-
- for (var i = 0; i < manifest.Bundles.Length; i++)
- {
- var info = manifest.Bundles[i];
- _bundleMap[info.Name] = i;
- }
-
- foreach (var assetRef in manifest.AssetRefs)
- {
- var path = StringFormat.Format("{0}/{1}", manifest.Dirs[assetRef.DirIdx], assetRef.Name);
- // MotionLog.Log(ELogLevel.Log, $"path is {path}");
- if (!_assetToBundleMap.TryGetValue(assetRef.DirIdx, out var assetNameToBundleId))
- {
- assetNameToBundleId = new Dictionary<string, int>();
- _assetToBundleMap.Add(assetRef.DirIdx, assetNameToBundleId);
- }
- assetNameToBundleId.Add(assetRef.Name, assetRef.BundleId);
- }
-
- bundle.Unload(false);
- return manifest;
- }
看上面代码就知道,这个asset文件也是被打进了bundle里,并且单独一个ab包。再看一下本篇文章的标题:《使用Manifest二次构建资源索引》,那么,这个asset所在的bundle就是本篇文章的核心!!!
讲述一下在项目中开发人员是如何加载资源的,首先,开发人员会调用一个Loader去加载资源,如果是使用AB包加载模式(本地资源加载不讨论),那么一定会传入一个资源路径,和加载成功回调
- Loader.Load("Assets/Works/Resource/Sprite/UIBG/bg_lihui",callbackFunction)
-
- //成功后回调
- void callbackFunction(资源文件)
- {
- //使用资源文件
- }
我们知道,项目启动时会加载这个资源索引文件,所以框架当然知道所有资源路径和它引用的AB包名称,因此加载资源时会自然而然的找到对应的AB包,同时资源索引文件还记录了AB包的互相依赖关系,加载目标AB包时,递归加载所有依赖包就好啦。
项目里如何使用这个二次构建的资源索引文件上面已经讲清楚了,下面开始讲如何在项目启动时热更下载所有AB包。
CreatePatchManifestFile方法是创建AB包下载清单,请注意,创建新清单前会先加载老清单,并且对比AB包生成的MD5有没有发生变化,如果没变化,则继续沿用老清单的版本号,举个例子:假设UI_Login预设是在版本1生成的,这次打包时版本2,由于UI_Login在本次打包中对比发现MD5没变化,则UI_Login所在的AB包版本依然写1,其他变化的、以及新添加的资源版本号写2。
-
- /// <summary>
- /// 1. 创建补丁清单文件到输出目录
- /// params: isInit 创建的是否是包内的补丁清单
- /// useAAB 创建的是否是aab包使用的补丁清单
- /// </summary>
- private void CreatePatchManifestFile(string[] allAssetBundles, bool isInit = false, bool useAAB = false)
- {
- // 加载旧文件
- PatchManifest patchManifest = LoadPatchManifestFile(isInit);
-
- // 删除旧文件
- string filePath = OutputPath + $"/{PatchDefine.PatchManifestFileName}";
- if (isInit)
- filePath = OutputPath + $"/{PatchDefine.InitManifestFileName}";
- if (File.Exists(filePath))
- File.Delete(filePath);
-
- // 创建新文件
- Log($"创建补丁清单文件:{filePath}");
- var sb = new StringBuilder();
- using (FileStream fs = File.OpenWrite(filePath))
- {
- using (var bw = new BinaryWriter(fs))
- {
- // 写入强更版本信息
- //bw.Write(GameVersion.Version);
- //sb.AppendLine(GameVersion.Version.ToString());
- int ver = BuildVersion;
- // 写入版本信息
- // if (isReview)
- // {
- // ver = ver * 10;
- // }
- bw.Write(ver);
- sb.AppendLine(ver.ToString());
-
- // 写入所有AssetBundle文件的信息
- var fileCount = allAssetBundles.Length;
- bw.Write(fileCount);
- for (var i = 0; i < fileCount; i++)
- {
- var assetName = allAssetBundles[i];
- string path = $"{OutputPath}/{assetName}";
- string md5 = HashUtility.FileMD5(path);
- long sizeKB = EditorTools.GetFileSize(path) / 1024;
- int version = BuildVersion;
- EBundlePos tag = EBundlePos.buildin;
- string readableLabel = "undefined";
- if (_labelToAssets.TryGetValue(assetName, out var list))
- {
- readableLabel = list[0].ReadableLabel;
- if (useAAB)
- tag = list[0].bundlePos;
- }
-
- // 注意:如果文件没有变化使用旧版本号
- PatchElement element;
- if (patchManifest.Elements.TryGetValue(assetName, out element))
- {
- if (element.MD5 == md5)
- version = element.Version;
- }
- var curEle = new PatchElement(assetName, md5, version, sizeKB, tag.ToString(), isInit);
- curEle.Serialize(bw);
-
-
- if (isInit)
- sb.AppendLine($"{assetName}={readableLabel}={md5}={sizeKB}={version}={tag.ToString()}");
- else
- sb.AppendLine($"{assetName}={readableLabel}={md5}={sizeKB}={version}");
- }
- }
-
- string txtName = "PatchManifest.txt";
- if (isInit)
- txtName = "InitManifest.txt";
- File.WriteAllText(OutputPath + "/" + txtName, sb.ToString());
- Debug.Log($"{OutputPath}/{txtName} OK");
- }
- }
生成的AB包清单长下面这个样子。
第一行是SVN版本号
第二行是AB包数量
从第三行开始是资源包信息,以=号分割开有效数据,分别是
MD5.unity3d = 资源路径 = 资源路径的HashId = 包体KB大小 = SVN版本号 = 启动热更模式
最终把这个InitManifest.txt写成bytes,传到服务器就可以对比数据包了
当客户端启动后,进入下载清单状态机, Http先下载InitManifest.txt或者InitManifest.bytes文件,并解析AB包清单。
下面是解析AB包清单的代码。
-
- public class PatchElement
- {
- /// <summary>
- /// 文件名称
- /// </summary>
- public string Name { private set; get; }
-
- /// <summary>
- /// 文件MD5
- /// </summary>
- public string MD5 { private set; get; }
-
- /// <summary>
- /// 文件版本
- /// </summary>
- public int Version { private set; get; }
-
- /// <summary>
- /// 文件大小
- /// </summary>
- public long SizeKB { private set; get; }
-
- /// <summary>
- /// 构建类型
- /// buildin 在安装包中
- /// ingame 游戏中下载
- /// </summary>
- public string Tag { private set; get; }
-
- /// <summary>
- /// 是否是安装包内的Patch
- /// </summary>
- public bool IsInit { private set; get; }
-
- /// <summary>
- /// 下载文件的保存路径
- /// </summary>
- public string SavePath;
-
- /// <summary>
- /// 每次更新都会先下载到Sandbox_Temp目录,防止下到一半重启导致逻辑不一致报错
- /// temp目录下的文件在重新进入更新流程时先校验md5看是否要跳过下载
- /// </summary>
- public bool SkipDownload { get; set; }
-
-
- public PatchElement(string name, string md5, int version, long sizeKB, string tag, bool isInit = false)
- {
- Name = name;
- MD5 = md5;
- Version = version;
- SizeKB = sizeKB;
- Tag = tag;
- IsInit = isInit;
- SkipDownload = false;
- }
-
- public void Serialize(BinaryWriter bw)
- {
- bw.Write(Name);
- bw.Write(MD5);
- bw.Write(SizeKB);
- bw.Write(Version);
- if (IsInit)
- bw.Write(Tag);
- }
-
- public static PatchElement Deserialize(BinaryReader br, bool isInit = false)
- {
- var name = br.ReadString();
- var md5 = br.ReadString();
- var sizeKb = br.ReadInt64();
- var version = br.ReadInt32();
- var tag = EBundlePos.buildin.ToString();
- if (isInit)
- tag = br.ReadString();
- return new PatchElement(name, md5, version, sizeKb, tag, isInit);
- }
- }
请注意,中断续传也是个很重要的功能,AB包清单记录了每个AB包的大小,当项目启动时,优先遍历Temp文件夹内的AB包,如果大小和清单内的不一致,则开启Http的下载功能,Http是支持断点续传的,Http的Header里定义要下载的数据段。如果你觉得这样不保险,可以直接删掉这个AB包重新下载。
AB包清单解析完后,切换到下载清单状态机,开启清单的每一个文件下载,请注意,热更下载文件时,我们可以先创建一个Temp文件夹,未全部下载成功前的AB包都在这里,全部下载成功后,再全部剪切到PersistentData文件夹内,PersistentData文件夹是Unity内置的沙盒目录,Unity有读写权限。
全部下载完成后,完成PersistentData文件夹剪切工作。
全部资源已就绪,启动正式业务框架。
疑问:为何在热更完后再启动正式业务框架?
目前大多数商业项目都是Tolua、Xlua框架,很多框架层代码都是写到Lua中去的,Lua代码属于AB包的一部分,因此只能等热更完后启动。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。