三国卡牌客户端基础资源仓库
yyl
2026-06-12 e42a15cee005412ffe72341b49d51baa4ec44dc3
启动流程加速 计时
5个文件已修改
804 ■■■■ 已修改文件
Assets/Editor/Tool/ClientPackage.cs 250 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Launch/Launch.cs 346 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Launch/Manager/LocalResManager.cs 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Launch/Manager/YooAssetInitializer.cs 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Launch/UI/LaunchWins/LaunchExWin.cs 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/Tool/ClientPackage.cs
@@ -133,93 +133,117 @@
            }
        }
        // ========== 使用 YooAsset 打包资源 ==========
        // 全量打包所有 Package(含 HybridCLR),自动拷贝到 StreamingAssets/yoo/{PackageName}/
        Debug.Log("[ClientPackage] 开始 YooAsset 全量打包所有 Package...");
        if (!YooAssetBuildTool.BuildAllPackagesCore(incremental: false))
        {
            Debug.LogError("[ClientPackage] YooAsset 资源打包失败,中止构建!");
            EditorUtility.DisplayDialog("打包失败", "YooAsset 资源打包失败,请查看 Console 日志。", "确定");
            return;
        }
        Debug.Log("[ClientPackage] YooAsset 资源打包完成。");
        // 打包成功后,将完整 StreamingAssets/yoo 拷贝到 Output Path(裁剪前执行,确保拿到全量资源)
        if (!string.IsNullOrEmpty(_assetBundlePath))
        {
            YooAssetBuildTool.CopyStreamingAssetsToOutputPath(_assetBundlePath);
        }
        // YooAsset StreamingAssets 根目录
        string yooRoot = AssetBundleBuilderHelper.GetStreamingAssetsRoot();
        // ---- NullAsset(小包):不随包携带 StreamingAssets 资源,全部从 CDN 按需下载 ----
        if (smallPackages.Count > 0)
        if (!Directory.Exists(yooRoot))
        {
            YooAssetBuildTool.ClearStreamingAssetsYooDirectory();
            Debug.Log("[ClientPackage] NullAsset: 已清空 StreamingAssets/yoo");
            // 写入随包 Package 列表,NullAsset 为空列表
            YooAssetBuildTool.WriteBuildinPackageList();
            for (int i = 0; i < smallPackages.Count; i++)
            {
#if UNITY_ANDROID
                BuildApk(_sdkPath, _output, smallPackages[i], _buildIndex, _development, _assetBundlePath);
#elif UNITY_IOS
                BuildIpa(_sdkPath, smallPackages[i], _buildIndex, _replace);
#endif
            }
            // 恢复完整 StreamingAssets 供后续使用
            RestoreYooStreamingAssets();
            Debug.LogError($"[ClientPackage] StreamingAssets/yoo 目录不存在: {yooRoot}");
            EditorUtility.DisplayDialog("打包失败", "未找到 StreamingAssets/yoo,请先准备好资源后再执行 BuildApk。", "确定");
            return;
        }
        // ---- HalfAsset(中包):剔除部分 Package ----
        if (halfPackages.Count > 0)
        if (!ValidateLaunchRequiredPackages(yooRoot))
        {
            var removePackages = CreateHalfAssetRemovePackages();
            EditorUtility.DisplayDialog("打包失败", "StreamingAssets/yoo 缺少必需包(Builtin/Dll),请先准备完整资源。", "确定");
            return;
        }
            foreach (var pkgName in removePackages)
        string yooBackupPath = CreateYooStreamingAssetsBackup(yooRoot);
        if (string.IsNullOrEmpty(yooBackupPath))
        {
            EditorUtility.DisplayDialog("打包失败", "备份 StreamingAssets/yoo 失败,请查看 Console 日志。", "确定");
            return;
        }
        Debug.Log("[ClientPackage] 跳过 YooAsset 资源打包,直接使用现有 StreamingAssets/yoo 构建。");
        // 将完整 StreamingAssets/yoo 拷贝到 Output Path(裁剪前执行,确保拿到全量资源)
        if (!string.IsNullOrEmpty(_assetBundlePath))
        {
            if (!YooAssetBuildTool.CopyStreamingAssetsToOutputPath(_assetBundlePath))
            {
                string pkgDir = Path.Combine(yooRoot, pkgName);
                if (Directory.Exists(pkgDir))
                Debug.LogError($"[ClientPackage] 拷贝 StreamingAssets/yoo 到输出路径失败: {_assetBundlePath}");
                return;
            }
        }
        try
        {
            // ---- NullAsset(小包):不随包携带 StreamingAssets 资源,全部从 CDN 按需下载 ----
            if (smallPackages.Count > 0)
            {
                YooAssetBuildTool.ClearStreamingAssetsYooDirectory();
                Debug.Log("[ClientPackage] NullAsset: 已清空 StreamingAssets/yoo");
                // 写入随包 Package 列表,NullAsset 为空列表
                YooAssetBuildTool.WriteBuildinPackageList();
                for (int i = 0; i < smallPackages.Count; i++)
                {
                    Directory.Delete(pkgDir, true);
                    Debug.Log($"[ClientPackage] HalfAsset: 已剔除 Package '{pkgName}'");
#if UNITY_ANDROID
                    BuildApk(_sdkPath, _output, smallPackages[i], _buildIndex, _development, _assetBundlePath);
#elif UNITY_IOS
                    BuildIpa(_sdkPath, smallPackages[i], _buildIndex, _replace);
#endif
                }
                // 恢复完整 StreamingAssets 供后续使用
                if (!RestoreYooStreamingAssetsFromBackup(yooRoot, yooBackupPath))
                    return;
            }
            // ---- HalfAsset(中包):剔除部分 Package ----
            if (halfPackages.Count > 0)
            {
                var removePackages = CreateHalfAssetRemovePackages();
                foreach (var pkgName in removePackages)
                {
                    string pkgDir = Path.Combine(yooRoot, pkgName);
                    if (Directory.Exists(pkgDir))
                    {
                        Directory.Delete(pkgDir, true);
                        Debug.Log($"[ClientPackage] HalfAsset: 已剔除 Package '{pkgName}'");
                    }
                }
                // 写入剔除后的 Buildin 包列表
                YooAssetBuildTool.WriteBuildinPackageList();
                for (int i = 0; i < halfPackages.Count; i++)
                {
#if UNITY_ANDROID
                    BuildApk(_sdkPath, _output, halfPackages[i], _buildIndex, _development, _assetBundlePath);
#elif UNITY_IOS
                    BuildIpa(_sdkPath, halfPackages[i], _buildIndex, _replace);
#endif
                }
                // 恢复完整 StreamingAssets 供后续使用
                if (!RestoreYooStreamingAssetsFromBackup(yooRoot, yooBackupPath))
                    return;
            }
            // ---- FullAsset(大包):保留所有 Package ----
            if (bigPackages.Count > 0)
            {
                // 全量 StreamingAssets,写入完整 Buildin 包列表
                YooAssetBuildTool.WriteBuildinPackageList();
                for (int i = 0; i < bigPackages.Count; i++)
                {
#if UNITY_ANDROID
                    BuildApk(_sdkPath, _output, bigPackages[i], _buildIndex, _development, _assetBundlePath);
#elif UNITY_IOS
                    BuildIpa(_sdkPath, bigPackages[i], _buildIndex, _replace);
#endif
                }
            }
            // 写入剔除后的 Buildin 包列表
            YooAssetBuildTool.WriteBuildinPackageList();
            for (int i = 0; i < halfPackages.Count; i++)
            {
#if UNITY_ANDROID
                BuildApk(_sdkPath, _output, halfPackages[i], _buildIndex, _development, _assetBundlePath);
#elif UNITY_IOS
                BuildIpa(_sdkPath, halfPackages[i], _buildIndex, _replace);
#endif
            }
            // 恢复完整 StreamingAssets 供后续使用
            RestoreYooStreamingAssets();
        }
        // ---- FullAsset(大包):保留所有 Package ----
        if (bigPackages.Count > 0)
        finally
        {
            // 全量 StreamingAssets,写入完整 Buildin 包列表
            YooAssetBuildTool.WriteBuildinPackageList();
            for (int i = 0; i < bigPackages.Count; i++)
            {
#if UNITY_ANDROID
                BuildApk(_sdkPath, _output, bigPackages[i], _buildIndex, _development, _assetBundlePath);
#elif UNITY_IOS
                BuildIpa(_sdkPath, bigPackages[i], _buildIndex, _replace);
#endif
            }
            DeleteDirectoryIfExists(yooBackupPath);
        }
    }
