
1. 项目概述为什么热更新是Cocos Creator项目的生命线如果你正在用Cocos Creator开发游戏尤其是面向移动端的原生游戏那么“热更新”这个词对你来说绝对不是一个可选项而是项目上线后必须掌握的生存技能。想象一下这个场景你的游戏刚上线玩家热情高涨但突然发现了一个致命的Bug——一个导致游戏崩溃的脚本错误或者一个严重影响平衡的道具数值。如果没有热更新你只能眼睁睁看着差评如潮然后心急火燎地准备新包提交给各个渠道重新审核等待短则一天、长则数周的审核周期。等新包终于过审玩家可能已经流失大半了。热更新技术就是解决这个痛点的“后悔药”和“强心针”。它允许你在不重新提交应用商店审核、不要求玩家重新下载完整安装包的情况下通过网络将修复后的代码、资源、配置等推送到玩家设备上。对于Cocos Creator v2.3.0这个依然被大量项目使用的稳定版本来说虽然官方没有在编辑器里提供一键式的热更新解决方案但其底层引擎Cocos2d-x的AssetsManager模块已经提供了完整的能力。这意味着我们完全可以通过一些额外的脚本和流程设计为自己的项目搭建一套可靠的热更新机制。这套机制的核心价值在于“敏捷”和“可控”。你可以快速响应线上问题可以小步快跑地迭代新功能也可以通过热更新进行活动玩法的投放。本质上它把游戏客户端从一个“静态”的发布物变成了一个可以“动态”生长的在线服务。接下来我将为你彻底拆解在Cocos Creator v2.3.0中实现热更新的完整技术栈、实操步骤以及那些官方文档里不会写的“坑”和技巧。2. 核心原理与架构设计AssetsManager是如何工作的在开始写代码之前我们必须先吃透热更新的核心原理。Cocos Creator的热更新能力继承自Cocos2d-x的AssetsManager模块。理解它的工作模式是避免后续采坑的关键。2.1 差异对比更新不是全量替换很多新手会误以为热更新就是下载一个新版本覆盖安装。其实不然AssetsManager采用的是差异对比更新机制。它的设计非常巧妙完整版本镜像无论是服务器还是用户本地都保存着完整版本的游戏资源。服务器上存着最新版本例如1.1.0的所有文件用户设备上存着当前版本例如1.0.0的所有文件。Manifest清单文件每个版本都对应一个project.manifest文件。这个JSON文件就像是这个版本的“身份证”和“文件目录”它记录了该版本所有文件的路径及其对应的版本标识通常使用文件的MD5校验码或自定义版本号。更新决策过程当客户端启动时会去服务器检查最新的version.manifest一个简化的清单只包含版本号或project.manifest。通过对比本地和远程的Manifest文件引擎可以精确计算出哪些文件有变动新增、修改、删除。按需下载引擎只会下载那些有变动的文件并用它们替换掉本地旧版本的文件。这个过程完成后本地的文件集合就“变成”了最新版本。这种设计的巨大优势在于支持跨版本更新。比如用户本地是1.0.0服务器最新是1.2.0。你不需要依次下载1.0.0-1.1.0和1.1.0-1.2.0的更新包引擎可以直接计算出1.0.0与1.2.0之间的所有文件差异一次下载到位。这大大简化了版本维护的复杂度。2.2 原生平台专属为什么Web平台不需要这里有一个至关重要的限制这套基于AssetsManager的热更新机制仅适用于原生平台如Android、iOS、Windows等不适用于Web平台。原因在于运行环境的根本差异Web平台游戏资源HTML、JS、图片等本身就托管在服务器上。每次用户访问浏览器都会根据缓存策略加载资源。更新游戏只需要替换服务器文件用户刷新页面即可获得新内容。这本身就是一种“天然”的热更新。原生平台游戏资源被打包在安装包内安装在用户的设备存储中。应用启动后默认从本地存储读取资源。要想更新必须有一套机制能从网络下载新资源并让游戏运行时优先读取这些新资源。因此当我们讨论Cocos Creator热更新时默认语境就是针对原生发布包。AssetsManager类也只存在于jsbJavaScript Binding命名空间下在浏览器环境中是无法调用的。2.3 搜索路径Search Paths的魔法差异文件下载到本地后放在哪里游戏又如何知道去读取它们而不是读取安装包里的旧文件这是热更新实现的另一个核心——搜索路径重定向。Cocos2d-x引擎有一个FileUtils模块它负责所有文件的读写操作。它有一个“搜索路径”列表。当引擎需要读取一个文件比如assets/images/hero.png时它会按照搜索路径列表的顺序依次查找使用第一个找到的文件。热更新正是利用了这个机制更新开始时将下载的远程资源存放到一个特定目录例如{可写路径}/mygame_remote/。更新成功后将这个远程资源目录插入到搜索路径列表的最前面。此后当游戏尝试读取assets/images/hero.png时会先在{可写路径}/mygame_remote/assets/images/下寻找如果找到就直接使用这就是热更新后的新文件如果没找到才会去安装包内的原始路径寻找。这样我们就实现了对原始资源的“覆盖”而无需物理删除或移动安装包内的文件。3. 实战准备搭建热更新演示工程与环境理论讲完了我们动手搭建一个最小化的可运行示例。我会带你走一遍完整流程并解释每一个步骤的意图。3.1 创建基础工程与场景首先我们创建一个干净的Cocos Creator v2.3.0项目命名为HotUpdateDemo。创建一个简单的场景Main里面放一个按钮文本显示“检查更新”。再创建一个场景Game作为更新成功后进入的主游戏场景。为了演示效果我们在Game场景里放一张明显的图片比如一个桌子table的Sprite和一段文字“游戏主场景 - 版本1.0.0”。编写场景切换逻辑。在Main场景按钮的点击事件中先进行更新检查如果无更新或更新完成后再跳转到Game场景。这个Game场景里的table图片将是我们后续演示热更新的关键。我们计划初始发布的1.0.0版本不包含这张桌子图片。热更新1.1.0版本则包含这张图片。通过更新玩家就能从“空荡荡的房间”进入“有桌子的房间”直观感受到热更新的效果。3.2 设计项目资源结构清晰的项目结构是管理热更新的基础。我建议采用如下目录约定你的项目/ ├──assets/ │ ├──scripts/ │ │ └── HotUpdate.js // 热更新核心逻辑组件 │ ├──resources/ // 动态加载的资源可被热更新 │ ├──scene/ │ │ ├──Main.fire │ │ └──Game.fire │ └──project.manifest // **构建后**生成的本地清单文件初始为空或由工具生成 ├──build/ │ └──jsb-default/ // 原生构建输出目录 ├──remote-assets/ // **本地模拟的远程资源文件夹** │ ├──assets/ │ │ ├──resources/ │ │ └──... │ ├──src/ // 热更新的脚本也放在这里 │ └──project.manifest // 远程版本清单文件 └──version_generator.js // 我们自己写的清单生成工具脚本关键点assets/这是编辑器中的资源目录也是构建的输入目录。build/jsb-default/构建后产生的原生工程目录。我们最终发布的1.0.0安装包就来源于此。remote-assets/这是一个在开发阶段本地维护的文件夹用于模拟服务器上的最新版本1.1.0。我们将把需要热更新的文件如图片、脚本放在这里并使用工具生成对应的清单。开发完成后这个文件夹的全部内容需要上传到你的真实更新服务器。3.3 编写清单生成工具version_generator.jsproject.manifest文件是更新的依据手动编写极易出错。我们需要一个Node.js脚本来自动化生成它。这个脚本的核心工作是遍历指定目录如remote-assets下的所有文件。计算每个文件的MD5值或自定义版本标识作为该文件的“版本号”。生成一个包含packageUrl、remoteManifestUrl、remoteVersionUrl、version、assets文件列表等字段的JSON文件。这里给出一个简化版的核心逻辑你可以在此基础上扩展// version_generator.js const fs require(fs); const path require(path); const crypto require(crypto); function calculateMD5(filePath) { const buffer fs.readFileSync(filePath); const hash crypto.createHash(md5); hash.update(buffer); return hash.digest(hex); } function generateManifest(version, packageUrl, localDir, outputPath) { const manifest { packageUrl: packageUrl, // 远程资源包的根URL如 http://your-server.com/v1.1.0/ remoteManifestUrl: ${packageUrl}project.manifest, // 远程清单文件地址 remoteVersionUrl: ${packageUrl}version.manifest, // 远程版本文件地址 version: version, // 主版本号如 1.1.0 assets: {}, // 文件列表 searchPaths: [] // 搜索路径初始为空 }; // 递归遍历目录填充assets function walk(dir, root) { const files fs.readdirSync(dir); files.forEach(file { const fullPath path.join(dir, file); const stat fs.statSync(fullPath); if (stat.isDirectory()) { walk(fullPath, root); } else { // 计算相对路径作为key const relativePath path.relative(root, fullPath).replace(/\\/g, /); // 计算MD5作为版本标识 const md5 calculateMD5(fullPath); manifest.assets[relativePath] { md5: md5 }; } }); } walk(localDir, localDir); // 生成完整的project.manifest fs.writeFileSync(path.join(outputPath, project.manifest), JSON.stringify(manifest, null, 2)); // 生成简化的version.manifest (只包含关键信息用于快速版本检查) const versionManifest { packageUrl: manifest.packageUrl, remoteManifestUrl: manifest.remoteManifestUrl, remoteVersionUrl: manifest.remoteVersionUrl, // 注意这里指向自己 version: manifest.version }; fs.writeFileSync(path.join(outputPath, version.manifest), JSON.stringify(versionManifest, null, 2)); console.log(Manifest generated for version ${version} at ${outputPath}); } // 使用示例node version_generator.js -v 1.1.0 -u http://localhost:8000/ -s ./remote-assets/ -d ./ const args process.argv.slice(2); // ... 解析参数 -v, -u, -s, -d // generateManifest(parsedVersion, parsedUrl, parsedSourceDir, parsedOutputDir);这个脚本是热更新流程的“发动机”务必保证其正确性。在实际项目中你可能需要将其集成到构建流水线中。4. 热更新核心组件HotUpdate.js实现详解现在我们来编写热更新的核心逻辑。我们将创建一个HotUpdate组件并将其挂载到Main场景的某个节点上。4.1 组件初始化与状态管理首先定义组件的属性和基本状态。// assets/scripts/HotUpdate.js cc.Class({ extends: cc.Component, properties: { // 用于在UI上显示状态的Label statusLabel: cc.Label, // 用于显示进度的ProgressBar progressBar: cc.ProgressBar, // 远程版本文件的URL用于快速检查 remoteVersionUrl: http://localhost:8000/version.manifest, // 热更新临时文件存储的本地路径前缀 storagePath: , // 更新提示面板 updatePanel: cc.Node }, onLoad () { // 1. 确定可写路径 this.storagePath jsb.fileUtils.getWritablePath() /hot-update-storage/; // 2. 初始化本地存储的键名 this.localStorageKey HotUpdateSearchPaths; // 3. 设置初始状态 this.isUpdating false; this.canRetry false; this.updatePanel.active false; // 默认隐藏更新面板 // 4. **关键步骤**应用上一次更新保存的搜索路径 this._applyExistingUpdate(); },_applyExistingUpdate方法负责在游戏启动时检查本地是否已有热更新资源并应用搜索路径。这是保证热更新资源生效的关键我们稍后实现。4.2 更新流程主逻辑我们设计一个checkForUpdate方法作为更新的入口。checkForUpdate () { if (this.isUpdating) { cc.log(Update is already in progress.); return; } this.statusLabel.string 正在检查更新...; this.progressBar.progress 0; // 1. 创建AssetsManager // 注意AssetsManager仅在原生平台可用 if (typeof jsb undefined || !jsb.AssetsManager) { this.statusLabel.string 热更新仅支持原生平台。; this._enterGame(); // 直接进游戏 return; } const storagePath this.storagePath; // 本地manifest文件路径初始包内的 const localManifestPath project.manifest; // 注意这是相对于assets的路径构建后会放在对应位置 const localManifestUrl cc.url.raw(resources/project.manifest); // 一种获取raw路径的方式需根据项目调整 this.am new jsb.AssetsManager(localManifestUrl, storagePath); if (!this.am) { this.statusLabel.string 创建AssetsManager失败; return; } // 2. 配置AssetsManager this.am.setVerifyCallback(function (path, asset) { // 可选下载后的文件校验回调。这里可以用之前生成的MD5进行校验。 // 如果返回false则该文件下载失败。 // 简单实现可以只返回true。 return true; }); // 3. 设置事件监听器 this._setupEventListener(); // 4. 加载本地Manifest并开始检查远程版本 if (this.am.getLocalManifest()) { this.am.setEventCallback(this._onAMEvent.bind(this)); this.am.checkUpdate(); } else { this.am.loadLocalManifest(localManifestUrl); } },4.3 事件监听与状态处理AssetsManager的工作是异步的通过事件回调来通知进度和结果。我们必须妥善处理这些事件。_setupEventListener () { // 必须将回调绑定到当前组件实例否则this指向会出错 this.am.setEventCallback(this._onAMEvent.bind(this)); }, _onAMEvent (event) { cc.log(AssetsManager Event:, event.getEventCode(), event.getMessage()); switch (event.getEventCode()) { case jsb.AssetsManager.EventCode.ERROR_NO_LOCAL_MANIFEST: this.statusLabel.string 加载本地清单文件失败。; this._enterGame(); // 失败也尝试进入游戏 break; case jsb.AssetsManager.EventCode.ERROR_DOWNLOAD_MANIFEST: case jsb.AssetsManager.EventCode.ERROR_PARSE_MANIFEST: this.statusLabel.string 获取或解析远程清单失败。; this.canRetry true; break; case jsb.AssetsManager.EventCode.NEW_VERSION_FOUND: this.statusLabel.string 发现新版本: ${event.getMessage()}; // 显示更新确认面板 this.updatePanel.active true; break; case jsb.AssetsManager.EventCode.ALREADY_UP_TO_DATE: this.statusLabel.string 已是最新版本。; this._enterGame(); break; case jsb.AssetsManager.EventCode.UPDATE_PROGRESSION: const percent event.getPercent(); this.progressBar.progress percent / 100; this.statusLabel.string 正在下载更新... ${percent.toFixed(2)}%; // 你可以根据event.getDownloadedBytes()和event.getTotalBytes()显示更详细的信息 break; case jsb.AssetsManager.EventCode.ASSET_UPDATED: cc.log(文件更新成功: ${event.getAssetId()}); break; case jsb.AssetsManager.EventCode.ERROR_UPDATING: this.statusLabel.string 更新出错: ${event.getAssetId()} - ${event.getMessage()}; this.canRetry true; break; case jsb.AssetsManager.EventCode.UPDATE_FINISHED: this.statusLabel.string 更新完成; // **关键步骤**保存新的搜索路径并重启应用 this._onUpdateFinished(); break; case jsb.AssetsManager.EventCode.ERROR_DECOMPRESS: this.statusLabel.string 解压文件失败。; this.canRetry true; break; default: break; } },4.4 更新完成后的收尾工作当UPDATE_FINISHED事件触发时意味着所有差异文件已下载到临时目录。接下来需要做三件事将临时目录的文件移动到正式的存储目录。将正式存储目录添加到搜索路径的首位并保存这个路径列表。重启游戏以使新的搜索路径生效。_onUpdateFinished () { // 1. 获取本次更新对应的存储路径AssetsManager内部生成 const newAssetsStoragePath this.am.getLocalManifest().getPackageUrl(); // 注意getPackageUrl()返回的是本地文件路径如 file:///.../hot-update-storage/1.1.0/ // 我们需要将其转换为可用的搜索路径。 // 2. 构建新的搜索路径数组 // 引擎默认的搜索路径包含安装包内路径。我们先把新的路径加在前面。 let searchPaths jsb.fileUtils.getSearchPaths(); // 移除可能存在的旧的热更新路径避免重复 const oldHotUpdatePath this.storagePath; searchPaths searchPaths.filter(p p.indexOf(oldHotUpdatePath) -1); // 将新的存储路径插入到最前面优先级最高 searchPaths.unshift(newAssetsStoragePath); // 3. 将新的搜索路径保存到LocalStorage cc.sys.localStorage.setItem(this.localStorageKey, JSON.stringify(searchPaths)); // 4. 提示用户重启 this.statusLabel.string 更新完成即将重启应用...; // 在实际项目中这里可以显示一个“立即重启”的按钮。 // 为了演示我们延迟2秒后调用重启逻辑。 this.scheduleOnce(() { this._restartGame(); }, 2); }, _applyExistingUpdate () { // 游戏启动时调用应用已保存的搜索路径 const savedPaths cc.sys.localStorage.getItem(this.localStorageKey); if (savedPaths) { try { const paths JSON.parse(savedPaths); if (Array.isArray(paths) paths.length 0) { jsb.fileUtils.setSearchPaths(paths); cc.log(Applied saved search paths:, paths); } } catch (e) { cc.error(Failed to parse saved search paths:, e); } } }, _restartGame () { // 原生平台重启游戏的方法 if (cc.sys.os cc.sys.OS.ANDROID) { // Android: 可以通过JNI调用Java的接口来重启Activity这里简化处理为退出 cc.game.restart(); } else if (cc.sys.os cc.sys.OS.IOS || cc.sys.os cc.sys.OS.OSX) { // iOS/macOS: 直接重启游戏进程 cc.game.restart(); } else { // 其他平台如Windows cc.director.loadScene(Main); // 重新加载更新场景 } // 注意简单的cc.game.restart()在某些平台可能不能完全重置搜索路径。 // 更可靠的做法是退出游戏让用户重新启动。这里用重启场景模拟。 cc.director.loadScene(Main); }, _enterGame () { // 无更新或更新完成后进入主游戏场景 cc.director.loadScene(Game); } });4.5 构建与清单生成的关键步骤代码写好了但要让它真正工作构建和配置环节至关重要这里也是最容易出错的地方。构建第一个版本1.0.0在Cocos Creator编辑器中打开构建发布面板。选择Android或iOS等原生平台。务必注意不要勾选MD5 Cache选项。如果勾选构建出的资源文件名会附带MD5戳这会导致我们生成的project.manifest中的文件路径与实际文件名不匹配热更新完全失效。点击构建。构建完成后在build/jsb-default/目录下找到assets和src目录。这就是我们1.0.0版本的完整资源。生成1.0.0版本的初始清单我们需要为这个初始版本也生成一个project.manifest文件它描述了1.0.0版本有哪些文件。运行我们的生成工具node version_generator.js -v 1.0.0 -u http://your-server.com/v1.0.0/ -s ./build/jsb-default/assets/ -d ./assets/resources/这个命令会扫描构建输出的资源生成清单并放到assets/resources/目录下。这样当项目被打包时这个初始清单就会包含在安装包内。准备1.1.0版本的远程资源在remote-assets/目录下创建与构建输出目录类似的结构remote-assets/assets/和remote-assets/src/。将1.1.0版本新增或修改的文件复制到这里。例如把我们之前准备的table图片资源复制到remote-assets/assets/resources/textures/下。重要如果你修改了脚本.js文件需要将编译后的jsc文件位于build/jsb-default/src/下的.jsc文件复制到remote-assets/src/而不是原始的.js文件。因为原生发布版运行的是加密后的字节码。生成1.1.0版本的远程清单运行node version_generator.js -v 1.1.0 -u http://your-server.com/v1.1.0/ -s ./remote-assets/ -d ./remote-assets/这会在remote-assets/目录下生成project.manifest和version.manifest。配置本地测试服务器将整个remote-assets/目录放到一个HTTP服务器下。本地测试可以用http-server或python -m http.server。假设服务器地址是http://localhost:8000那么remote-assets/目录就应该映射到服务器的根路径使得http://localhost:8000/project.manifest能够访问到我们刚生成的文件。修改HotUpdate.js组件中remoteVersionUrl等字段的配置指向你的测试服务器地址。5. 疑难排查与实战经验总结即使按照步骤操作你也可能会遇到问题。下面是我在实践中总结的常见坑点和解决方案。5.1 常见问题排查表问题现象可能原因排查步骤与解决方案更新提示“已是最新版本”但实际服务器有新版本1.version.manifest版本号未递增。2. 本地project.manifest中的remoteVersionUrl或remoteManifestUrl地址配置错误导致根本请求不到远程清单。3. 服务器未正确部署清单文件返回404或错误内容。1. 检查远程version.manifest中的version字段是否大于本地清单中的版本号。2. 在浏览器或Postman中直接访问HotUpdate.js中配置的远程URL看是否能下载到正确的JSON文件。3. 检查服务器CORS设置确保允许跨域请求如果域名不同。更新进度卡住或失败报错网络错误1. 服务器地址不可用或网络不通。2. 更新的文件较大超时。3.packageUrl配置的路径不正确导致文件下载404。1. 用浏览器尝试下载一个更新包中的具体文件确认网络和路径。2. 适当增加AssetsManager的超时设置通过其内部_downloader对象。3. 检查project.manifest中的packageUrl它应该是远程资源文件夹的完整URL路径且以/结尾。确保assets中的文件路径能与之拼接成正确的下载地址。更新完成后新资源不生效1. 搜索路径未正确设置或保存。2. 热更新资源目录的权限问题移动端常见。3.最可能构建时勾选了MD5 Cache导致文件名带MD5后缀但清单中记录的仍是原始文件名引擎找不到文件。1. 在_onUpdateFinished中打印出newAssetsStoragePath和保存的searchPaths确认路径正确且被保存到LocalStorage。2. 检查jsb.fileUtils.getWritablePath()返回的路径是否可写。3.彻底检查重新构建绝对不要勾选MD5 Cache。清理手机上的App数据和缓存重新安装测试。更新后游戏崩溃或脚本错误1. 热更新的脚本.jsc与当前运行的引擎版本不兼容。2. 更新的脚本存在语法或逻辑错误。3. 资源依赖关系断裂比如A脚本引用了B图片但B图片没有被打进更新包。1. 确保热更新使用的引擎版本与主包一致。不要用v2.3.0的包去更新v2.4.0的项目。2. 在本地充分测试更新包。可以先将更新包放到本地搜索路径进行调试。3. 使用Creator的“构建”面板中的“参与构建”选项来管理资源确保依赖资源被正确收集。清单生成工具应遍历整个资源目录避免遗漏。iOS平台更新失败提示权限问题iOS对应用沙盒内文件访问有严格限制。getWritablePath返回的路径可能在更新后权限不足。确保使用的路径在应用沙盒的Documents或Library目录下。Cocos2d-x的getWritablePath通常返回的是Documents目录是可行的。检查是否在更新过程中尝试写入系统保护区。5.2 必须牢记的实战心得版本号管理是灵魂version.manifest里的版本号必须是纯数字或点分数字如1.2.3并且远程版本号必须大于本地版本号才能触发更新。建议使用语义化版本号并建立严格的版本发布流程。清单Manifest的完整性project.manifest里的assets字段必须包含所有需要被版本管理的文件不仅是新增的也包括未修改的。我们的生成工具是遍历整个remote-assets目录来生成的这保证了完整性。如果你手动维护这个列表几乎一定会出错。测试测试再测试本地模拟测试在开发阶段可以将remote-assets直接放在项目的build/jsb-default/目录下模拟服务器然后修改本地project.manifest的远程地址指向本地文件file:///...这样可以不依赖网络进行流程测试。差分测试务必测试从1.0.0到1.1.0再从1.1.0到1.2.0的连续更新是否正常。回滚测试思考如果1.2.0版本有严重问题如何让用户回退到1.1.0一种方案是在更新逻辑中加入版本验证如果新版本启动失败则自动清除该版本的搜索路径回退到上一个版本。关于脚本热更的特别警告热更新JavaScript脚本风险较高。因为脚本逻辑复杂容易引发崩溃。如果只是更新资源、配置表风险则小很多。对于重大功能更新如果涉及大量脚本改动建议强制引导用户下载新安装包。热更新更适合用于修复紧急Bug、微调数值、投放小型活动。资源管理对于大量小文件考虑在更新前将它们打包成.zip文件下载后解压可以减少HTTP请求数量提升更新效率。但需要自己实现打包和解压逻辑。用户体验增量更新AssetsManager本身是增量更新但你要确保服务器上始终保留每个历史版本的文件吗通常的做法是服务器只保留最新完整版由客户端计算增量。这要求客户端本地必须有完整的上一版本文件我们的方案符合这一点。断点续传基础的AssetsManager不支持。对于大文件更新可以考虑集成更强大的下载库。更新提示一定要在UI上清晰告知用户更新大小、进度并提供“稍后提醒”或“在Wi-Fi下自动更新”的选项。为Cocos Creator v2.3.0项目实现一套健壮的热更新系统初期需要投入一些时间进行搭建和测试但一旦跑通它将为你的项目运维带来巨大的灵活性和安全感。这套基于AssetsManager的方案虽然需要手动集成但让你对更新流程有了完全的控制力。在后续的Cocos Creator版本中官方提供了更集成的Asset Bundle系统其思想一脉相承但使用起来更加便捷。理解v2.3.0的这套底层机制会让你在面对任何版本的热更新问题时都能做到心中有数手中有策。