227 lines
9.4 KiB
C#
227 lines
9.4 KiB
C#
using System;
|
||
using System.IO;
|
||
using System.Reflection;
|
||
using HybridCLR;
|
||
using UnityEngine;
|
||
using UnityEngine.AddressableAssets;
|
||
using UnityEngine.Networking;
|
||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||
using UnityEngine.SceneManagement;
|
||
|
||
/// <summary>
|
||
/// AOT 启动器(必须位于 AOT 程序集,且不能编译期引用任何热更类型)。
|
||
///
|
||
/// 启动流程:
|
||
/// BootScene(纯 AOT 空场景)
|
||
/// → Launcher 在 AfterAssembliesLoaded 阶段加载热更 dll(Game.Hotfix / Game.LegacyScripts)
|
||
/// → 等 BootScene 加载完(AfterSceneLoad)后,通过 Addressables 加载真正的业务场景 UIScene
|
||
///
|
||
/// 为什么不能直接把 UIScene 放进 Build Settings 当启动场景:
|
||
/// HybridCLR 官方明确限制——热更脚本不能挂在"随主包的资源"上(含 Build Settings 里的场景)。
|
||
/// 因为构建时 IFilterBuildAssemblies 会把热更 dll 从序列化列表里剔除,导致场景里的热更脚本
|
||
/// 字段写不进包体,运行时反序列化就报 "The referenced script on this Behaviour is missing!"。
|
||
/// 解决办法:热更脚本所在场景必须走 AssetBundle/Addressables 加载。
|
||
///
|
||
/// 为什么加载 UIScene 要放在 AfterSceneLoad 而不是更早:
|
||
/// Addressables.LoadSceneAsync(..., Single) 会替换当前场景,如果在 Unity 自身加载第一个场景
|
||
/// (BeforeSceneLoad)期间发起,会和引擎的首次场景加载相互冲突。等第一个场景加载完再替换才安全。
|
||
/// </summary>
|
||
public static class Launcher
|
||
{
|
||
/// <summary>热更 dll 目录(相对 StreamingAssets)</summary>
|
||
private const string HotfixDir = "HybridCLR/Hotfix";
|
||
|
||
/// <summary>AOT 补充元数据 dll 目录(相对 StreamingAssets)</summary>
|
||
private const string AOTMetaDir = "HybridCLR/AOT";
|
||
|
||
/// <summary>需要热更加载的程序集(与 ProjectSettings/HybridCLRSettings.asset 的 hotUpdateAssemblies 一致)</summary>
|
||
private static readonly string[] HotfixAssemblies = { "Game.Hotfix", "Game.LegacyScripts" };
|
||
|
||
/// <summary>通过 Addressables 加载的主场景 key(需在 Addressables 里把 UIScene 标记为可寻址)</summary>
|
||
private const string MainSceneKey = "UIScene";
|
||
|
||
/// <summary>是否已加载完成(多个阶段只加载一次)</summary>
|
||
private static bool _loaded;
|
||
|
||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
|
||
private static void BootAfterAssembliesLoaded() { EnsureLoaded(RuntimeInitializeLoadType.AfterAssembliesLoaded); }
|
||
|
||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSplashScreen)]
|
||
private static void BootBeforeSplashScreen() { EnsureLoaded(RuntimeInitializeLoadType.BeforeSplashScreen); }
|
||
|
||
/// <summary>第一个场景(BootScene)加载完成后,再切换到业务主场景 UIScene</summary>
|
||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||
private static void BootAfterSceneLoad()
|
||
{
|
||
// 兜底:若前面的阶段都没能加载(例如被平台时序打断),这里再试一次
|
||
if (!_loaded)
|
||
EnsureLoaded(RuntimeInitializeLoadType.AfterSceneLoad);
|
||
|
||
if (!_loaded)
|
||
return;
|
||
|
||
LoadMainSceneViaAddressables();
|
||
}
|
||
|
||
private static void EnsureLoaded(RuntimeInitializeLoadType phase)
|
||
{
|
||
if (_loaded)
|
||
{
|
||
Debug.Log($"[Launcher] 已在更早阶段加载完成,{phase} 阶段跳过");
|
||
return;
|
||
}
|
||
|
||
int frame = Time.frameCount;
|
||
Debug.Log($"[Launcher] 开始加载:阶段={phase},frame={frame}");
|
||
|
||
bool aotOk = LoadAOTMetadata();
|
||
bool hotOk = LoadHotfixAssemblies();
|
||
_loaded = aotOk && hotOk;
|
||
|
||
Debug.Log($"[Launcher] 加载完成:阶段={phase},AOT元数据={(aotOk ? "全部OK" : "有缺失")}," +
|
||
$"热更dll={(hotOk ? "全部OK" : "有失败")},Game.Hotfix 类型总数={HotfixTypeCount()}," +
|
||
$"GameManager 可解析={HotfixTypeResolvable("GameManager")}");
|
||
|
||
if (!_loaded)
|
||
{
|
||
Debug.LogError("[Launcher] 热更程序集未全部加载成功,游戏将无法正常运行。请检查 StreamingAssets/HybridCLR 下的 dll 是否已拷贝。");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化 Addressables 并加载主场景 UIScene。
|
||
/// UIScene 必须事先在 Addressables Groups 窗口里标记为可寻址(key 为 "UIScene")。
|
||
/// </summary>
|
||
private static void LoadMainSceneViaAddressables()
|
||
{
|
||
Debug.Log("[Launcher] 开始初始化 Addressables ...");
|
||
var initHandle = Addressables.InitializeAsync();
|
||
initHandle.Completed += handle =>
|
||
{
|
||
if (handle.Status != AsyncOperationStatus.Succeeded)
|
||
{
|
||
Debug.LogError($"[Launcher] Addressables 初始化失败: {handle.OperationException}");
|
||
return;
|
||
}
|
||
|
||
Debug.Log("[Launcher] Addressables 初始化完成,开始加载 UIScene ...");
|
||
var sceneHandle = Addressables.LoadSceneAsync(MainSceneKey, LoadSceneMode.Single);
|
||
sceneHandle.Completed += sceneOp =>
|
||
{
|
||
if (sceneOp.Status == AsyncOperationStatus.Succeeded)
|
||
{
|
||
Debug.Log($"[Launcher] UIScene 加载成功: {sceneOp.Result.Scene.name}");
|
||
}
|
||
else
|
||
{
|
||
Debug.LogError($"[Launcher] UIScene 加载失败: {sceneOp.OperationException}。" +
|
||
$"请确认已在 Addressables Groups 里把 UIScene 标记为可寻址,且 address key 为 '{MainSceneKey}'。");
|
||
}
|
||
};
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 加载 AOT 补充元数据。清单来自 HybridCLR 生成的 AOTGenericReferences.PatchedAOTAssemblyList
|
||
/// (它由 HybridCLR/Generate/AOTGenericReferences.cs 产出,与本脚本同属 AOT 程序集)。
|
||
/// </summary>
|
||
private static bool LoadAOTMetadata()
|
||
{
|
||
bool allOk = true;
|
||
foreach (var dllName in AOTGenericReferences.PatchedAOTAssemblyList)
|
||
{
|
||
// dllName 形如 "DOTween.dll",磁盘文件为 "DOTween.dll.bytes"
|
||
byte[] bytes = ReadStreamingAsset(Path.Combine(AOTMetaDir, dllName + ".bytes"));
|
||
if (bytes == null)
|
||
{
|
||
Debug.LogError($"[Launcher] 缺少 AOT 补充元数据: {dllName}");
|
||
allOk = false;
|
||
continue;
|
||
}
|
||
|
||
LoadImageErrorCode err = RuntimeApi.LoadMetadataForAOTAssembly(bytes, HomologousImageMode.SuperSet);
|
||
if (err != LoadImageErrorCode.OK)
|
||
{
|
||
Debug.LogError($"[Launcher] AOT 元数据加载失败: {dllName} → {err}");
|
||
allOk = false;
|
||
}
|
||
}
|
||
return allOk;
|
||
}
|
||
|
||
/// <summary>加载热更程序集(Assembly.Load 之后,场景里的热更 MonoBehaviour 才能被实例化)</summary>
|
||
private static bool LoadHotfixAssemblies()
|
||
{
|
||
bool allOk = true;
|
||
foreach (var asmName in HotfixAssemblies)
|
||
{
|
||
byte[] bytes = ReadStreamingAsset(Path.Combine(HotfixDir, asmName + ".dll.bytes"));
|
||
if (bytes == null)
|
||
{
|
||
Debug.LogError($"[Launcher] 缺少热更 dll: {asmName}");
|
||
allOk = false;
|
||
continue;
|
||
}
|
||
|
||
try
|
||
{
|
||
Assembly asm = Assembly.Load(bytes);
|
||
Debug.Log($"[Launcher] 热更程序集已加载: {asm.FullName}");
|
||
}
|
||
catch (System.Exception e)
|
||
{
|
||
Debug.LogError($"[Launcher] 热更 dll 加载异常: {asmName} → {e}");
|
||
allOk = false;
|
||
}
|
||
}
|
||
return allOk;
|
||
}
|
||
|
||
/// <summary>诊断用:Game.Hotfix 里已加载的类型总数(-1 表示该程序集没找到)</summary>
|
||
private static int HotfixTypeCount()
|
||
{
|
||
var asm = FindAssembly("Game.Hotfix");
|
||
if (asm == null) return -1;
|
||
try { return asm.GetTypes().Length; } catch { return -2; }
|
||
}
|
||
|
||
/// <summary>诊断用:能否在 Game.Hotfix 里解析到指定类型</summary>
|
||
private static bool HotfixTypeResolvable(string typeName)
|
||
{
|
||
var asm = FindAssembly("Game.Hotfix");
|
||
return asm != null && asm.GetType(typeName) != null;
|
||
}
|
||
|
||
private static Assembly FindAssembly(string name)
|
||
{
|
||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||
if (asm.GetName().Name == name) return asm;
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 读取 StreamingAssets 下的文件(兼容 Android 的 apk 内路径)。
|
||
/// 找不到或读取失败返回 null。
|
||
/// </summary>
|
||
private static byte[] ReadStreamingAsset(string relativePath)
|
||
{
|
||
string fullPath = Path.Combine(Application.streamingAssetsPath, relativePath);
|
||
|
||
// Android / WebGL:StreamingAssets 在 apk/包体内,必须走 UnityWebRequest
|
||
#if UNITY_ANDROID && !UNITY_EDITOR
|
||
using (var req = UnityWebRequest.Get(fullPath))
|
||
{
|
||
req.SendWebRequest();
|
||
while (!req.isDone) { } // 启动阶段短暂阻塞,可接受
|
||
if (req.result != UnityWebRequest.Result.Success)
|
||
return null;
|
||
return req.downloadHandler.data;
|
||
}
|
||
#else
|
||
if (!File.Exists(fullPath))
|
||
return null;
|
||
return File.ReadAllBytes(fullPath);
|
||
#endif
|
||
}
|
||
}
|