@@ -781,6 +805,84 @@
        YooAssetBuildTool.RestoreBuildOutputToStreamingAssets();
    }
    private static string CreateYooStreamingAssetsBackup(string yooRoot)
    {
        try
        {
            string backupRoot = Path.Combine(Path.GetTempPath(), $"ProjSG_YooBackup_{Guid.NewGuid():N}");
            if (Directory.Exists(backupRoot))
                Directory.Delete(backupRoot, true);
            FileExtersion.DirectoryCopy(yooRoot, backupRoot);
            Debug.Log($"[ClientPackage] 已备份 StreamingAssets/yoo: {backupRoot}");
            return backupRoot;
        }
        catch (Exception ex)
        {
            Debug.LogError($"[ClientPackage] 备份 StreamingAssets/yoo 失败: {ex}");
            return null;
        }
    }
    private static bool RestoreYooStreamingAssetsFromBackup(string yooRoot, string backupRoot)
    {
        if (string.IsNullOrEmpty(backupRoot) || !Directory.Exists(backupRoot))
        {
            Debug.LogError($"[ClientPackage] StreamingAssets/yoo 备份不存在: {backupRoot}");
            return false;
        }
        try
        {
            if (Directory.Exists(yooRoot))
                Directory.Delete(yooRoot, true);
            FileExtersion.DirectoryCopy(backupRoot, yooRoot);
            Debug.Log("[ClientPackage] 已从临时备份恢复 StreamingAssets/yoo");
            return true;
        }
        catch (Exception ex)
        {
            Debug.LogError($"[ClientPackage] 从备份恢复 StreamingAssets/yoo 失败: {ex}");
            return false;
        }
    }
    private static bool ValidateLaunchRequiredPackages(string yooRoot)
    {
        var missing = new List<string>();
        for (int i = 0; i < LaunchRequiredYooPackages.Length; i++)
        {
            string packageDir = Path.Combine(yooRoot, LaunchRequiredYooPackages[i]);
            if (!Directory.Exists(packageDir))
                missing.Add(LaunchRequiredYooPackages[i]);
        }
        if (missing.Count > 0)
        {
            Debug.LogError($"[ClientPackage] StreamingAssets/yoo 缺少必需包: {string.Join(", ", missing)}");
            return false;
        }
        return true;
    }
    private static void DeleteDirectoryIfExists(string path)
    {
        if (string.IsNullOrEmpty(path))
            return;
        try
        {
            if (Directory.Exists(path))
                Directory.Delete(path, true);
        }
        catch (Exception ex)
        {
            Debug.LogError($"[ClientPackage] 删除临时目录失败: {path}, error={ex}");
        }
    }
    private static List<string> CreateHalfAssetRemovePackages()
    {
        var removePackages = new List<string> { "Battle", "Hero", "Audio", "Video", "UIEffect" };
Assets/Launch/Launch.cs
@@ -59,13 +59,20 @@
    private const string PRE_SCENE_RES_PACKAGE_NAME = "PreSceneRes";
    private const string LAUNCH_STATIC_WIN_LOCATION = "Assets/ResourcesOut/PreSceneRes/LaunchStaticWin.prefab";
    private const int PRE_SCENE_RES_VERIFY_MAX_CONCURRENCY = 8;
    // 上次更新成功的 PreSceneRes 版本号(方案 A:自存版本号,下次启动本地激活,不联网)。
    private const string PRE_SCENE_RES_VERSION_KEY = "yoo_ver_PreSceneRes";
    private static void LogTiming(string phase, float startTime)
    {
        Debug.LogError($"[Launch][Timing] {phase} cost={(Time.realtimeSinceStartup - startTime):F3}s");
    }
    private static Launch m_Instance;
    private void Awake()
    {
        float startTime = Time.realtimeSinceStartup;
        if (m_Instance != null)
        {
            Debug.LogError("Launch Instance is not null");
@@ -89,12 +96,16 @@
#endif
        OpenLaunchStaticWin().Forget();
        LogTiming("Awake", startTime);
    }
    private async UniTaskVoid OpenLaunchStaticWin()
    {
        if (isOpeningLaunchStaticWin || launchStaticWin != null)
            return;
        float startTime = Time.realtimeSinceStartup;
        float phaseStart = startTime;
        isOpeningLaunchStaticWin = true;
        try
@@ -111,9 +122,13 @@
                    Debug.LogError($"[Launch] LaunchStaticWin prefab not found: {LAUNCH_STATIC_WIN_LOCATION}");
                    return;
                }
                LogTiming("OpenLaunchStaticWin.Editor.LoadPrefab", phaseStart);
                float editorInstantiateStart = Time.realtimeSinceStartup;
                launchStaticWin = Instantiate(prefab);
                launchStaticWin.name = "LaunchStaticWin";
                launchStaticWin.AddComponent<DontDestroyOnLoad>();
                LogTiming("OpenLaunchStaticWin.Editor.Instantiate", editorInstantiateStart);
                return;
            }
#endif
@@ -125,7 +140,7 @@
                if (YooAssetInitializer.Instance.State == YooAssetInitializer.InitState.InitFailed ||
                    YooAssetInitializer.Instance.State == YooAssetInitializer.InitState.VersionFailed)
                {
                    Debug.LogWarning($"[Launch] Skip LaunchStaticWin because YooAsset state={YooAssetInitializer.Instance.State}");
                    Debug.LogError($"[Launch] Skip LaunchStaticWin because YooAsset state={YooAssetInitializer.Instance.State}");
                    return;
                }
@@ -134,11 +149,13 @@
                await UniTask.Yield();
            }
            LogTiming("OpenLaunchStaticWin.WebGL.WaitReady", phaseStart);
            phaseStart = Time.realtimeSinceStartup;
            var webPackage = YooAssetInitializer.Instance.GetPackage(PRE_SCENE_RES_PACKAGE_NAME);
            if (webPackage == null)
            {
                Debug.LogWarning($"[Launch] Package '{PRE_SCENE_RES_PACKAGE_NAME}' not initialized. Skip LaunchStaticWin.");
                Debug.LogError($"[Launch] Package '{PRE_SCENE_RES_PACKAGE_NAME}' not initialized. Skip LaunchStaticWin.");
                return;
            }
@@ -150,79 +167,133 @@
                return;
            }
            prefab = webHandle.GetAssetObject<GameObject>();
            LogTiming("OpenLaunchStaticWin.WebGL.LoadPrefab", phaseStart);
