目錄
打包與資源加載框架目錄
正文
正文開始前,先把打包代碼放過來,請注意,前面的代碼已省略,自己去對比前面的文章。本篇文章從第一次執(zhí)行打包代碼開始。
public void PostAssetBuild()
{
? ? //前面的代碼省略,和上一篇文章一致
Log($"開始構建......");
BuildAssetBundleOptions opt = MakeBuildOptions();
AssetBundleManifest buildManifest = BuildPipeline.BuildAssetBundles(OutputPath, buildInfoList.ToArray(), opt, BuildTarget);
if (buildManifest == null)
throw new Exception("[BuildPatch] 構建過程中發(fā)生錯誤!");
? ? //本篇的代碼從這開始==============================================
// 清單列表
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已經幫我們創(chuàng)建好了AB包之間的依賴關系引用了,為何還要創(chuàng)建AB包的引用關系?
【解答】:BuildPipeline.BuildAssetBundles打包API執(zhí)行完生成的UnityManifest.manifest文件記錄了所有AB包信息以及依賴關系,但是!企業(yè)級項目打包是要考慮增量打包的,因此我們想要知道每個AB是哪個版本打出的,需要一個標記,比如記錄該AB包是從SVN 某某某階段打出來的。因此打包接口生成的UnityManifest.manifest文件是個半成品。
下面開始正式介紹對UnityManifest.manifest文件的二次加工
string[] allAssetBundles = buildManifest.GetAllAssetBundles();拿到allAssetBundles再使用CreateResManifest方法創(chuàng)建一個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方法就是創(chuàng)建了一套屬于我們自己的,資源與AB包索引關系。
ResManifes序列化(代碼在下面)文件存儲了3類數據,
所有資源文件夾List
資源所在的AB包List編號、資源所在文件夾List編號
AB包的Name、依賴包名字、版本號MD5,使用加密類型。
【疑問】:為何要序列化這個asset文件?
回答問題之前,先提出一個問題:資源加載肯定是給開發(fā)人員用的,開發(fā)人員要如何找到想要的資源在哪個ab包里?
【解答】:項目啟動的時候,我們要使用這個asset文件去創(chuàng)建所有資源的一個引用信息,項目啟動后是要加載這個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就是本篇文章的核心?。?!
講述一下在項目中開發(fā)人員是如何加載資源的,首先,開發(fā)人員會調用一個Loader去加載資源,如果是使用AB包加載模式(本地資源加載不討論),那么一定會傳入一個資源路徑,和加載成功回調
Loader.Load("Assets/Works/Resource/Sprite/UIBG/bg_lihui",callbackFunction)
//成功后回調
void callbackFunction(資源文件)
{
? ? //使用資源文件
}
我們知道,項目啟動時會加載這個資源索引文件,所以框架當然知道所有資源路徑和它引用的AB包名稱,因此加載資源時會自然而然的找到對應的AB包,同時資源索引文件還記錄了AB包的互相依賴關系,加載目標AB包時,遞歸加載所有依賴包就好啦。
項目里如何使用這個二次構建的資源索引文件上面已經講清楚了,下面開始講如何在項目啟動時熱更下載所有AB包。
CreatePatchManifestFile方法是創(chuàng)建AB包下載清單,請注意,創(chuàng)建新清單前會先加載老清單,并且對比AB包生成的MD5有沒有發(fā)生變化,如果沒變化,則繼續(xù)沿用老清單的版本號,舉個例子:假設UI_Login預設是在版本1生成的,這次打包時版本2,由于UI_Login在本次打包中對比發(fā)現MD5沒變化,則UI_Login所在的AB包版本依然寫1,其他變化的、以及新添加的資源版本號寫2。
/// <summary>
/// 1. 創(chuàng)建補丁清單文件到輸出目錄
/// params: isInit 創(chuàng)建的是否是包內的補丁清單
/// useAAB 創(chuàng)建的是否是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);
// 創(chuàng)建新文件
Log($"創(chuàng)建補丁清單文件:{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,傳到服務器就可以對比數據包了
本系列文章加載篇我會正式的講解AB包的加載,本文只是簡單介紹一下。
第一步:
當客戶端啟動后,進入下載清單狀態(tài)機, 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);
}
}
第二步:
請注意,中斷續(xù)傳也是個很重要的功能,AB包清單記錄了每個AB包的大小,當項目啟動時,優(yōu)先遍歷Temp文件夾內的AB包,如果大小和清單內的不一致,則開啟Http的下載功能,Http是支持斷點續(xù)傳的,Http的Header里定義要下載的數據段。如果你覺得這樣不保險,可以直接刪掉這個AB包重新下載。
AB包清單解析完后,切換到下載清單狀態(tài)機,開啟清單的每一個文件下載,請注意,熱更下載文件時,我們可以先創(chuàng)建一個Temp文件夾,未全部下載成功前的AB包都在這里,全部下載成功后,再全部剪切到PersistentData文件夾內,PersistentData文件夾是Unity內置的沙盒目錄,Unity有讀寫權限。
全部下載完成后,完成PersistentData文件夾剪切工作。
第三步:
全部資源已就緒,啟動正式業(yè)務框架。
疑問:為何在熱更完后再啟動正式業(yè)務框架?文章來源:http://www.zghlxwxcb.cn/news/detail-772636.html
目前大多數商業(yè)項目都是Tolua、Xlua框架,很多框架層代碼都是寫到Lua中去的,Lua代碼屬于AB包的一部分,因此只能等熱更完后啟動。文章來源地址http://www.zghlxwxcb.cn/news/detail-772636.html
到了這里,關于[游戲開發(fā)][Unity]Assetbundle打包篇(5)使用Manifest二次構建資源索引的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!