目次
文章
本文を始める前に、ここにパッケージ化コードを記載します。前のコードは省略されていることに注意してください。前回の記事と比較してください。この記事は、パッケージ化コードを初めて実行するところから始まります。
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 を初めて呼び出した後 (詳細についてはコードの 7 行目を参照)、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 アセット ファイルを作成し、UnityManifest.manifest 内のいくつかのデータをアセット ファイルにシリアル化します。以下に示すように、アセットのシリアル化スクリプトは 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 種類のデータが保存されます。
すべてのリソースフォルダーのリスト
AB パッケージ リソースが配置されているリスト番号、リソースが配置されているフォルダーのリスト番号
AB パッケージの名前、依存パッケージの名前、バージョン番号 MD5、および暗号化の種類。
[質問]: このアセット ファイルをシリアル化する必要があるのはなぜですか?
質問に答える前に質問させてください: リソースの読み込みは間違いなく開発者向けですが、開発者は目的のリソースが配置されている ab パッケージをどのように見つけますか?
[回答]: プロジェクトの開始時に、このアセット ファイルを使用してすべてのリソースの参照情報を作成する必要があります。プロジェクトの開始後、このアセットをロードする必要があります読み込みコードは以下の通りです。
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;
}
上記のコードを見ると、このアセット ファイルもバンドルに含まれており、別個の ab パッケージであることがわかります。この記事のタイトル「マニフェストを使用してリソース インデックスを 2 回構築する」をもう一度見てください。このアセットが配置されているバンドルがこの記事の核心です。 ! !
開発者がプロジェクトにリソースをロードする方法について話しましょう。まず、開発者はリソースをロードするためにローダーを呼び出します。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 のバージョン番号です
2行目はABパッケージの数です。
3 行目からはリソース パッケージ情報であり、有効なデータは = 記号で区切られています。
MD5.unity3d = リソース パス = リソース パスの HashId = パッケージ本体の KB サイズ = SVN バージョン番号 = ホット アップデート モードの開始
最後に、この InitManifest.txt をバイトに書き込み、サーバーに送信してデータ パケットを比較します。
この一連の記事の読み込み部分では、AB パッケージの読み込みについて正式に説明しますが、この記事では簡単な紹介に留めます。
最初の一歩:
クライアントが起動すると、ダウンロード リスト ステート マシンに入り、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);
}
}
ステップ2:
ダウンロードの中断と再開も非常に重要な機能であることに注意してください。AB パッケージ リストには、各 AB パッケージのサイズが記録されます。プロジェクトが開始されると、Temp フォルダ内の AB パッケージが最初に走査されます。サイズが一致しない場合は、 HTTP ダウンロード機能、HTTP はブレークポイント再開をサポートしており、ダウンロードされるデータ セグメントは Http ヘッダーで定義されます。これが安全でないと思われる場合は、AB パッケージを直接削除して、再度ダウンロードすることができます。
AB パッケージ リストが解析されたら、ダウンロード リスト ステート マシンに切り替えて、リスト内の各ファイルのダウンロードを開始します。ホット アップデート用のファイルをダウンロードするときは、最初に一時フォルダーを作成できることに注意してください。正常にダウンロードされる前のすべての AB パッケージすべてのダウンロードが成功したら、それらをすべて PersistentData フォルダーに切り取ります。PersistentData フォルダーは Unity の組み込みサンドボックス ディレクトリであり、Unity には読み取りおよび書き込み権限があります。
すべてのダウンロードが完了したら、PersistentData フォルダーのカットを完了します。
3番目のステップ:
すべてのリソースが整い、正式なビジネス フレームワークが開始されます。
質問: ホット アップデートが完了した後に正式なビジネス フレームワークを開始するのはなぜですか?
現在、ほとんどの商用プロジェクトは Tolua および Xlua フレームワークに基づいており、多くのフレームワーク レイヤー コードは Lua で記述されています。Lua コードは AB パッケージの一部であるため、ホット アップデートが完了した後にのみ開始できます。