#else
            // Android/iOS/PC(含 Editor AB 模式):本地秒开,独立初始化 PreSceneRes,不等网络。
            YooAssets.Initialize();
            var package = YooAssets.TryGetPackage(PRE_SCENE_RES_PACKAGE_NAME)
                ?? YooAssets.CreatePackage(PRE_SCENE_RES_PACKAGE_NAME);
            // 快路径:若本地已缓存 savedVer 的 manifest/hash,则无需再触发 buildin 版本读取与复制。
            string savedVer = LocalSave.GetString(PRE_SCENE_RES_VERSION_KEY);
            bool hasSavedManifest = !string.IsNullOrEmpty(savedVer) && IsPreSceneResManifestCached(savedVer);
            UniTask<string> buildinVerTask = default;
            if (!hasSavedManifest)
            {
                // 只有在需要 buildin 回退时才读取,避免常规热更后启动的额外开销。
                buildinVerTask = ReadBuildinPreSceneResVersionAsync().Preserve();
            }
            if (package.InitializeStatus != EOperationStatus.Succeed)
            {
                // 始终用 HostPlayMode(随包 buildin + 缓存)。
                // cdnUrl 在 Awake 阶段通常还没拿到,但 CacheFileSystem 初始化不需要它,
                // 远程地址由 DynamicCdnRemoteServices 在请求版本/下载时实时读取。
                var remoteServices = new DynamicCdnRemoteServices(PRE_SCENE_RES_PACKAGE_NAME);
                var initParams = new HostPlayModeParameters
                {
                    BuildinFileSystemParameters = FileSystemParameters.CreateDefaultBuildinFileSystemParameters(),
                    CacheFileSystemParameters = FileSystemParameters.CreateDefaultCacheFileSystemParameters(remoteServices)
                };
                var initOp = package.InitializeAsync(initParams);
                await initOp.ToUniTask();
                if (initOp.Status != EOperationStatus.Succeed)
                {
                    Debug.LogError($"[Launch] Init PreSceneRes failed: {initOp.Error}");
                if (!await InitializePreSceneResPackageAsync(package, hasSavedManifest))
                    return;
                }
            }
            LogTiming("OpenLaunchStaticWin.Native.InitPackage", phaseStart);
            phaseStart = Time.realtimeSinceStartup;
            // 本地激活 manifest(不联网):取「上次更新保存的版本」与「包内 buildin 版本」中较新的一个。
            // 版本号格式 yyyy-MM-dd-HHmm 字典序可排序;
            // 这样重新打包带入更新的 buildin 资源时,不会被旧的 LocalSave 版本号覆盖。
            string savedVer = LocalSave.GetString(PRE_SCENE_RES_VERSION_KEY);
            string buildinVer = await ReadBuildinPreSceneResVersionAsync();
            string buildinVer = null;
            string localVer;
            if (string.IsNullOrEmpty(savedVer))
                localVer = buildinVer;
            else if (string.IsNullOrEmpty(buildinVer))
            if (hasSavedManifest)
            {
                // 快路径:缓存版本存在时直接激活,避免额外等待读取 buildin 版本。
                localVer = savedVer;
            }
            else
                localVer = string.CompareOrdinal(savedVer, buildinVer) >= 0 ? savedVer : buildinVer;
            {
                buildinVer = await buildinVerTask;
                localVer = buildinVer;
            }
            if (!string.IsNullOrEmpty(savedVer) && !hasSavedManifest)
            {
                Debug.LogError($"[Launch] Saved PreSceneRes manifest missing in cache, ignore savedVer={savedVer}, use buildinVer={buildinVer}.");
            }
            if (string.IsNullOrEmpty(localVer))
            {
                Debug.LogError("[Launch] PreSceneRes local version unavailable.");
                return;
            }
            Debug.Log($"[Launch] PreSceneRes localVer={localVer} (saved={savedVer}, buildin={buildinVer})");
            Debug.LogError($"[Launch] PreSceneRes localVer={localVer} (saved={savedVer}, buildin={buildinVer})");
            LogTiming("OpenLaunchStaticWin.Native.PickVersion", phaseStart);
            phaseStart = Time.realtimeSinceStartup;
            var localManifestOp = package.UpdatePackageManifestAsync(localVer);
            await localManifestOp.ToUniTask();
            if (localManifestOp.Status != EOperationStatus.Succeed)
            // 快路径:当前已激活目标版本时,无需再次加载 manifest。
            if (package.PackageValid)
            {
                // 选中的版本激活失败(如缓存被清且无网络),回退到包内 buildin 版本。
                if (!string.IsNullOrEmpty(buildinVer) && buildinVer != localVer)
                string activeVer = package.GetPackageVersion();
                if (!string.IsNullOrEmpty(activeVer) && activeVer == localVer)
                {
                    Debug.LogWarning($"[Launch] Activate manifest v{localVer} failed, fallback buildin v{buildinVer}: {localManifestOp.Error}");
                    localVer = buildinVer;
                    localManifestOp = package.UpdatePackageManifestAsync(localVer);
                    await localManifestOp.ToUniTask();
                    var fastHandle = package.LoadAssetSync<GameObject>(LAUNCH_STATIC_WIN_LOCATION);
                    if (fastHandle.Status == EOperationStatus.Succeed)
                    {
                        prefab = fastHandle.GetAssetObject<GameObject>();
                    }
                    else
                    {
                        Debug.LogError($"[Launch] Fast-path load LaunchStaticWin failed: {fastHandle.LastError}");
                    }
                    LogTiming("OpenLaunchStaticWin.Native.FastPathLoadPrefab", phaseStart);
                    phaseStart = Time.realtimeSinceStartup;
                }
            }
            if (prefab == null)
            {
                var localManifestOp = package.UpdatePackageManifestAsync(localVer);
                await localManifestOp.ToUniTask();
                if (localManifestOp.Status != EOperationStatus.Succeed)
                {
                    Debug.LogError($"[Launch] Activate PreSceneRes manifest v{localVer} failed: {localManifestOp.Error}");
                    // 缓存快路径可能遇到异常缓存;重建为完整 Host 文件系统后再重试一次 saved 版本。
                    if (hasSavedManifest)
                    {
                        Debug.LogError($"[Launch] Activate saved manifest v{localVer} failed, retry full host init: {localManifestOp.Error}");
                        if (!await ReinitializePreSceneResPackageWithBuildinAsync(package))
                            return;
                        localManifestOp = package.UpdatePackageManifestAsync(localVer);
                        await localManifestOp.ToUniTask();
                    }
                    // 选中的版本激活失败(如缓存被清且无网络),回退到包内 buildin 版本。
                    if (localManifestOp.Status != EOperationStatus.Succeed && string.IsNullOrEmpty(buildinVer))
                    {
                        if (!hasSavedManifest)
                            buildinVer = await buildinVerTask;
                        else
                            buildinVer = await ReadBuildinPreSceneResVersionAsync();
                    }
                    if (localManifestOp.Status != EOperationStatus.Succeed && !string.IsNullOrEmpty(buildinVer) && buildinVer != localVer)
                    {
                        Debug.LogError($"[Launch] Activate manifest v{localVer} failed, fallback buildin v{buildinVer}: {localManifestOp.Error}");
                        localVer = buildinVer;
                        localManifestOp = package.UpdatePackageManifestAsync(localVer);
                        await localManifestOp.ToUniTask();
                    }
                    if (localManifestOp.Status != EOperationStatus.Succeed)
                    {
                        Debug.LogError($"[Launch] Activate PreSceneRes manifest v{localVer} failed: {localManifestOp.Error}");
                        return;
                    }
                }
                LogTiming("OpenLaunchStaticWin.Native.ActivateManifest", phaseStart);
                phaseStart = Time.realtimeSinceStartup;
                var handle = package.LoadAssetSync<GameObject>(LAUNCH_STATIC_WIN_LOCATION);
                if (handle.Status != EOperationStatus.Succeed)
                {
                    Debug.LogError($"[Launch] Load LaunchStaticWin failed: {handle.LastError}");
                    return;
                }
                prefab = handle.GetAssetObject<GameObject>();
                LogTiming("OpenLaunchStaticWin.Native.LoadPrefab", phaseStart);
            }
            var handle = package.LoadAssetSync<GameObject>(LAUNCH_STATIC_WIN_LOCATION);
            if (handle.Status != EOperationStatus.Succeed)
            {
                Debug.LogError($"[Launch] Load LaunchStaticWin failed: {handle.LastError}");
                return;
            }
            prefab = handle.GetAssetObject<GameObject>();
            // 后台更新(不阻塞秒开):等 cdnUrl 就绪后请求最新版本、更新清单、加载一次触发下载落地,
            // 并保存新版本号,供下次启动直接用最新资源。
