Appearance
动态化与热更新:AAB、ClassLoader 与 CodePush
回到总览:混合开发与桥接相关模块:大型 App 模块化架构、SPI 机制与组件间通信 · CI/CD 流水线与 Fastlane 自动化分发
一句话定义
「动态化 / 热更新」是指不通过应用商店整包发版,就向已安装用户推送新逻辑或资源的能力:Android 侧以 AAB(App Bundle)+ Dynamic Feature 做按需下发、以 ClassLoader 插桩做 Dex 热修,跨端侧以 React Native CodePush / Flutter 资源热更做 JS/资源级 OTA——它和模块化的边界(07 页)天然咬合:能动态下发的,必须是清晰解耦、可被独立加载的模块。
代码索引
对应 Lab:dynamic-delivery-hot-update —— Dynamic Feature + ClassLoader 热修 + CodePush 思路骨架
为什么需要
- 为什么大厂都要做热更新,而不是直接发版?
- 一句话答:发版受应用商店审核周期(尤其 iOS)和全量铺开速度限制,线上紧急 bug、活动页、AB 实验来不及走整包;热更新能把「逻辑/资源」与「安装包」解耦,分钟级触达用户。
- 为什么 Android 推 AAB 后,安装包反而「变小了」?
- 一句话答:AAB 是发布格式而非最终包:Google Play 按用户设备的 ABI、语言、分辨率,现场生成只含所需资源的 split APK(base + config + feature),用户下载体积远低于一个「全量 APK」;但 AAB 本身不等于热更新,只是按需下发的基础。
- 为什么 iOS 上「热更新原生代码」基本不被允许,而 RN/Flutter 的 JS 热更可以?
- 一句话答:App Store 审核指南禁止下载并执行「原生可执行代码」;但苹果明确允许通过 JS 引擎(JavaScriptCore)执行的脚本,所以 RN 的 JS bundle、Flutter 在调试态的 JS 热更可走 OTA,原生
.o/.so的运行时替换在 iOS 上会被拒。
- 一句话答:App Store 审核指南禁止下载并执行「原生可执行代码」;但苹果明确允许通过 JS 引擎(JavaScriptCore)执行的脚本,所以 RN 的 JS bundle、Flutter 在调试态的 JS 热更可走 OTA,原生
底层机制
1. Android ClassLoader 插桩热修(Dex 插入顺序)
Tinker/Qzone 思路的本质:把补丁 Dex 插到 PathClassLoader 的 dexElements 最前面,让类加载优先命中补丁里的类。
- 关键约束
- Android 禁止替换 Application 自身及已加载的类(类一旦被加载不可卸载/替换),所以 Application 类和冷启动路径难热修。
- Dex 插入顺序决定命中优先级,补丁必须在前;顺序错会导致旧类先生效。
- 可能输出(插桩骨架示意)java
// 反射把 patch dex 的 Element 拼到 dexElements 数组头部 Object elements = combineArray(patchElements, originalElements); setField(dexPathList, "dexElements", elements); // 让补丁优先被 findClass 命中 - 预期现象:打补丁后同一类名解析到补丁里的实现,
verify/optimize走补丁路径;未覆盖的类仍走原 base。
2. AAB / Dynamic Feature 按需下发
- 可能输出(Dynamic Feature 模块
build.gradle片段)groovyapply plugin: 'com.android.dynamic-feature' // 在 base 模块的 build.gradle 中声明 // dynamicFeatures = [':feature-payment'] - 预期现象:基础包只含核心功能,重型/低频 feature(如支付、AR)在用户首次进入时才通过 Play Core 流式安装,首屏下载体积下降。
3. RN CodePush:JS Bundle 差量 OTA
CodePush 把 JS bundle 与资源做差量,客户端在启动时向服务端询问是否有新版本,命中则下载并 reload。
- 预期现象:JS 层逻辑/界面秒级更新,无需走商店;但原生侧(native module)改动仍需发版。
Android / Flutter / Web / Backend 对照
| 维度 | Android 原生 | Flutter | React Native | Web 前端 |
|---|---|---|---|---|
| 热更形态 | Tinker/类加载 Dex 插桩 | 资源热更易,Dart AOT 原生难热更 | CodePush(JS bundle OTA) | 天生热更(重新发静态资源) |
| 界限 | 不能换已加载类/Application | release 是 AOT 原生,热更受限 | 仅 JS 层,原生模块需发版 | 无原生/脚本之分 |
| 平台政策 | 国内宽松,Google Play 限制自更新 | 同 Android 政策 | JS 层允许,原生受限 | 无商店约束 |
| 与模块化关系 | 依赖清晰 SPI/模块边界 | 依赖 feature 解耦 | 依赖 js 模块边界 | 依赖代码分割 |
常见场景
- 紧急 bug 热修:支付 SDK 出现空指针,原生 Tinker 下发 Dex 补丁,几小时内覆盖全量,绕开发版审核。
- 按需加载重型模块:电商 App 把「直播」「AR 试穿」做成 Dynamic Feature,只在用户进入时下载,降低首装体积。
- 活动页 OTA:RN/Flutter JS 层活动页用 CodePush 推送,运营改文案/玩法无需发版。
常见误配、事故后果与排障
1. 误配:热修补丁覆盖了的类恰好是已加载/Application 类
- 现象:补丁下发后崩溃或「没生效」,因为类在补丁加载前已被系统
PathClassLoader加载,插桩无法替换。 - 排障与修法:把修复目标限定在非冷启动关键路径的类;Application 与启动必需类走常规发版;Tinker 用「全量替换 dex」策略规避部分限制但增大补丁。
2. 事故:iOS 端用热更偷偷替换原生逻辑被拒/下架
- 后果:审核发现下载并执行原生可执行代码,违反指南,轻则拒审重提、重则已上架版本被下架。
- 排障与修法:iOS 只走 JS/资源级热更(RN/Flutter-Debug-JS),任何
.framework/.so运行时替换都移除;原生改动一律走 App Store 发版。
3. 误配:AAB 改造后本地仍按整包思维测试,漏测 split 场景
- 现象:本地
assembleApk跑通,但 Play 内部 split 后某语言/ABI 资源缺失,部分机型功能异常。 - 排障与修法:用
bundletool build-apks+install-apks在本地按目标设备配置模拟 split 安装,验证各配置组合;CI 也用 bundletool 出包验证。
相近概念对比
| 概念 | 它解决什么 | 不解决什么 |
|---|---|---|
| AAB | 减小下载体积、按需裁剪 | 不等于热更新,仍需商店下发 |
| Dynamic Feature | 重型模块按需安装 | 首次进入需联网下载,有延迟 |
| ClassLoader 热修 | 原生 Dex 级紧急修复 | 不能换已加载类/Application |
| CodePush | JS/资源级 OTA | 原生改动仍需发版 |
对应实验
| 实验 | 说明 | 关键源码 |
|---|---|---|
| dynamic-delivery-hot-update | Play Core 拉取 Dynamic Feature + Dex 插桩骨架,观察加载顺序与命中优先级 | 代码内联于 README |
复习检查题
AAB 让安装包「变小」的原理是什么?它和热更新是一回事吗? 答:AAB 是发布格式,Google Play 根据用户设备的 ABI、语言、屏幕密度现场生成只含所需资源的 split APK,所以用户下载体积小;但它仍由商店下发,不是热更新。热更新是绕过整包发版、直接推送逻辑/资源的能力(Dex 插桩、CodePush 等),二者解决不同问题——AAB 管「怎么更小地下发」,热更新管「怎么不发版就改逻辑」。
为什么 Android 的 Dex 热修必须把补丁插到
dexElements最前面?为什么 Application 类难热修? 答:PathClassLoader.findClass按dexElements数组顺序从前到后找类,补丁放最前才能优先命中修复后的类。而类一旦被加载进虚拟机就不可卸载/替换,Application 及其冷启动关键依赖在补丁加载前早已被系统加载,所以插桩对它们无效,只能走常规发版。iOS 上为什么 RN 的 CodePush 可以、原生热更不行? 答:App Store 禁止下载并执行原生可执行代码,但允许通过 JavaScriptCore 执行脚本。CodePush 更新的是 JS bundle(脚本),落在允许范围;而替换
.framework/.so等原生二进制属于禁止的「执行下载的原生代码」,会被拒或下架。Flutter release 是 AOT 原生二进制,同理受限。
速记
- AAB≠热更:只是按需裁剪下发;Dynamic Feature 才是按需装模块。
- Dex 插桩:补丁置
dexElements头,优先命中;已加载/Application 类免谈。 - iOS 红线:原生二进制不可 OTA;只放 JS/资源级热更。
- 与模块化咬合:能热更的,必须先解耦成清晰边界的模块。