热更新流程
在前面第 42 篇和第 43 篇中,我们完成了 HybridCLR 的环境配置与基础框架搭建。本章将深入实战,系统讲解热更新流程中的四个核心环节:代码编写规范、代码编译、资源打包和资源加载。理解并掌握这些环节,是构建稳定可靠的热更新系统的关键。
一、热更新代码的编写规范
HybridCLR 虽然极大扩展了 Unity 的热更新能力,但它并非"银弹"。运行时热更新代码的编写存在一些必须遵守的规范和限制。开发者若不了解这些边界条件,很容易写出看似正确、但运行时崩溃或行为异常的代码。从第 42 篇的环境搭建到第 43 篇的基础框架,我们已经具备了运行热更新代码的底层能力,但代码本身的编写质量决定了这套系统能发挥多大价值。
在正式开始编码之前,需要明确一条核心原则:热更新程序集运行在解释器模式下,其执行效率约为 AOT 原生代码的 30% 到 50%。这就要求我们在设计代码时更加关注性能敏感路径,将高频调用的逻辑尽量放入主工程,而将业务逻辑、UI 控制和数据配置等低频变化的代码放入热更新程序集。
1.1 可热更新的代码特征
在 HybridCLR 架构下,被标记为热更新程序集(通常通过 Assembly Definition 引用)中的绝大多数 C# 代码都可以被热更新。具体来说,以下类型的代码可以安全地运行在热更新域中:
| 普通类、结构体、接口 | ✅ 完全支持 | 包括泛型类 |
| 方法调用与继承 | ✅ 完全支持 | 包括虚方法重写 |
| 协程(IEnumerator) | ✅ 完全支持 | 需 Unity 主工程定义 |
| 泛型方法与泛型类型 | ✅ 支持 | 但存在 AOT 泛型共享限制 |
| LINQ 表达式 | ✅ 支持 | 注意装箱开销 |
| 异步方法(async/await) | ✅ 支持 | 需主工程预定义 SynchronizationContext |
| unsafe 代码 | ✅ 支持 | 指针操作需谨慎 |
| P/Invoke 与 C++ 插件调用 | ✅ 支持 | 需正确的 DllImport 声明 |
| 反射操作 | ⚠️ 有限支持 | 部分反射 API 在 AOT 受限 |
| Delegate.CreateDelegate | ⚠️ 有限支持 | 需使用 CreateDelegate 而非 Create |
| 动态代码生成(Emit) | ❌ 不支持 | AOT 环境无法 JIT |
1.2 不支持热更新的代码类型
以下是热更新代码中必须避免或绕过的"雷区":
AOT 泛型实例化限制:这是 HybridCLR 用户最容易踩的坑。假设热更新程序集中定义了一个泛型方法 Foo<T>(),但泛型参数 T 的具体类型在 AOT 主工程中没有被实例化过,运行时将抛出 ExecutionEngineException。解决办法是在主工程的反向元数据补充配置(aotGenericTypes)中声明这些泛型实例,或使用 HybridCLR.RuntimeApi 的 LoadMetadataForAOTAssembly 在运行时补充。建议在项目初始化阶段统一调用补充元数据加载,避免遗漏。
值类型装箱频繁:热更新代码与主工程代码之间的值类型传递,如果未经过优化处理,可能产生大量装箱操作。例如将 struct MyData 作为 object 参数传递到主工程方法中时,每次调用都会触发装箱 GC 分配。建议在主工程中为频繁调用的方法同时提供泛型版本。在游戏主循环等高频路径中,一次不经意的装箱可能导致每秒数十 KB 的 GC 分配积累,最终引发频繁的 GC 暂停。
静态构造函数的时序依赖:热更新程序集中的静态构造函数(.cctor)的调用时机与普通 Mono 运行时不同,可能出现延迟执行或重复执行的情况。尽量避免在热更新代码中依赖静态构造函数的副作用来完成关键初始化。取而代之的做法是:在热更新程序集的入口 Main 方法中显式调用各个模块的初始化方法。
Marshal.StructureToPtr 等内存操作:部分与内存布局紧密相关的 API 在解释器模式下行为与预期不一致。如果热更新代码中需要频繁进行序列化和反序列化操作,建议将序列化逻辑封装到主工程提供的工具类中,热更新代码只做调用方。
System.Reflection.Emit 系列:由于 AOT 运行时无法在程序运行时动态生成新的 IL 代码,所有依赖 Emit 的库(如某些轻量级 ORM、动态代理生成器)都无法在热更新域中正常工作。如果需要动态创建类型,必须将这部分逻辑放在主工程中。
1.3 代码组织建议
根据实战经验,推荐以下代码组织模式:
// 热更新程序集中的主入口类
public class HotUpdateEntry
{
public static void Main()
{
// 初始化资源管理
AssetManager.Instance.Initialize();
// 初始化 UI 系统
UIManager.Instance.OpenPanel<LoginPanel>();
// 注册热更新回调,供主工程调用
GameBridge.RegisterHotUpdateCallbacks(new HotUpdateCallbacks
{
onEnterGame = OnEnterGame,
onPauseGame = OnPauseGame
});
}
private static void OnEnterGame()
{
// 仅用作回调委托,不在此处做复杂逻辑
SceneManager.LoadSceneAsync("MainScene");
}
private static void OnPauseGame() { /* 略 */ }
}
// 主工程中定义的桥接接口
public static class GameBridge
{
public static void RegisterHotUpdateCallbacks(HotUpdateCallbacks callbacks)
{
s_callbacks = callbacks;
}
}
建议将热更新程序集按功能拆分为多个子模块(如 HotUpdate_Logic、HotUpdate_UI、HotUpdate_Config),每个模块负责单一职责。主工程通过接口或抽象类与热更新模块交互,避免直接引用热更新程序集中的具体类型。具体模块划分方案可参考第 45 篇中关于项目结构的详细说明。
二、热更新代码的编译
当热更新代码编写完成后,需要编译成目标平台可执行的程序集。HybridCLR 使用原生 Unity 的 PlayerBuildPipeline 结合自定义脚本完成编译——这也是整个热更新流水线的起点。从第 43 篇我们搭建的构建环境中,已经配置好了 HybridCLR 编辑器扩展和必要的编译参数,本节在此基础上深入编译环节的每一个细节。
2.1 编译目标设置
热更新程序集的编译目标必须与主工程保持一致。HybridCLR 支持两种编译模式:
| 解析模式 | HybridCLR 解释器逐条执行 IL | 原生 30-50% | 最高 | 大多数 UI、配置、逻辑代码 |
| AOT 编译模式 | 部分编译为原生 + 元数据补充 | 原生 60-80% | 较高 | 战斗逻辑、寻路算法等性能敏感模块 |
解析模式:默认模式。热更新 DLL 被编译为 .NET Standard 2.0 格式的托管程序集,运行时由 HybridCLR 的解释器逐条解释执行 IL 指令。这种方式兼容性最好,但执行效率低于 AOT 编译模式。适合处理频率不高的业务逻辑、UI 事件响应和配置表读取。
AOT 编译模式:通过 HybridCLR 的 –aot 参数将部分热更新代码编译为原生代码,再结合补充元数据运行。适用于性能敏感的逻辑模块。需要注意,使用 AOT 编译模式时,被编译的代码无法再通过热更新替换——如果后续版本需要修改这些代码,必须随主工程重新发布。因此在项目早期阶段优先使用解析模式,待到版本稳定后再将确定不变的高频模块切换为 AOT 编译模式。
在 HybridCLR.Editor.Settings 中配置编译目标:
| hotUpdateAssemblyNames | 标记为热更新的程序集名称列表 | ["HotUpdate_Logic", "HotUpdate_UI"] |
| aotAssemblyNames | AOT 主工程程序集列表 | 自动生成 |
| externalHotUpdateAssembliyDirs | 外部热更新 DLL 路径 | ["Assets/HotUpdate"] |
| enableAotAssemblyOverride | 是否启用 AOT 程序集覆盖规则 | true |
2.2 依赖管理
热更新程序集的依赖管理至关重要。HybridCLR 要求在编译热更新代码时,必须能解析其引用的所有主工程程序集及元数据。依赖配置包括:
主工程依赖:热更新代码中引用的 UnityEngine.CoreModule、GameFramework 等主工程 DLL,需要在编译脚本中通过 -reference 参数显式声明。HybridCLR 的 CompileDllCommand 已经自动处理了大多数常见依赖,但如果项目中使用了第三方插件(如 DOTween、UniTask、Newtonsoft.Json 等),需要额外确认这些插件的程序集是否被正确引用。如果第三方插件本身也是热更新程序集则不需要额外处理,但如果它们是主工程 AOT 程序集,则在热更新代码中引用它们时必须保证对应的补充元数据已被顺利加载。
补充元数据:编译热更新程序集后,需要调用 HybridCLR.RuntimeApi.LoadMetadataForAOTAssembly 加载补充元数据。这些元数据文件在打包阶段从 AOT 程序集中提取,后缀为 -metadata.dll。补充元数据的加载不可逆,且必须在首次调用热更新代码之前完成。一种标准做法是在游戏启动场景的 Awake 方法中注册加载逻辑,确保 UI 界面初始化之前元数据已准备就绪。
循环依赖处理:严禁热更新程序集之间形成循环引用。在 Assembly Definition 的 Assembly Definition References 中只应声明单向依赖。若出现循环依赖,编译器会报告 CS1668 错误。常见的依赖组织方式是采用分层架构:HotUpdate_Common 作为基础模块被所有其他热更新程序集引用,HotUpdate_Logic 引用 HotUpdate_Common,HotUpdate_UI 引用 HotUpdate_Logic 和 HotUpdate_Common。这种单向的依赖链既清晰又便于维护。
编辑器与运行时路径差异:在 Unity Editor 中运行热更新代码时,DLL 的加载路径和 IL2CPP 打包后的真机环境不同。编译脚本需要区分 Editor 模式和真机模式,避免在 Editor 中误用了针对 IL2CPP 裁剪后的元数据文件。推荐的做法是在编译脚本中增加 #if UNITY_EDITOR 条件分支,为两种场景分别指定资源路径。
2.3 编译脚本
以下是一个完整的编译脚本示例,可以在 Editor 菜单中一键编译所有热更新程序集:
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using HybridCLR.Editor;
using HybridCLR.Editor.Commands;
public static class HotUpdateBuildTools
{
private const string HotUpdateDllOutputDir = "Build/HotUpdateDlls";
[MenuItem("HybridCLR/Compile HotUpdate Assemblies")]
public static void CompileHotUpdateAssemblies()
{
var buildTarget = EditorUserBuildSettings.activeBuildTarget;
var outputDir = Path.Combine(HotUpdateDllOutputDir, buildTarget.ToString());
if (!Directory.Exists(outputDir))
Directory.CreateDirectory(outputDir);
// Step 1: 编译热更新程序集(调用 HybridCLR 内置编译命令)
CompileDllCommand.CompileDll(buildTarget);
// Step 2: 复制热更新 DLL 到输出目录
var hotUpdateAssemblies = SettingsUtil.HotUpdateAssemblyNamesExcludePreserved;
foreach (var assemblyName in hotUpdateAssemblies)
{
var sourcePath = SettingsUtil.GetHotUpdateDllsOutputDirByTarget(buildTarget);
var destPath = Path.Combine(outputDir, $"{assemblyName}.dll");
File.Copy(Path.Combine(sourcePath, $"{assemblyName}.dll"), destPath, true);
UnityEngine.Debug.Log($"[HybridCLR] 已复制热更新 DLL: {destPath}");
}
// Step 3: 生成补充元数据
var aotMetadataDir = Path.Combine(outputDir, "AOTMetadata");
if (!Directory.Exists(aotMetadataDir))
Directory.CreateDirectory(aotMetadataDir);
foreach (var aotDll in Directory.GetFiles(SettingsUtil.GetAssembliesPostIl2CppStripDir(buildTarget), "*.dll"))
{
var fileName = Path.GetFileNameWithoutExtension(aotDll);
var destPath = Path.Combine(aotMetadataDir, $"{fileName}-metadata.dll");
File.Copy(aotDll, destPath, true);
}
UnityEngine.Debug.Log("[HybridCLR] 热更新编译与元数据提取完成!");
}
}
此脚本与第 43 篇中介绍的构建流水线配合使用,在每次发布新版本前自动执行。第 46 篇将进一步讨论如何在自动化 CI/CD 流程中集成此编译步骤。
三、热更新资源的打包
编译完成后,我们需要将热更新 DLL 和元数据打包为可供下载的资源包。这是热更新流程中承上启下的关键环节。
3.1 DLL 打包
热更新 DLL 建议按照版本号和平台分目录组织。常用的打包策略如下:
public static void BuildHotUpdateBundles(string version)
{
var buildTarget = EditorUserBuildSettings.activeBuildTarget;
var bundleDir = $"HotUpdateBundles/{buildTarget}/{version}";
if (!Directory.Exists(bundleDir))
Directory.CreateDirectory(bundleDir);
// 将热更新 DLL 打包为 AssetBundle
var dllFiles = Directory.GetFiles($"Build/HotUpdateDlls/{buildTarget}", "*.dll");
foreach (var dllPath in dllFiles)
{
var fileName = Path.GetFileName(dllPath);
var abPath = Path.Combine(bundleDir, $"{fileName}.bundle");
// 调用 AssetBundle 压缩:使用 LZ4 压缩保持读取效率
BuildPipeline.BuildAssetBundle(
null,
new[] { AssetDatabase.LoadAssetAtPath<Object>(dllPath) },
abPath,
BuildAssetBundleOptions.ChunkBasedCompression,
buildTarget
);
}
// 生成版本清单文件
GenerateVersionManifest(bundleDir, version);
}
无论 DLL 是否打包为 AssetBundle,都需要为每个资源计算 MD5 值,作为下载校验的依据。建议使用 LZ4 压缩而非 LZMA,因为 LZ4 支持随机读取,更适合运行时按需加载。
DLL 打包时还需注意文件命名规则。热更新 DLL 的文件名在 HotUpdateAssemblyNames 配置中指定,通常与 Assembly Definition 的名称保持一致。当 DLL 作为 AssetBundle 打包时,AssetBundle 的名称建议采用 {assemblyName}_{platform}.bundle 的格式,以便在资源加载时能够快速定位。此外,考虑到某些平台的文件名长度限制(如 Android 的 APK 内部路径),文件名应控制在 64 个字符以内。
关于 DLL 的加密处理,敏感项目可以在打包阶段对 DLL 进行 XOR 混淆或 AES 加密,然后在运行时由加载管理器解密后再交给 HybridCLR 的解释器。需要注意的是,加密会引入额外的运行时开销和启动延迟,需要权衡安全性和用户体验。大多数中小型项目使用简单的 XOR 混淆就足以防止普通的文件篡改。
3.2 元数据打包
在 Build/HotUpdateDlls/{platform}/AOTMetadata/ 目录下生成的 *-metadata.dll 文件需要与热更新 DLL 一同发布。元数据文件虽然体积较小(通常几十 KB 到几百 KB),但必须保证版本严格匹配。版本不匹配时,LoadMetadataForAOTAssembly 会返回失败码。
| mscorlib-metadata.dll | BCL 基础类库补充元数据 | 几乎不变 |
| UnityEngine.CoreModule-metadata.dll | Unity 引擎补充元数据 | 引擎升级时 |
| GameFramework-metadata.dll | 自研框架补充元数据 | 随主工程发布 |
| Assembly-CSharp-metadata.dll | 主工程脚本 AOT 补充元数据 | 随主工程发布 |
元数据文件的打包与热更新 DLL 完全相同,随版本一起部署到 CDN 服务器。
3.3 增量包生成
每次版本更新时,不必让用户下载所有热更新文件,只需下载相比上个版本的增量差异即可。增量包生成的核心是"按文件粒度对比版本差异":
public static void BuildIncrementalPackage(
string previousVersion,
string currentVersion,
string platform)
{
var prevManifest = LoadVersionManifest($"HotUpdateBundles/{platform}/{previousVersion}/manifest.json");
var currManifest = LoadVersionManifest($"HotUpdateBundles/{platform}/{currentVersion}/manifest.json");
var incrementalFiles = new List<string>();
foreach (var currEntry in currManifest.Files)
{
if (prevManifest.Files.TryGetValue(currEntry.Key, out var prevEntry))
{
// 文件已存在,但 MD5 不同 => 需要更新
if (prevEntry.MD5 != currEntry.MD5)
incrementalFiles.Add(currEntry.Key);
}
else
{
// 新增文件 => 需要下载
incrementalFiles.Add(currEntry.Key);
}
}
// 生成增量包配置文件
var incrementalManifest = new IncrementalManifest
{
BaseVersion = previousVersion,
TargetVersion = currentVersion,
Files = incrementalFiles
};
File.WriteAllText(
$"HotUpdateBundles/{platform}/incremental_{previousVersion}_to_{currentVersion}.json",
JsonUtility.ToJson(incrementalManifest)
);
}
在实际线上环境中,建议保留最近 3 个基准版本对应的增量包,避免用户从过老的版本更新时等待时间过长。如果检测到用户版本过旧,应引导用户下载完整包而非拼接多个增量包。因为拼接多个增量包意味着每个版本之间的增量规则都要连续叠加,出错概率逐层累积,出现问题后也难以定位。
增量包生成后,还需要为每个增量包生成对应的版本对比报告,列出所有变更文件的详细信息:变更类型(新增、修改、删除)、变更前后的大小对比、MD5 变化值。这份报告不仅用于内部复盘,还可以提交到版本管理系统作为发布记录。第 46 篇会进一步讨论如何将这些信息自动整合到 CI/CD 的发布日志中。
需要注意的是,当热更新涉及资源结构变更(如新增 AssetBundle 分组、调整资源目录结构)时,文件级的增量对比可能失去意义——因为同一个逻辑文件可能在结构中迁移到了不同的路径,导致客户端和服务器端的文件映射关系错乱。这种情况下建议直接触发完整包下载,同时在下个版本发布前避免大规模结构调整。
四、热更新资源的加载
资源打包并部署到 CDN 后,客户端就需要在合适的时机下载并加载这些热更新资源。这是整个流程中与用户体验最密切的一环。如果下载策略设计不当,用户可能卡在进度条界面数分钟;如果加载时机选择错误,可能出现资源尚未就绪玩家就进入了新场景的尴尬情况。因此本节将从下载策略、加载时机和错误处理三个维度给出完整的设计方案。
4.1 下载策略
热更新资源的下载策略直接影响到用户体验和更新成功率。不同的下载时机和并发策略适用于不同的场景。按下载时机划分,主要策略包括启动时全量下载、按需懒加载和后台静默预下载三种模式。启动时全量下载适合首次安装的小体量更新(<50MB),按需懒加载适合资源分布零散的大型项目,后台静默预下载则在用户体验和更新及时性之间取得平衡。
以下代码实现了一个基础的热更新下载管理器,包含版本清单获取、并发控制和错误处理:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
public class HotUpdateDownloader : MonoBehaviour
{
public IEnumerator DownloadHotUpdateFiles(string baseUrl, string version)
{
var manifestUrl = $"{baseUrl}{version}/manifest.json";
var manifestRequest = UnityWebRequest.Get(manifestUrl);
yield return manifestRequest.SendWebRequest();
if (manifestRequest.result != UnityWebRequest.Result.Success)
{
Debug.LogError($"获取版本清单失败: {manifestRequest.error}");
yield break;
}
var manifest = JsonUtility.FromJson<VersionManifest>(manifestRequest.downloadHandler.text);
var downloadTasks = new List<UnityWebRequest>();
var maxConcurrent = 3; // 最大并发下载数
for (int i = 0; i < manifest.Files.Count; i += maxConcurrent)
{
var batch = manifest.Files.GetRange(i,
Mathf.Min(maxConcurrent, manifest.Files.Count – i));
foreach (var fileEntry in batch)
{
var request = UnityWebRequest.Get($"{baseUrl}{version}/{fileEntry.Name}");
request.downloadHandler = new DownloadHandlerFile(
Path.Combine(Application.persistentDataPath, fileEntry.Name));
downloadTasks.Add(request);
request.SendWebRequest();
}
// 等待当前批次全部完成
foreach (var task in downloadTasks)
{
while (!task.isDone)
yield return null;
if (task.result != UnityWebRequest.Result.Success)
{
Debug.LogError($"下载 {task.url} 失败: {task.error}");
}
}
downloadTasks.Clear();
}
Debug.Log("所有热更新文件下载完成!");
}
}
推荐的下载策略要点:
- 并发下载:限制同时下载的数量为 3 个,避免移动设备带宽打满影响正常网络请求。对于体积较大的元数据文件(如 mscorlib-metadata.dll),可以单独调整优先级,优先完成核心文件的下载。
- 断点续传:使用 DownloadHandlerFile 写入临时目录,下载完成后再移动到目标目录。临时目录的路径建议使用 Application.temporaryCachePath,因为它可能被系统自动清理,而最终的目标目录使用 Application.persistentDataPath,保证文件在应用更新时仍保留。
- MD5 校验:下载完成后校验文件 MD5 是否与清单一致,不一致则删除重下。校验过程应在异步线程中执行,避免阻塞主线程。对于体积较大的文件,可使用分块校验的方式,读取前 1 MB 数据快速校验,或者使用内存映射文件提高大文件校验效率。
- 下载进度回调:向 UI 层提供进度回调,显示类似"正在更新 45/128"的进度信息。进度数据应当包含三个指标:总文件数、已下载文件数、当前下载文件的百分比进度。UI 层根据这些数据计算并展示总体进度百分比和预估剩余时间。
此外,下载管理器还需要处理以下边界情况:
下载任务队列管理:热更新下载通常不是一次性任务。在游戏运营过程中,可能因为活动配置更新、资源修复等原因多次下发下载任务。下载管理器需要维护一个优先级队列,确保关键资源(如热更新 DLL)始终优先于非关键资源(如活动海报图)下载。
网络切换处理:用户可能在下载过程中从 Wi-Fi 切换到移动数据,或从有网络环境进入无网络区域。下载管理器需要监听 Application.internetReachability 的变化,在网络切换时暂停下载并弹出确认对话框,获得用户确认后再继续。特别是在移动数据网络下,应当提示用户当前处于非 Wi-Fi 环境并预估流量消耗。
磁盘空间检测:在开始下载前,检查 Application.persistentDataPath 所在分区是否有足够的剩余空间。如果预估下载总量超过剩余空间的 80%,应提示用户清理空间或使用清理工具。一个大意的情况是:设备剩余空间足够下载单个文件,但解压或文件移动时需要两倍于原文件的临时空间,导致下载完成后写入失败。
4.2 加载时机
热更新资源加载的时机选择直接影响用户体验。加载过程建议分为三个阶段:
启动阶段(Splash):在游戏启动画面阶段,判断是否有热更新版本更新。若有,展示下载进度条并执行全量/增量下载。此阶段应保持轻量,仅下载必须的核心热更新 DLL 和元数据。
登录阶段(Login):游戏登录完成后,后台静默下载非关键资源(如活动配置、UI 图集等)。此阶段可以利用用户停留在登录界面或选服界面的时间窗口。
游戏内阶段(In-Game):玩家实际游戏过程中,仅在空闲帧下载后续关卡资源。可利用 Application.backgroundLoadingPriority 控制下载线程的 CPU 占用。
| 启动阶段 | 热更新 DLL、补充元数据、核心 prefab | 🔴 最高 | 看到下载进度条 |
| 登录阶段 | 通用 UI、公共配置、音效 | 🟡 中 | 无感知(静默下载) |
| 游戏内阶段 | 后续关卡资源、活动资源 | 🟢 低 | 可能的小卡顿 |
4.3 错误处理
热更新流程中可能出现的错误类型和处理策略如下:
网络错误:包括超时、DNS 解析失败、连接中断。解决方案是实现指数退避重试机制(1 秒、2 秒、4 秒、8 秒……最多重试 5 次)。每次重试前检查网络连通性,如果检测到无网络则暂停下载并提示用户。
MD5 校验失败:下载的文件内容与清单不匹配。应删除缓存文件并重新下载。如果连续重试 3 次仍然失败,则判定 CDN 节点异常,向服务器请求备用 CDN 域名。
元数据加载失败:LoadMetadataForAOTAssembly 返回失败码。常见原因是 DLL 版本与元数据版本不匹配。客户端应回滚到上一个稳定版本并提示用户重新启动。预防措施是在打包阶段将元数据文件的 MD5 哈希值写入版本清单,加载前先校验哈希值是否匹配。
资源加载空引用:热更新资源加载后,初始化过程中出现 NullReferenceException。通常是因为序列化数据格式与运行时预期不一致。应确保资源版本号在客户端和服务端严格对齐,第 42 篇中介绍的环境检测机制可以在此处发挥作用。
文件损坏:缓存目录中的文件被用户手动删除或被系统清理。需要在每次加载资源前执行完整性检查,缺失的文件自动进入下载队列。建议在本地维护一个"已缓存文件清单"的 JSON 文件,与服务器版本清单对比后确定需要补充的文件列表。同时,建议在每次成功加载资源后,在本地持久化记录该资源的加载次数和最近访问时间,作为后续版本清理冗余缓存文件的参考依据,避免因缓存膨胀消耗过多设备存储空间。
以上四个环节——编写、编译、打包、加载——构成了 HybridCLR 热更新流程的完整闭环。每个环节都有其特有的挑战和最佳实践,需要开发者结合项目实际情况灵活调整。核心要点可以概括为:编写时严格遵守可热更新规范(避免 AOT 泛型陷阱和反射限制),编译时确保依赖路径和元数据配置正确,打包时实现增量更新减少用户下载量,加载时做好错误处理和重试机制。第 45 篇将继续深入打包与发布环节,涵盖 iOS 和 Android 平台的构建配置和灰度发布策略;第 46 篇则将聚焦常见问题的排查与解决方案,帮助团队快速定位并解决热更新流程中的各种异常。