@@ -236,9 +307,11 @@
                return;
            }
            float instantiateStart = Time.realtimeSinceStartup;
            launchStaticWin = Instantiate(prefab);
            launchStaticWin.name = "LaunchStaticWin";
            launchStaticWin.AddComponent<DontDestroyOnLoad>();
            LogTiming("OpenLaunchStaticWin.Instantiate", instantiateStart);
        }
        catch (Exception ex)
        {
@@ -246,8 +319,87 @@
        }
        finally
        {
            LogTiming("OpenLaunchStaticWin", startTime);
            isOpeningLaunchStaticWin = false;
        }
    }
    private async UniTask<bool> InitializePreSceneResPackageAsync(ResourcePackage package, bool hasSavedManifest)
    {
        // cdnUrl 在 Awake 阶段通常还没拿到,但 CacheFileSystem 初始化不依赖 cdnUrl。
        // 远程地址由 DynamicCdnRemoteServices 在请求版本/下载时实时读取。
        if (hasSavedManifest)
        {
            var cacheOnlyParams = new HostPlayModeParameters
            {
                CacheFileSystemParameters = CreatePreSceneResCacheFileSystemParameters()
            };
            var cacheOnlyOp = package.InitializeAsync(cacheOnlyParams);
            await cacheOnlyOp.ToUniTask();
            if (cacheOnlyOp.Status == EOperationStatus.Succeed)
            {
                Debug.LogError("[Launch] Init PreSceneRes with cache-only fast path succeeded.");
                return true;
            }
            Debug.LogError($"[Launch] Init PreSceneRes cache-only failed, fallback full host init: {cacheOnlyOp.Error}");
        }
        var fullHostParams = new HostPlayModeParameters
        {
            BuildinFileSystemParameters = CreatePreSceneResBuildinFileSystemParameters(copyBuildinManifest: true),
            CacheFileSystemParameters = CreatePreSceneResCacheFileSystemParameters()
        };
        var initOp = package.InitializeAsync(fullHostParams);
        await initOp.ToUniTask();
        if (initOp.Status != EOperationStatus.Succeed)
        {
            Debug.LogError($"[Launch] Init PreSceneRes failed: {initOp.Error}");
            return false;
        }
        return true;
    }
    private async UniTask<bool> ReinitializePreSceneResPackageWithBuildinAsync(ResourcePackage package)
    {
        var destroyOp = package.DestroyAsync();
        await destroyOp.ToUniTask();
        if (destroyOp.Status != EOperationStatus.Succeed)
        {
            Debug.LogError($"[Launch] Destroy PreSceneRes package before reinit failed: {destroyOp.Error}");
            return false;
        }
        return await InitializePreSceneResPackageAsync(package, hasSavedManifest: false);
    }
    private FileSystemParameters CreatePreSceneResBuildinFileSystemParameters(bool copyBuildinManifest)
    {
        var buildinFsParams = FileSystemParameters.CreateDefaultBuildinFileSystemParameters();
        buildinFsParams.AddParameter(FileSystemParametersDefine.COPY_BUILDIN_PACKAGE_MANIFEST, copyBuildinManifest);
        // 启动期优先速度:只做存在性校验。
        buildinFsParams.AddParameter(FileSystemParametersDefine.FILE_VERIFY_LEVEL, EFileVerifyLevel.Low);
        // 控制并发可降低移动端线程调度开销,缩短初始化尾部抖动。
        buildinFsParams.AddParameter(FileSystemParametersDefine.FILE_VERIFY_MAX_CONCURRENCY, PRE_SCENE_RES_VERIFY_MAX_CONCURRENCY);
        // 启动期跳过覆盖安装自动清理,避免首次启动额外磁盘清理开销。
        buildinFsParams.AddParameter(FileSystemParametersDefine.INSTALL_CLEAR_MODE, EOverwriteInstallClearMode.None);
        return buildinFsParams;
    }
    private FileSystemParameters CreatePreSceneResCacheFileSystemParameters()
    {
        var remoteServices = new DynamicCdnRemoteServices(PRE_SCENE_RES_PACKAGE_NAME);
        var cacheFsParams = FileSystemParameters.CreateDefaultCacheFileSystemParameters(remoteServices);
        // 启动期优先速度:只做存在性校验。
        cacheFsParams.AddParameter(FileSystemParametersDefine.FILE_VERIFY_LEVEL, EFileVerifyLevel.Low);
        // 控制并发可降低移动端线程调度开销,缩短初始化尾部抖动。
        cacheFsParams.AddParameter(FileSystemParametersDefine.FILE_VERIFY_MAX_CONCURRENCY, PRE_SCENE_RES_VERIFY_MAX_CONCURRENCY);
        // 启动期跳过覆盖安装自动清理,避免首次启动额外磁盘清理开销。
        cacheFsParams.AddParameter(FileSystemParametersDefine.INSTALL_CLEAR_MODE, EOverwriteInstallClearMode.None);
        return cacheFsParams;
    }
#if !UNITY_WEBGL || UNITY_EDITOR
@@ -257,11 +409,21 @@
    /// </summary>
    private async UniTask<string> ReadBuildinPreSceneResVersionAsync()
    {
        float startTime = Time.realtimeSinceStartup;
        try
        {
            string folder = YooAssetSettingsData.GetDefaultYooFolderName();
            string fileName = YooAssetSettingsData.GetPackageVersionFileName(PRE_SCENE_RES_PACKAGE_NAME);
            string filePath = $"{Application.streamingAssetsPath}/{folder}/{PRE_SCENE_RES_PACKAGE_NAME}/{fileName}";
            string filePath = Path.Combine(Application.streamingAssetsPath, folder, PRE_SCENE_RES_PACKAGE_NAME, fileName);
#if !UNITY_ANDROID || UNITY_EDITOR
            // 非 Android 平台优先直接读本地文件,减少 UnityWebRequest 开销。
            if (File.Exists(filePath))
            {
                return File.ReadAllText(filePath).Trim();
            }
#endif
            // Android 上 streamingAssetsPath 已是 jar:file://... 形式,可直接用于 UnityWebRequest;
            // 其它平台为本地文件路径,需补 file:// 前缀。
            string url;
@@ -278,7 +440,7 @@
                await req.SendWebRequest().ToUniTask();
                if (req.result != UnityEngine.Networking.UnityWebRequest.Result.Success)
                {
                    Debug.LogWarning($"[Launch] Read buildin PreSceneRes version failed: {req.error}");
                    Debug.LogError($"[Launch] Read buildin PreSceneRes version failed: {req.error}");
                    return null;
                }
                return req.downloadHandler.text.Trim();
@@ -289,13 +451,61 @@
            Debug.LogError($"[Launch] ReadBuildinPreSceneResVersionAsync exception: {ex}");
            return null;
        }
        finally
        {
            LogTiming("ReadBuildinPreSceneResVersionAsync", startTime);
        }
    }
    /// <summary>
    /// 检查指定版本的 PreSceneRes manifest/hash 是否已落地到本地缓存。
    /// 仅当本地真实存在时才允许启动阶段激活该版本,避免在 cdnUrl 未就绪时触发远程请求。
    /// </summary>
    private bool IsPreSceneResManifestCached(string packageVersion)
    {
        if (string.IsNullOrEmpty(packageVersion))
            return false;
        try
        {
            string cacheRoot = GetYooDefaultCacheRoot();
            string manifestRoot = Path.Combine(cacheRoot, PRE_SCENE_RES_PACKAGE_NAME, "ManifestFiles");
            string hashPath = Path.Combine(manifestRoot, YooAssetSettingsData.GetPackageHashFileName(PRE_SCENE_RES_PACKAGE_NAME, packageVersion));
            string manifestPath = Path.Combine(manifestRoot, YooAssetSettingsData.GetManifestBinaryFileName(PRE_SCENE_RES_PACKAGE_NAME, packageVersion));
            return File.Exists(hashPath) && File.Exists(manifestPath);
        }
        catch (Exception ex)
        {
            Debug.LogError($"[Launch] Check PreSceneRes cache manifest failed: {ex.Message}");
            return false;
        }
    }
    /// <summary>
    /// 对齐 YooAsset 默认缓存根目录规则,供启动阶段做本地 manifest 存在性判断。
    /// </summary>
    private string GetYooDefaultCacheRoot()
    {
        string yooFolder = YooAssetSettingsData.GetDefaultYooFolderName();
#if UNITY_EDITOR
        string projectPath = Path.GetDirectoryName(Application.dataPath);
        return string.IsNullOrEmpty(yooFolder) ? projectPath : Path.Combine(projectPath, yooFolder);
#elif UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX
        return string.IsNullOrEmpty(yooFolder) ? Application.dataPath : Path.Combine(Application.dataPath, yooFolder);
#elif UNITY_STANDALONE_OSX
        return string.IsNullOrEmpty(yooFolder) ? Application.persistentDataPath : Path.Combine(Application.persistentDataPath, yooFolder);
#else
        return string.IsNullOrEmpty(yooFolder) ? Application.persistentDataPath : Path.Combine(Application.persistentDataPath, yooFolder);
#endif
    }
#endif
    // 启动入口
    async void Start()
    {
        Debug.Log("Launch Start");
        float startTime = Time.realtimeSinceStartup;
        Debug.LogError("[Launch] Start");
#if UNITY_WEBGL
            await VersionConfigEx.Get();
#endif
@@ -311,6 +521,7 @@
        InitSetting();
        // 1. 打开加载界面
        LogTiming("Start", startTime);
    }
    /// <summary>
