鸿蒙百篇系列第 75 篇 · 专家
第 74 篇讲了灰度发布。灰度能控制风险,但有一个限制:发现问题到修复版本上架,走 AGC 审核至少要一两天。有些 Bug 等不起——闪退率超过 1%、支付页面打不开、核心功能不可用。这时候需要的是"不发版就修好"。
鸿蒙不支持传统意义上的"热修复"(替换已编译的方法体),但提供了动态模块加载能力,可以做到接近热修复的效果。这篇讲清楚鸿蒙能做什么、不能做什么、怎么做。
一、鸿蒙能做的"热修复"
先说清楚边界。以下三种方式按"侵入性从低到高"排列:
方式一:远程配置降级
最轻量的修复方式。不是真的修 Bug,而是把有 Bug 的功能关掉。
// 应用启动时拉取配置
async function initRemoteConfig() {
try {
const config = await httpClient.get<Record<string, boolean>>('/api/config');
AppStorage.setOrCreate('featureFlags', config);
} catch (err) {
// 网络失败时用本地默认值
AppStorage.setOrCreate('featureFlags', { enable_live: true, enable_pay: true });
}
}
// 业务代码根据开关决定走哪条路径
@Entry
@Component
struct HomePage {
@StorageLink('featureFlags') flags: Record<string, boolean> = {};
build() {
Column() {
if (this.flags['enable_live']) {
LiveEntryComponent() // 正常逻辑
} else {
MaintenanceComponent() // 降级提示
}
}
}
}
优点:不需要审核,配置改了用户下次启动就生效。
缺点:只能关功能,不能修逻辑。Bug 严重的模块可以关,但不影响主流程的 Bug 关不了。
方式二:HSP 动态模块
HSP(Harmony Shared Package)是运行时加载的共享包。可以把有 Bug 的模块做成 HSP,修复后重新编译 HSP 包,通过服务端下发给用户。
第 68 篇讲过包管理机制,HSP 是鸿蒙的三种模块形态之一。关键特性是:HSP 可以独立于主包更新,不需要整个应用重新安装。
实现步骤:
第一步:将有问题的模块配置为 HSP
在模块的 oh-package.json5 或 build-profile.json5 中配置:
{
"name": "feature_payment",
"type": "shared",
"description": "支付模块"
}
第二步:定义模块版本
// feature_payment/oh-package.json5
{
"name": "feature_payment",
"version": "1.0.1", // 修复后升版本号
"main": "index.ets"
}
第三步:应用启动时检查模块版本
import { bundleManager } from '@kit.AbilityKit';
import { preferences } from '@kit.ArkData';
async function checkModuleUpdate() {
// 获取当前安装的模块信息
const bundleInfo = await bundleManager.getBundleInfo('com.example.app',
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_HAP_MODULE);
// 检查支付模块版本
const currentVersion = getModuleVersion(bundleInfo, 'feature_payment');
const latestVersion = await fetchServerModuleVersion('feature_payment');
if (compareVersion(currentVersion, latestVersion) < 0) {
// 需要更新
await downloadAndUpdateModule('feature_payment', latestVersion);
}
}
async function downloadAndUpdateModule(moduleName: string, version: string) {
// 1. 从服务端下载 HSP 包
const downloadUrl = `https://cdn.example.com/modules/${moduleName}_${version}.hsp`;
const filePath = await downloadFile(downloadUrl);
// 2. 安装 HSP 包(需要系统权限或用户确认)
// 注意:应用内安装 HSP 需要通过系统接口,具体可用性取决于应用权限
try {
await installHspModule(filePath);
console.info('模块更新成功');
} catch (err) {
console.error('模块更新失败: ' + (err as Error).message);
// 降级处理
}
}
第四步:动态加载模块
import { router } from '@kit.ArkUI';
// 跳转到 HSP 模块中的页面
function gotoPaymentPage() {
router.pushUrl({
url: '@bundle:com.example.app/feature_payment/ets/pages/PaymentPage'
}).then(() => {
console.info('跳转支付页面成功');
}).catch((err: BusinessError) => {
console.error('跳转失败: ' + err.message);
});
}
方式三:Feature HAP 按需更新
Feature HAP 是按需安装的模块(参考第 68 篇)。配置 deliveryWithInstall: false 的 feature 模块不会随主包安装,而是用户需要时才下载。
// feature_live/module.json5
{
"module": {
"name": "feature_live",
"type": "feature",
"deliveryWithInstall": false, // 不随主包安装
"installationFree": false,
"deviceTypes": ["phone"],
"abilities": [
{
"name": "LiveAbility",
"srcEntry": "./ets/liveability/LiveAbility.ets"
}
]
}
}
更新流程:修复 Bug → 编译新版本 feature HAP → 上传 AGC → AGC 分发给已安装该 feature 的用户。
二、模块拆分原则
不是所有模块都适合做热修复。拆分原则:
一个模块要能热更新,它和主包之间的耦合度要低。如果支付模块直接引用了 common 层的类型定义,HSP 更新时 common 层也要兼容。否则旧 common + 新 HSP 会出现类型不匹配。
接口隔离
HSP 模块和主包之间通过接口通信,不直接引用内部实现:
// common 层定义接口
export interface IPaymentService {
pay(orderId: string, amount: number): Promise<PaymentResult>;
checkPaymentStatus(orderId: string): Promise<PaymentStatus>;
}
// feature_payment 模块实现接口
export class PaymentServiceImpl implements IPaymentService {
async pay(orderId: string, amount: number): Promise<PaymentResult> {
// 实际支付逻辑
}
async checkPaymentStatus(orderId: string): Promise<PaymentStatus> {
// 查询逻辑
}
}
主包通过接口调用,不关心实现是哪个版本。HSP 更新时只要接口签名不变,主包不需要改。
三、版本兼容性管理
HSP 动态更新最怕的是版本不兼容。新版 HSP 用了新接口,旧版主包调不了;旧版 HSP 被新版主包调用,缺了新参数。
版本协商机制
// HSP 模块导出版本信息
export const MODULE_VERSION = '1.2.0';
export const SUPPORTED_API_VERSIONS = ['1.0', '1.1', '1.2'];
// 主包调用前检查兼容性
function checkCompatibility(hspModule: any): boolean {
const supportedVersions: string[] = hspModule.SUPPORTED_API_VERSIONS;
const requiredVersion = '1.1';
return supportedVersions.includes(requiredVersion);
}
async function callPayment(hspModule: any, orderId: string, amount: number) {
if (!checkCompatibility(hspModule)) {
// 不兼容,降级到旧逻辑或提示用户更新
throw new Error('支付模块版本不兼容,请更新应用');
}
return hspModule.pay(orderId, amount);
}
向下兼容原则
HSP 模块的接口设计必须遵守向下兼容:
修复 Bug 时只能改实现,不能改接口。改接口等于改了契约,必须整个应用发新版。
四、安全更新机制
热修复涉及从服务端下载代码包并加载,安全性是核心问题。
HSP 包签名校验
import { cryptoFramework } from '@kit.CryptoArchitectureKit';
import { fs } from '@kit.CoreFileKit';
async function verifyHspSignature(filePath: string, expectedHash: string): Promise<boolean> {
// 1. 读取 HSP 文件
const file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
const stat = fs.statSync(filePath);
const buffer = new ArrayBuffer(stat.size);
fs.readSync(file.fd, buffer);
fs.closeSync(file);
// 2. 计算 SHA-256 摘要
const md = cryptoFramework.createMd('SHA256');
md.update({ data: buffer });
const digest = md.digest();
const actualHash = Array.from(new Uint8Array(digest.data))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
// 3. 对比预期摘要
return actualHash === expectedHash;
}
async function downloadAndUpdateModule(moduleName: string, version: string) {
const downloadUrl = `https://cdn.example.com/modules/${moduleName}_${version}.hsp`;
// 从服务端获取预期哈希值(通过 HTTPS)
const expectedHash = await fetchModuleHash(moduleName, version);
// 下载 HSP 文件
const filePath = `${context.filesDir}/${moduleName}_${version}.hsp`;
await downloadFile(downloadUrl, filePath);
// 验签
const isValid = await verifyHspSignature(filePath, expectedHash);
if (!isValid) {
throw new Error('模块签名校验失败,可能被篡改');
}
// 验签通过后安装
await installHspModule(filePath);
}
更新流程安全检查清单
五、热修复适用场景
六、降级和容错
热修复失败时不能影响用户正常使用:
async function safeLoadModule(moduleName: string): Promise<any> {
try {
// 尝试加载最新 HSP
const module = await loadHspModule(moduleName);
return module;
} catch (err) {
console.warn('HSP 加载失败,使用内置版本: ' + (err as Error).message);
// 降级使用打包时内置的旧版本
try {
const fallback = await loadBuiltinModule(moduleName);
return fallback;
} catch (fallbackErr) {
// 内置版也挂了,返回空实现
return createNoopModule(moduleName);
}
}
}
// 空实现,防止调用报错
function createNoopModule(moduleName: string): any {
return new Proxy({}, {
get: () => () => {
throw new Error(`模块 ${moduleName} 不可用,请更新应用`);
}
});
}
七、小结
鸿蒙的热修复本质上是"动态模块加载 + 版本协商 + 安全校验"。你不能像 Android 那样直接替换一个方法的字节码,但可以通过合理的模块拆分,让需要热修复的部分变成可独立更新的 HSP 模块。
前提是在架构设计阶段(第 71 篇)就把模块边界划清楚。等出了 Bug 再想热修复,发现代码全塞在 entry 里,那就只能走灰度发新版。
下一篇讲跨平台工程实践——从纯鸿蒙应用到多端复用的选型决策。