@@ -319,6 +530,7 @@
    /// </summary>
    public async UniTask InitYooAssetEarlyAsync()
    {
        float startTime = Time.realtimeSinceStartup;
        try
        {
            YooAsset.IRemoteServices remoteServices = null;
@@ -362,7 +574,7 @@
                : WebGLRemoteConfig.CreateRemoteServices();
            if (!string.IsNullOrEmpty(webCdnUrl))
                remoteCdnBaseUrl = webCdnUrl;
            Debug.Log($"[Launch] WebGL remoteCdnBaseUrl={remoteCdnBaseUrl ?? WebGLRemoteConfig.ActiveServerURL}");
            Debug.LogError($"[Launch] WebGL remoteCdnBaseUrl={remoteCdnBaseUrl ?? WebGLRemoteConfig.ActiveServerURL}");
#else
            // 非 WebGL 正式包:
            // 先读 VersionConfigEx(Resources.Load,不依赖 YooAsset),
@@ -380,11 +592,11 @@
            else
            {
                playMode = EPlayMode.OfflinePlayMode;
                Debug.Log("[Launch] No cdnUrl configured, using OfflinePlayMode");
                Debug.LogError("[Launch] No cdnUrl configured, using OfflinePlayMode");
            }
#endif
            Debug.Log($"[Launch] cdnUrl={remoteCdnBaseUrl+ LocalResManager.fixPath}, PlayMode={playMode}");
            Debug.LogError($"[Launch] cdnUrl={remoteCdnBaseUrl+ LocalResManager.fixPath}, PlayMode={playMode}");
            await YooAssetInitializer.Instance.InitializeAsync(playMode, remoteServices, remoteCdnBaseUrl + LocalResManager.fixPath);
@@ -401,16 +613,21 @@
            // HostPlayMode: 不预下载全部资源。YooAsset CacheFileSystem 在 LoadAssetAsync 时自动按需从 CDN 下载。
            Debug.Log("[Launch] YooAsset early init completed. State: " + YooAssetInitializer.Instance.State);
            Debug.LogError("[Launch] YooAsset early init completed. State: " + YooAssetInitializer.Instance.State);
        }
        catch (Exception ex)
        {
            Debug.LogError($"[Launch] YooAsset early init exception: {ex}");
        }
        finally
        {
            LogTiming("InitYooAssetEarlyAsync", startTime);
        }
    }
    private void InitSetting()
    {
        float startTime = Time.realtimeSinceStartup;
        System.Globalization.CultureInfo culture = System.Globalization.CultureInfo.CreateSpecificCulture("en-US");
        System.Globalization.CultureInfo.CurrentCulture = culture;
        System.Globalization.CultureInfo.CurrentUICulture = culture;
@@ -424,16 +641,21 @@
        // 任何都要去请求版本信息
        LocalResManager.step = LocalResManager.LoadDllStep.RequestVersion;
        LogTiming("InitSetting", startTime);
    }
    private void InitPlugins()
    {
        float startTime = Time.realtimeSinceStartup;
        DOTween.Init();
        LogTiming("InitPlugins", startTime);
    }
    private void SDKInit()
    {
        float startTime = Time.realtimeSinceStartup;
        AotSdkUtility.Instance.Init();
        LogTiming("SDKInit", startTime);
    }
    /// <summary>
@@ -449,6 +671,7 @@
        if (launchExWin != null || isOpeningLaunchUI)
            return;
        float startTime = Time.realtimeSinceStartup;
        isOpeningLaunchUI = true;
        try
        {
@@ -462,6 +685,7 @@
        finally
        {
            isOpeningLaunchUI = false;
            LogTiming("ShowLaunchUIAsync", startTime);
        }
    }
    
@@ -471,6 +695,7 @@
    /// </summary>
    private void LoadMetadataForAOTAssemblies()
    {
        float startTime = Time.realtimeSinceStartup;
        /// 注意,补充元数据是给 AOT dll 补充元数据,而不是给热更新 dll 补充元数据。
        /// 热更新 dll 不缺元数据,不需要补充,如果调用 LoadMetadataForAOTAssembly 会返回错误。
        /// 
@@ -481,8 +706,9 @@
                continue;
            // 加载assembly对应的dll,会自动为它hook。一旦aot泛型函数的native函数不存在,用解释器版本代码
            LoadImageErrorCode err = RuntimeApi.LoadMetadataForAOTAssembly(ReadBytesFromStreamingAssets(aotDllName), mode);
            Debug.Log($"LoadMetadataForAOTAssembly:{aotDllName}. mode:{mode} ret:{err}");
            Debug.LogError($"LoadMetadataForAOTAssembly:{aotDllName}. mode:{mode} ret:{err}");
        }
        LogTiming("LoadMetadataForAOTAssemblies", startTime);
    }
    public static byte[] ReadBytesFromStreamingAssets(string dllName)
@@ -492,6 +718,7 @@
    private void StartGame()
    {
        float startTime = Time.realtimeSinceStartup;
#if !UNITY_EDITOR
        LoadMetadataForAOTAssemblies();
        _hotUpdateAss = Assembly.Load(ReadBytesFromStreamingAssets("Main.dll.bytes"));
@@ -515,7 +742,8 @@
            Debug.LogError("无法找到get_Instance方法");
        }
        LocalResManager.Instance.RecordLauchEvent(3);
        Debug.Log("进入游戏流程");
        Debug.LogError("进入游戏流程");
        LogTiming("StartGame", startTime);
    }
    private void DestroySingleton()
@@ -615,6 +843,7 @@
    /// </summary>
    private async UniTask ReadDllBytes(Action callback)
    {
        float startTime = Time.realtimeSinceStartup;
        var dllPackage = YooAssets.TryGetPackage("Dll");
        if (dllPackage == null)
        {
@@ -646,12 +875,12 @@
                    downloadBytes = downloader != null ? downloader.TotalDownloadBytes : 0;
                    needRemote = downloadCount > 0 || downloadBytes > 0;
                    downloadStartTime = Time.realtimeSinceStartup;
                    Debug.Log($"[YooAssetDownload] Start on-demand source=Launch.ReadDllBytes, package={dllPackage.PackageName}, location='{location}', files={downloadCount}, size={FormatBytes(downloadBytes)}");
                    Debug.LogError($"[YooAssetDownload] Start on-demand source=Launch.ReadDllBytes, package={dllPackage.PackageName}, location='{location}', files={downloadCount}, size={FormatBytes(downloadBytes)}");
                }
            }
            catch (Exception ex)
            {
                Debug.LogWarning($"[YooAssetDownload] Check DLL download failed. location='{location}', error={ex.Message}");
                Debug.LogError($"[YooAssetDownload] Check DLL download failed. location='{location}', error={ex.Message}");
            }
            var handle = dllPackage.LoadAssetAsync<TextAsset>(location);
@@ -671,15 +900,16 @@
                if (needRemote)
                {
                    float cost = Time.realtimeSinceStartup - downloadStartTime;
                    Debug.Log($"[YooAssetDownload] Finish on-demand source=Launch.ReadDllBytes, package={dllPackage.PackageName}, location='{location}', files={downloadCount}, size={FormatBytes(downloadBytes)}, cost={cost:F2}s");
                    Debug.LogError($"[YooAssetDownload] Finish on-demand source=Launch.ReadDllBytes, package={dllPackage.PackageName}, location='{location}', files={downloadCount}, size={FormatBytes(downloadBytes)}, cost={cost:F2}s");
                }
                s_assetDatas[dllName] = ((TextAsset)handle.AssetObject).bytes;
                Debug.Log($"[Launch] Loaded DLL: {dllName} size:{s_assetDatas[dllName].Length}");
                Debug.LogError($"[Launch] Loaded DLL: {dllName} size:{s_assetDatas[dllName].Length}");
            }
            DllLoadProgress = (float)(i + 1) / total;
        }
        LogTiming("ReadDllBytes", startTime);
        callback?.Invoke();
    }
@@ -726,14 +956,14 @@
                }
            }
            Debug.Log($"[LocalFileRemoteServices] 索引了 {_index.Count} 个文件,根目录: {localPath}");
            Debug.LogError($"[LocalFileRemoteServices] 索引了 {_index.Count} 个文件,根目录: {localPath}");
        }
        public string GetRemoteMainURL(string fileName)
        {
            if (_index.TryGetValue(fileName, out var uri))
                return uri;
            Debug.LogWarning($"[LocalFileRemoteServices] 文件未找到: {fileName}");
            Debug.LogError($"[LocalFileRemoteServices] 文件未找到: {fileName}");
            return $"file:///NOT_FOUND/{fileName}";
        }
Assets/Launch/Manager/LocalResManager.cs
@@ -20,6 +20,11 @@
public class LocalResManager : Singleton<LocalResManager>
{
    private static void LogTiming(string phase, float startTime)
    {
        Debug.LogError($"[LocalResManager][Timing] {phase} cost={(Time.realtimeSinceStartup - startTime):F3}s");
    }
    //不下载时本地安卓测试路径
    public string assetBundlesPath = Application.dataPath + "/../AssetBundles/Android/";
    public string StreamingAssetPath
@@ -84,7 +89,7 @@
        set
        {
            m_step = value;
            Debug.Log("LoadDllStep:" + m_step);
            Debug.LogError("LoadDllStep:" + m_step);
        }
    }
@@ -184,6 +189,7 @@
    // BuiltIn 路径下的资源(Sprites/Prefabs)应传入 DefaultPackage(= Builtin 包)
    private async UniTask<T> LoadAssetWithRetryAsync<T>(string location, ResourcePackage package = null) where T : UnityEngine.Object
    {
        float startTime = Time.realtimeSinceStartup;
        package ??= YooAssetInitializer.Instance.DefaultPackage;
        Exception lastEx = null;
        for (int attempt = 0; attempt <= YOO_MAX_RETRY; attempt++)
@@ -216,12 +222,13 @@
                if (attempt < YOO_MAX_RETRY)
                {
                    int delayMs = YOO_BASE_DELAY_MS * (1 << attempt);
                    Debug.LogWarning($"[LocalResManager] Load '{location}' failed (attempt {attempt + 1}/{YOO_MAX_RETRY + 1}), retry in {delayMs}ms: {ex.Message}");
                    Debug.LogError($"[LocalResManager] Load '{location}' failed (attempt {attempt + 1}/{YOO_MAX_RETRY + 1}), retry in {delayMs}ms: {ex.Message}");
                    await UniTask.Delay(delayMs);
                }
            }
        }
        Debug.LogError($"[LocalResManager] Load '{location}' failed after {YOO_MAX_RETRY + 1} attempts: {lastEx?.Message}");
        LogTiming($"LoadAssetWithRetryAsync<{typeof(T).Name}>('{location}')", startTime);
        return null;
    }
@@ -251,12 +258,12 @@
            if (context.Enabled)
            {
                Debug.Log($"[YooAssetDownload] Start on-demand source={source}, package={context.PackageName}, location='{location}', files={context.Count}, size={FormatBytes(context.Bytes)}");
                Debug.LogError($"[YooAssetDownload] Start on-demand source={source}, package={context.PackageName}, location='{location}', files={context.Count}, size={FormatBytes(context.Bytes)}");
            }
        }
        catch (Exception ex)
        {
            Debug.LogWarning($"[YooAssetDownload] Check on-demand download failed. source={source}, package={context.PackageName}, location='{location}', error={ex.Message}");
            Debug.LogError($"[YooAssetDownload] Check on-demand download failed. source={source}, package={context.PackageName}, location='{location}', error={ex.Message}");
        }
        return context;
@@ -270,7 +277,7 @@
        float cost = Time.realtimeSinceStartup - context.StartTime;
        if (succeed)
        {
            Debug.Log($"[YooAssetDownload] Finish on-demand source={context.Source}, package={context.PackageName}, location='{context.Location}', files={context.Count}, size={FormatBytes(context.Bytes)}, cost={cost:F2}s");
            Debug.LogError($"[YooAssetDownload] Finish on-demand source={context.Source}, package={context.PackageName}, location='{context.Location}', files={context.Count}, size={FormatBytes(context.Bytes)}, cost={cost:F2}s");
        }
        else
        {
@@ -319,12 +326,13 @@
            locationValid = $"check-error:{ex.Message}";
        }
        Debug.Log($"[LocalResManager][Diag] BeforeLoad attempt={attempt + 1}, location='{location}', package='{package.PackageName}', initStatus={package.InitializeStatus}, packageValid={package.PackageValid}, manifestVersion={manifestVersion}, assets={assetTotalCount}, bundles={bundleTotalCount}, locationValid={locationValid}, state={YooAssetInitializer.Instance.State}, lastError={YooAssetInitializer.Instance.LastError}");
        Debug.LogError($"[LocalResManager][Diag] BeforeLoad attempt={attempt + 1}, location='{location}', package='{package.PackageName}', initStatus={package.InitializeStatus}, packageValid={package.PackageValid}, manifestVersion={manifestVersion}, assets={assetTotalCount}, bundles={bundleTotalCount}, locationValid={locationValid}, state={YooAssetInitializer.Instance.State}, lastError={YooAssetInitializer.Instance.LastError}");
    }
#endif
    public void Init()
    {
        float startTime = Time.realtimeSinceStartup;
        if (LocalSave.GetString("#@#BrancH") != string.Empty)
        {
            int tmpbranch;
@@ -347,12 +355,13 @@
#endif
        Clock.Init();
        Debug.Log("LocalResManager.Init");
        Debug.LogError("LocalResManager.Init");
        LogTiming("Init", startTime);
    }
    public void Release()
    {
        Debug.Log("提前ResManager.Release资源");
        Debug.LogError("提前ResManager.Release资源");
    }
#if UNITY_WEBGL
@@ -375,7 +384,7 @@
            urlIndex++;
            versionUrl = url;
            Debug.Log("http地址:versionUrl  " + url);
            Debug.LogError("http地址:versionUrl  " + url);
            HttpRequest.Instance.RequestHttpGet(url, HttpRequest.defaultHttpContentType, 1, OnVersionCheckResult);
        }
        catch (Exception ex)
@@ -386,6 +395,7 @@
#else
    public void RequestVersionCheck()
    {
        float startTime = Time.realtimeSinceStartup;
        var versionConfig = VersionConfigEx.Get();
        var tables = new Dictionary<string, string>();
        tables["channel"] = versionConfig.appId;
@@ -401,8 +411,9 @@
        urlIndex++;
        versionUrl = url;
        Debug.Log("http地址:versionUrl  " + url);
        Debug.LogError("http地址:versionUrl  " + url);
        HttpRequest.Instance.RequestHttpGet(url, HttpRequest.defaultHttpContentType, 1, OnVersionCheckResult);
        LogTiming("RequestVersionCheck", startTime);
    }
#endif
@@ -412,6 +423,7 @@
    /// </summary>
    private void OnVersionCheckResult(bool _ok, string _result)
    {
        float startTime = Time.realtimeSinceStartup;
        if (!_ok)
        {
            RetryVersionCheck($"[LocalResManager] 版本接口请求失败,url={versionUrl},error={_result}");
@@ -432,10 +444,12 @@
        // InitialFunction 是启动期功能开关和语言配置;CDN 失败时会回落到包内表,避免启动卡死。
        LoadInitialFunctionAndStartAsync(BuildInitialFunctionUrl()).Forget();
        LogTiming("OnVersionCheckResult", startTime);
    }
    private bool TryUpdateVersionInfo(string rawResult)
    {
        float startTime = Time.realtimeSinceStartup;
        versionUrlResult = NormalizeVersionJson(rawResult);
        if (string.IsNullOrEmpty(versionUrlResult))
        {
@@ -446,6 +460,7 @@
        try
        {
            versionInfo = JsonMapper.ToObject<VersionInfo>(versionUrlResult);
            LogTiming("TryUpdateVersionInfo", startTime);
            return true;
        }
        catch (Exception ex)
@@ -459,10 +474,11 @@
    private void TryApplyCdnUrlFromVersionInfo()
    {
        float startTime = Time.realtimeSinceStartup;
        var versionConfig = VersionConfigEx.config;
        if (versionInfo == null || versionConfig == null)
        {
            Debug.LogWarning("[LocalResManager] VersionInfo 或 VersionConfigEx 为空,将使用本地/离线资源模式。");
            Debug.LogError("[LocalResManager] VersionInfo 或 VersionConfigEx 为空,将使用本地/离线资源模式。");
            return;
        }
@@ -471,15 +487,16 @@
            string resourceUrl = versionInfo.GetResourcesURL(versionConfig.branch);
            if (string.IsNullOrEmpty(resourceUrl))
            {
                Debug.LogWarning($"[LocalResManager] resource_url 为空,将使用本地/离线资源模式。branch={versionConfig.branch}");
                Debug.LogError($"[LocalResManager] resource_url 为空,将使用本地/离线资源模式。branch={versionConfig.branch}");
                return;
            }
            versionConfig.cdnUrl = resourceUrl.TrimEnd('/');
            LogTiming("TryApplyCdnUrlFromVersionInfo", startTime);
        }
        catch (Exception urlEx)
        {
            Debug.LogWarning($"[LocalResManager] 获取 resource_url 失败,将使用本地/离线资源模式: {urlEx.Message}");
            Debug.LogError($"[LocalResManager] 获取 resource_url 失败,将使用本地/离线资源模式: {urlEx.Message}");
        }
    }
@@ -496,6 +513,7 @@
    private async UniTaskVoid LoadInitialFunctionAndStartAsync(string initFuncUrl)
    {
        float startTime = Time.realtimeSinceStartup;
        //默认要保证保底测试正确,后续可以在编辑器下增加个开关,来决定是否从CDN读取配置测试
        //打包时会默认拷贝InitialFunction.txt到 Resources/Config/InitialFunction.txt
#if UNITY_EDITOR
@@ -508,6 +526,7 @@
    }
#endif
        await InitializeYooAssetAndEnterReadBytesAsync();
    LogTiming("LoadInitialFunctionAndStartAsync", startTime);
    }
    private const int INIT_FUNC_MAX_RETRY = 5;
@@ -515,9 +534,10 @@
    private async UniTask<bool> TryLoadInitialFunctionFromCdnAsync(string initFuncUrl)
    {
        float startTime = Time.realtimeSinceStartup;
        if (string.IsNullOrEmpty(initFuncUrl))
        {
            Debug.LogWarning("[LocalResManager] cdnUrl 为空,跳过 CDN InitialFunction.txt,尝试读取包内配置。");
            Debug.LogError("[LocalResManager] cdnUrl 为空,跳过 CDN InitialFunction.txt,尝试读取包内配置。");
            return false;
        }
@@ -525,20 +545,20 @@
        {
            if (i > 0)
            {
                Debug.LogWarning($"[LocalResManager] 重试加载 InitialFunction.txt 第 {i}/{INIT_FUNC_MAX_RETRY} 次...");
                Debug.LogError($"[LocalResManager] 重试加载 InitialFunction.txt 第 {i}/{INIT_FUNC_MAX_RETRY} 次...");
                await UniTask.Delay(TimeSpan.FromSeconds(INIT_FUNC_RETRY_DELAY_SEC));
            }
            var response = await HttpRequest.Instance.UnityWebRequestGetAsync(initFuncUrl, HttpRequest.defaultHttpContentType, 10);
            if (!response.ok)
            {
                Debug.LogWarning($"[LocalResManager] 从 CDN 加载 InitialFunction.txt 失败(第 {i + 1} 次),url={initFuncUrl},error={response.message}");
                Debug.LogError($"[LocalResManager] 从 CDN 加载 InitialFunction.txt 失败(第 {i + 1} 次),url={initFuncUrl},error={response.message}");
                continue;
            }
            if (string.IsNullOrEmpty(response.message))
            {
                Debug.LogWarning($"[LocalResManager] CDN InitialFunction.txt 内容为空(第 {i + 1} 次),url={initFuncUrl}");
                Debug.LogError($"[LocalResManager] CDN InitialFunction.txt 内容为空(第 {i + 1} 次),url={initFuncUrl}");
                continue;
            }
@@ -547,7 +567,8 @@
                InitialFunctionConfigEx.Init(response.message);
                // 需要根据languagefix 重新把cdn地址转向正确的多语言版本的cdn地址
                InitDefaultLanguage();
                Debug.Log($"[LocalResManager] 成功加载 CDN InitialFunction.txt(第 {i + 1} 次尝试)");
                Debug.LogError($"[LocalResManager] 成功加载 CDN InitialFunction.txt(第 {i + 1} 次尝试)");
                LogTiming("TryLoadInitialFunctionFromCdnAsync", startTime);
                return true;
            }
            catch (Exception ex)
@@ -563,19 +584,21 @@
    private async UniTask<bool> TryLoadInitialFunctionFromResourcesAsync()
    {
        float startTime = Time.realtimeSinceStartup;
        try
        {
            TextAsset textAsset = await Resources.LoadAsync<TextAsset>("Config/InitialFunction") as TextAsset;
            if (textAsset == null || string.IsNullOrEmpty(textAsset.text))
            {
                Debug.LogWarning("[LocalResManager] 包内 InitialFunction.txt 不存在或内容为空,将继续启动。");
                Debug.LogError("[LocalResManager] 包内 InitialFunction.txt 不存在或内容为空,将继续启动。");
                return false;
            }
            InitialFunctionConfigEx.Init(textAsset.text);
            // 需要根据languagefix 重新把cdn地址转向正确的多语言版本的cdn地址
            InitDefaultLanguage();
            Debug.Log("[LocalResManager] 已使用包内 InitialFunction.txt 初始化启动配置。");
            Debug.LogError("[LocalResManager] 已使用包内 InitialFunction.txt 初始化启动配置。");
            LogTiming("TryLoadInitialFunctionFromResourcesAsync", startTime);
            return true;
        }
        catch (Exception ex)
@@ -587,6 +610,7 @@
    private async UniTask InitializeYooAssetAndEnterReadBytesAsync()
    {
        float startTime = Time.realtimeSinceStartup;
        if (Launch.Instance == null)
        {
            Debug.LogError("[LocalResManager] Launch.Instance 为空,无法继续启动流程。");
@@ -603,11 +627,12 @@
        // YooAsset 的 Manifest 就绪后,才可以显示加载界面并进入 DLL 读取阶段。
        Launch.Instance.ShowLaunchUI();
        step = LoadDllStep.ReadBytes;
        LogTiming("InitializeYooAssetAndEnterReadBytesAsync", startTime);
    }
    private void RetryVersionCheck(string message)
    {
        Debug.LogWarning(message + " 将在1秒后重试...");
        Debug.LogError(message + " 将在1秒后重试...");
        Clock.AlarmAt(DateTime.Now + TimeSpan.FromSeconds(1), RequestVersionCheck);
    }
@@ -623,7 +648,7 @@
        if (normalized == "{}" || normalized.Equals("null", StringComparison.OrdinalIgnoreCase))
        {
            Debug.LogWarning("[LocalResManager] 服务端返回空数据({}或null),该渠道可能未在服务端配置版本信息");
            Debug.LogError("[LocalResManager] 服务端返回空数据({}或null),该渠道可能未在服务端配置版本信息");
            return string.Empty;
        }
@@ -798,17 +823,17 @@
        if (languageStartDict == null || VersionConfigEx.config == null || !languageStartDict.ContainsKey(VersionConfigEx.config.appId))
        {
            //检查有没多语言
            Debug.Log("当前渠道未开启多语言:" + (VersionConfigEx.config != null ? VersionConfigEx.config.appId : "null"));
            Debug.LogError("当前渠道未开启多语言:" + (VersionConfigEx.config != null ? VersionConfigEx.config.appId : "null"));
            return;
        }
        Debug.LogFormat("系统语言:{0} {1}", Application.systemLanguage, config.Numerical1);
        Debug.LogError(string.Format("系统语言:{0} {1}", Application.systemLanguage, config.Numerical1));
        var id = LocalSave.GetString("LANGUAGE_ID1");
        if (!string.IsNullOrEmpty(id))
        {
            //玩家已经选择过语言,不做处理
            Debug.Log("当前选择语言:" + id);
            Debug.LogError("当前选择语言:" + id);
            return;
        }
@@ -842,7 +867,7 @@
        }
        Id = id;
        Debug.LogFormat("系统语言:{0} 设置为{1}", Application.systemLanguage, Id);
        Debug.LogError(string.Format("系统语言:{0} 设置为{1}", Application.systemLanguage, Id));
    }
    #region 事件汇报
Assets/Launch/Manager/YooAssetInitializer.cs
@@ -445,6 +445,23 @@
            }
            loadHandle.Release();
#if !UNITY_EDITOR
            // 后台清理旧版本缓存,降低下次启动时 CacheFileSystem 的扫描与校验成本。
            var clearBundleOp = package.ClearCacheFilesAsync(EFileClearMode.ClearUnusedBundleFiles);
            await clearBundleOp.ToUniTask();
            if (clearBundleOp.Status != EOperationStatus.Succeed)
            {
                Debug.LogWarning($"[YooAssetInitializer] UpdateAndWarmup clear unused bundles failed: {clearBundleOp.Error}");
            }
            var clearManifestOp = package.ClearCacheFilesAsync(EFileClearMode.ClearUnusedManifestFiles);
            await clearManifestOp.ToUniTask();
            if (clearManifestOp.Status != EOperationStatus.Succeed)
            {
                Debug.LogWarning($"[YooAssetInitializer] UpdateAndWarmup clear unused manifests failed: {clearManifestOp.Error}");
            }
#endif
            LocalSave.SetString(versionKey, latest);
            Debug.Log($"[YooAssetInitializer] Package '{package.PackageName}' updated to {latest}, will be used on next launch.");
        }
Assets/Launch/UI/LaunchWins/LaunchExWin.cs
@@ -1,5 +1,6 @@
using System.Collections;
using System.Collections.Generic;
using System;
using UnityEngine;
using UnityEngine.UI;
using Cysharp.Threading.Tasks;
@@ -43,6 +44,11 @@
        }
    }
    private static void LogTiming(string phase, float startTime)
    {
        Debug.LogError($"[LaunchExWin][Timing] {phase} cost={(Time.realtimeSinceStartup - startTime):F3}s");
    }
    void Awake()
    {
        launchExWin = this;
@@ -57,77 +63,43 @@
    /// </summary>
    public async UniTask InitSpritesAsync()
    {
        Debug.LogError($"<color=yellow>[YLTEST][LaunchExWin] 开始初始化精灵</color>");
        var languageShowDict = JsonMapper.ToObject<Dictionary<string, string>>(InitialFunctionConfigEx.Get("Language").Numerical1);
        var id = string.IsNullOrEmpty(LocalResManager.Id) ? "zh" : LocalResManager.Id;  // 默认中文
        sliderText = languageShowDict[id];
        var sprites = await UniTask.WhenAll(
            LocalResManager.Instance.LoadSpriteAsync("TY_TB_JH1"),
            LocalResManager.Instance.LoadSpriteAsync("TY_TB_JH2"),
            LocalResManager.Instance.LoadSpriteAsync("LoadingBottom"),
            LocalResManager.Instance.LoadSpriteAsync("LoadingSlider")
        );
        if (this == null) return; // destroyed check after await
        Debug.LogError($"<color=cyan>[YLTEST][LaunchExWin] 精灵加载完成 - LoadingBottom={sprites.Item3 != null}, LoadingSlider={sprites.Item4 != null}</color>");
        imagebg1.sprite = sprites.Item1;
        imagebg2.sprite = sprites.Item1;
        imageCircle.sprite = sprites.Item2;
        imagebg3.sprite = sprites.Item3;
        imageloding.sprite = sprites.Item4;
        // 诊断日志:检查进度条组件状态
        DiagnoseProgressBarComponents();
        if (sprites.Item3 != null && sprites.Item4 != null)
        float startTime = Time.realtimeSinceStartup;
        try
        {
            SetProgressBarImagesVisible(true);
            Debug.LogError($"<color=green>[YLTEST][LaunchExWin] 进度条精灵设置成功并显示</color>");
        }
        else
        {
            Debug.LogError($"<color=red>[YLTEST][LaunchExWin] 进度条精灵加载失败 LoadingBottom={sprites.Item3 != null}, LoadingSlider={sprites.Item4 != null}</color>");
        }
    }
            var languageShowDict = JsonMapper.ToObject<Dictionary<string, string>>(InitialFunctionConfigEx.Get("Language").Numerical1);
            var id = string.IsNullOrEmpty(LocalResManager.Id) ? "zh" : LocalResManager.Id;  // 默认中文
            sliderText = languageShowDict[id];
            var sprites = await UniTask.WhenAll(
                LocalResManager.Instance.LoadSpriteAsync("TY_TB_JH1"),
                LocalResManager.Instance.LoadSpriteAsync("TY_TB_JH2"),
                LocalResManager.Instance.LoadSpriteAsync("LoadingBottom"),
                LocalResManager.Instance.LoadSpriteAsync("LoadingSlider")
            );
    private void DiagnoseProgressBarComponents()
    {
        Debug.LogError($"<color=yellow>[YLTEST][LaunchExWin] 检查进度条组件状态</color>");
        if (imagebg3 != null)
        {
            Debug.LogError($"<color=cyan>[YLTEST][LaunchExWin] imagebg3 (Background) 存在: {imagebg3.name}</color>");
            Debug.LogError($"<color=cyan>[YLTEST][LaunchExWin] - Sprite: {(imagebg3.sprite != null ? imagebg3.sprite.name : "NULL")}</color>");
            Debug.LogError($"<color=cyan>[YLTEST][LaunchExWin] - Color: {imagebg3.color}</color>");
            Debug.LogError($"<color=cyan>[YLTEST][LaunchExWin] - Material: {(imagebg3.material != null ? imagebg3.material.name : "NULL")}</color>");
            Debug.LogError($"<color=cyan>[YLTEST][LaunchExWin] - Shader: {(imagebg3.material != null && imagebg3.material.shader != null ? imagebg3.material.shader.name : "NULL")}</color>");
            Debug.LogError($"<color=cyan>[YLTEST][LaunchExWin] - Enabled: {imagebg3.enabled}</color>");
        }
        else
        {
            Debug.LogError($"<color=red>[YLTEST][LaunchExWin] imagebg3 is NULL</color>");
        }
            if (this == null) return; // destroyed check after await
        if (imageloding != null)
        {
            Debug.LogError($"<color=cyan>[YLTEST][LaunchExWin] imageloding (Fill) 存在: {imageloding.name}</color>");
            Debug.LogError($"<color=cyan>[YLTEST][LaunchExWin] - Sprite: {(imageloding.sprite != null ? imageloding.sprite.name : "NULL")}</color>");
            Debug.LogError($"<color=cyan>[YLTEST][LaunchExWin] - Color: {imageloding.color}</color>");
            Debug.LogError($"<color=cyan>[YLTEST][LaunchExWin] - Material: {(imageloding.material != null ? imageloding.material.name : "NULL")}</color>");
            Debug.LogError($"<color=cyan>[YLTEST][LaunchExWin] - Shader: {(imageloding.material != null && imageloding.material.shader != null ? imageloding.material.shader.name : "NULL")}</color>");
            Debug.LogError($"<color=cyan>[YLTEST][LaunchExWin] - Enabled: {imageloding.enabled}</color>");
            if (imageloding.sprite == null)
            imagebg1.sprite = sprites.Item1;
            imagebg2.sprite = sprites.Item1;
            imageCircle.sprite = sprites.Item2;
            imagebg3.sprite = sprites.Item3;
            imageloding.sprite = sprites.Item4;
            if (sprites.Item3 != null && sprites.Item4 != null)
            {
                Debug.LogError($"<color=orange>[YLTEST][LaunchExWin] 问题: imageloding Sprite 为 NULL,这会导致显示白色</color>");
                SetProgressBarImagesVisible(true);
            }
            else
            {
                Debug.LogError($"[LaunchExWin] 进度条精灵加载失败 LoadingBottom={sprites.Item3 != null}, LoadingSlider={sprites.Item4 != null}");
            }
        }
        else
        catch (Exception ex)
        {
            Debug.LogError($"<color=red>[YLTEST][LaunchExWin] imageloding is NULL</color>");
            Debug.LogError($"[LaunchExWin] InitSpritesAsync exception: {ex}");
        }
        finally
        {
            LogTiming("InitSpritesAsync", startTime);
        }
    }
@@ -266,8 +238,16 @@
    public static async UniTask<GameObject> OpenWindow()
    {
        GameObject window = GameObject.Instantiate(await LocalResManager.Instance.LoadBuiltInPrefabAsync("LaunchExWin"));
        // window.transform.localScale = Vector3.zero;  // 移除这行,让 LaunchExWin 正常显示直到 LaunchWin 打开
        return window;
        float startTime = Time.realtimeSinceStartup;
        try
        {
            GameObject window = GameObject.Instantiate(await LocalResManager.Instance.LoadBuiltInPrefabAsync("LaunchExWin"));
            // window.transform.localScale = Vector3.zero;  // 移除这行,让 LaunchExWin 正常显示直到 LaunchWin 打开
            return window;
        }
        finally
        {
            LogTiming("OpenWindow", startTime);
        }
    }
}