HarmonyOS APP开发中的应用更新:OTA更新、增量更新与热修复全链路实战

举报
Jack20 发表于 2026/06/20 18:28:33 2026/06/20
【摘要】 HarmonyOS APP开发中的应用更新:OTA更新、增量更新与热修复全链路实战📌 核心要点:掌握 @ohos.update 的 OTA 全量/增量更新机制,理解热修复的原理与限制,实现版本检测、静默更新与强制更新的完整方案。 一、背景与动机你刚上线了一个新版本,修复了一个关键 Bug,但三天过去了,还有 30% 的用户停留在旧版本。他们不断投诉"怎么还有这个问题",你只能无奈地回复"...

HarmonyOS APP开发中的应用更新:OTA更新、增量更新与热修复全链路实战

📌 核心要点:掌握 @ohos.update 的 OTA 全量/增量更新机制,理解热修复的原理与限制,实现版本检测、静默更新与强制更新的完整方案。


一、背景与动机

你刚上线了一个新版本,修复了一个关键 Bug,但三天过去了,还有 30% 的用户停留在旧版本。他们不断投诉"怎么还有这个问题",你只能无奈地回复"请更新到最新版本"。这种"更新焦虑"每个开发者都经历过。

应用更新看似简单——下载新包、安装、重启——但在 HarmonyOS 的生态中,它远比你想的复杂。全量更新包太大,几百 MB 的下载量让用户望而却步;增量更新虽然小,但差分算法有版本限制;热修复听起来很美好,但能修的范围有限;强制更新又容易引发用户反感……

更关键的是,HarmonyOS 的应用更新机制与 Android 有本质区别。它不依赖 Google Play 的更新服务,而是有自己的 OTA 框架。理解这个框架的工作原理,是做好应用更新的前提。


二、核心原理

2.1 应用更新方式对比

graph TB
    A[应用更新方式] --> B[OTA全量更新]
    A --> C[OTA增量更新]
    A --> D[热修复]
    
    B --> B1[下载完整HAP]
    B1 --> B2[替换安装]
    B2 --> B3[重启应用]
    
    C --> C1[下载差分包]
    C1 --> C2[本地合成新包]
    C2 --> C3[校验完整性]
    C3 --> C4[替换安装]
    
    D --> D1[下载补丁文件]
    D1 --> D2[动态加载补丁]
    D2 --> D3[无需重启]
    
    classDef primary fill:#4CAF50,stroke:#388E3C,color:#fff
    classDef warning fill:#FF9800,stroke:#F57C00,color:#fff
    classDef error fill:#F44336,stroke:#D32F2F,color:#fff
    classDef info fill:#2196F3,stroke:#1976D2,color:#fff
    classDef purple fill:#9C27B0,stroke:#7B1FA2,color:#fff
    
    class A primary
    class B,B1,B2,B3 info
    class C,C1,C2,C3,C4 warning
    class D,D1,D2,D3 purple

三种更新方式对比

特征 OTA全量更新 OTA增量更新 热修复
更新包大小 完整HAP(大) 差分包(小) 补丁文件(极小)
下载耗时 极短
是否需要重启
修复范围 任意修改 任意修改 仅逻辑修复
版本限制 需从指定版本升级 需预先集成框架
安全性 高(完整校验) 高(合成后校验) 中(需信任补丁)
类比 换整辆车 换发动机 换机油

2.2 OTA 更新完整流程

sequenceDiagram
    participant App as 应用
    participant Server as 更新服务器
    participant Update as @ohos.update
    participant System as 系统
    
    App->>Server: 请求版本信息(当前版本号+包名)
    Server-->>App: 返回更新信息(新版本号+下载地址+更新策略)
    
    alt 有新版本
        App->>App: 判断更新策略(静默/提示/强制)
        
        alt 静默更新
            App->>Update: download() 下载更新包
        else 提示更新
            App->>App: 显示更新提示弹窗
            alt 用户同意
                App->>Update: download() 下载更新包
            else 用户拒绝
                App->>App: 记录拒绝,稍后重试
            end
        else 强制更新
            App->>App: 显示强制更新弹窗(不可关闭)
            App->>Update: download() 下载更新包
        end
        
        Update->>Server: 下载更新包
        Server-->>Update: 返回更新包数据
        Update-->>App: 下载进度回调
        
        Update->>Update: 校验包完整性
        Update->>System: upgrade() 安装更新
        System-->>App: 安装完成回调
        App->>App: 重启应用
    else 无新版本
        App->>App: 当前已是最新版本
    end
    
    classDef primary fill:#4CAF50,stroke:#388E3C,color:#fff
    classDef warning fill:#FF9800,stroke:#F57C00,color:#fff
    classDef error fill:#F44336,stroke:#D32F2F2,color:#fff
    classDef info fill:#2196F3,stroke:#1976D2,color:#fff
    classDef purple fill:#9C27B0,stroke:#7B1FA2,color:#fff

2.3 增量更新原理

增量更新的核心是差分算法:将新版本和旧版本进行二进制差分,只传输差异部分。

旧版本 (v1.0.0)          新版本 (v1.1.0)
┌──────────────┐         ┌──────────────┐
│  HAPA    │   →→→   │  HAPB    │
└──────────────┘         └──────────────┘
         │                        │
         └──────── bsdiff ────────┘
                    │
           ┌──────────────┐
           │  差分包 Δ       (仅包含差异部分)
           │  大小远小于B  │
           └──────────────┘
                    │
         客户端: A + Δ = B (合成新版本)

限制:增量更新要求客户端必须是差分时指定的基准版本。如果用户跳过了中间版本(如从 v1.0 直接升到 v1.2),差分包可能无法合成。


三、代码实战

3.1 版本检测与更新管理器

import { update } from '@kit.UpdateKit';
import { bundleManager } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';

const TAG = '[UpdateManager]';

/**
 * 更新策略枚举
 */
export enum UpdateStrategy {
  SILENT = 0,      // 静默更新:后台下载,下次启动生效
  PROMPT = 1,      // 提示更新:弹窗提示用户
  FORCED = 2       // 强制更新:必须更新才能使用
}

/**
 * 版本信息模型
 */
export interface VersionInfo {
  versionCode: number;
  versionName: string;
  versionDescription: string;
  downloadUrl: string;
  packageSize: number;
  md5: string;
  strategy: UpdateStrategy;
  isIncremental: boolean;
  baseVersionCode: number; // 增量更新的基准版本
}

/**
 * 更新状态枚举
 */
export enum UpdateStatus {
  IDLE = 'idle',
  CHECKING = 'checking',
  DOWNLOADING = 'downloading',
  DOWNLOAD_PAUSED = 'paused',
  DOWNLOAD_COMPLETE = 'downloaded',
  INSTALLING = 'installing',
  INSTALL_COMPLETE = 'installed',
  ERROR = 'error'
}

/**
 * 应用更新管理器
 * 封装版本检测、下载、安装的完整流程
 */
export class AppUpdateManager {
  private static instance: AppUpdateManager;
  private updater: update.Updater | null = null;
  private currentVersionCode: number = 0;
  private currentVersionName: string = '';

  // 更新状态回调
  private onStatusChange?: (status: UpdateStatus, progress: number) => void;

  private constructor() {
    this.loadCurrentVersion();
  }

  static getInstance(): AppUpdateManager {
    if (!AppUpdateManager.instance) {
      AppUpdateManager.instance = new AppUpdateManager();
    }
    return AppUpdateManager.instance;
  }

  /**
   * 加载当前版本信息
   */
  private loadCurrentVersion() {
    try {
      const bundleInfo = bundleManager.getBundleInfoForSelf(
        bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT
      );
      this.currentVersionCode = bundleInfo.versionCode;
      this.currentVersionName = bundleInfo.versionName;
      hilog.info(0x0001, TAG, `当前版本: ${this.currentVersionName}(${this.currentVersionCode})`);
    } catch (err) {
      hilog.error(0x0001, TAG, `获取版本信息失败: ${JSON.stringify(err)}`);
    }
  }

  /**
   * 注册状态回调
   */
  setOnStatusChange(callback: (status: UpdateStatus, progress: number) => void) {
    this.onStatusChange = callback;
  }

  /**
   * 检查更新
   * 向服务器请求最新版本信息
   */
  async checkUpdate(): Promise<VersionInfo | null> {
    this.notifyStatus(UpdateStatus.CHECKING, 0);

    try {
      // 向更新服务器请求版本信息(示意代码)
      // 实际项目中应调用自建更新服务或应用市场API
      const versionInfo = await this.fetchVersionFromServer();

      if (!versionInfo) {
        hilog.info(0x0001, TAG, '当前已是最新版本');
        this.notifyStatus(UpdateStatus.IDLE, 0);
        return null;
      }

      // 判断是否需要更新
      if (versionInfo.versionCode <= this.currentVersionCode) {
        hilog.info(0x0001, TAG, '当前已是最新版本');
        this.notifyStatus(UpdateStatus.IDLE, 0);
        return null;
      }

      // 判断增量更新是否可用
      if (versionInfo.isIncremental) {
        if (this.currentVersionCode !== versionInfo.baseVersionCode) {
          hilog.warn(0x0001, TAG, '增量更新基准版本不匹配,将使用全量更新');
          versionInfo.isIncremental = false;
        }
      }

      hilog.info(0x0001, TAG, `发现新版本: ${versionInfo.versionName}`);
      return versionInfo;
    } catch (err) {
      hilog.error(0x0001, TAG, `检查更新失败: ${JSON.stringify(err)}`);
      this.notifyStatus(UpdateStatus.ERROR, 0);
      return null;
    }
  }

  /**
   * 下载更新包
   */
  async downloadUpdate(versionInfo: VersionInfo): Promise<boolean> {
    this.notifyStatus(UpdateStatus.DOWNLOADING, 0);

    try {
      // 创建 Updater 实例
      this.updater = update.getOnlineUpdater({
        vendor: 'com.example.app',
        moduleName: 'entry'
      });

      // 订阅下载进度
      this.updater.on('downloadProgress', (progress: update.ProgressInfo) => {
        const percent = Math.floor(progress.percent * 100);
        hilog.info(0x0001, TAG, `下载进度: ${percent}%`);
        this.notifyStatus(UpdateStatus.DOWNLOADING, percent);
      });

      // 订阅下载错误
      this.updater.on('downloadError', (error: update.UpdateResult) => {
        hilog.error(0x0001, TAG, `下载失败: ${error.errorCode}`);
        this.notifyStatus(UpdateStatus.ERROR, 0);
      });

      // 构建下载请求
      const updateInfo: update.UpdateInfo = {
        version: versionInfo.versionName,
        size: versionInfo.packageSize,
        downloadUrl: versionInfo.downloadUrl
      };

      // 执行下载
      await this.updater.download(updateInfo);

      hilog.info(0x0001, TAG, '下载完成');
      this.notifyStatus(UpdateStatus.DOWNLOAD_COMPLETE, 100);
      return true;
    } catch (err) {
      const error = err as BusinessError;
      hilog.error(0x0001, TAG, `下载更新失败: ${error.code} - ${error.message}`);
      this.notifyStatus(UpdateStatus.ERROR, 0);
      return false;
    }
  }

  /**
   * 安装更新
   */
  async installUpdate(): Promise<boolean> {
    this.notifyStatus(UpdateStatus.INSTALLING, 0);

    try {
      if (!this.updater) {
        hilog.error(0x0001, TAG, 'Updater未初始化');
        return false;
      }

      // 订阅安装进度
      this.updater.on('upgradeProgress', (progress: update.ProgressInfo) => {
        const percent = Math.floor(progress.percent * 100);
        hilog.info(0x0001, TAG, `安装进度: ${percent}%`);
        this.notifyStatus(UpdateStatus.INSTALLING, percent);
      });

      // 执行安装
      await this.updater.upgrade();

      hilog.info(0x0001, TAG, '安装完成');
      this.notifyStatus(UpdateStatus.INSTALL_COMPLETE, 100);
      return true;
    } catch (err) {
      const error = err as BusinessError;
      hilog.error(0x0001, TAG, `安装更新失败: ${error.code} - ${error.message}`);
      this.notifyStatus(UpdateStatus.ERROR, 0);
      return false;
    }
  }

  /**
   * 取消下载
   */
  async cancelDownload(): Promise<void> {
    try {
      if (this.updater) {
        // 取消下载
        this.updater.off('downloadProgress');
        this.updater.off('downloadError');
      }
      this.notifyStatus(UpdateStatus.DOWNLOAD_PAUSED, 0);
      hilog.info(0x0001, TAG, '下载已取消');
    } catch (err) {
      hilog.error(0x0001, TAG, `取消下载失败: ${JSON.stringify(err)}`);
    }
  }

  /**
   * 从服务器获取版本信息(示意代码)
   */
  private async fetchVersionFromServer(): Promise<VersionInfo | null> {
    // 实际项目中应调用自建更新服务API
    // 返回格式示例:
    return {
      versionCode: 2000000,
      versionName: '2.0.0',
      versionDescription: '1. 全新UI设计\n2. 修复已知Bug\n3. 性能优化',
      downloadUrl: 'https://update.example.com/app-v2.0.0.hap',
      packageSize: 50 * 1024 * 1024, // 50MB
      md5: 'abc123def456',
      strategy: UpdateStrategy.PROMPT,
      isIncremental: false,
      baseVersionCode: 0
    };
  }

  /**
   * 通知状态变更
   */
  private notifyStatus(status: UpdateStatus, progress: number) {
    if (this.onStatusChange) {
      this.onStatusChange(status, progress);
    }
  }

  /**
   * 获取当前版本信息
   */
  getCurrentVersion(): { code: number; name: string } {
    return {
      code: this.currentVersionCode,
      name: this.currentVersionName
    };
  }
}

3.2 更新 UI 交互页面

import { AppUpdateManager, UpdateStrategy, UpdateStatus, VersionInfo } from './AppUpdateManager';

@Entry
@Component
struct AppUpdatePage {
  // 更新管理器
  private updateManager: AppUpdateManager = AppUpdateManager.getInstance();

  // 状态
  @State currentVersion: string = '';
  @State updateStatus: UpdateStatus = UpdateStatus.IDLE;
  @State downloadProgress: number = 0;
  @State versionInfo: VersionInfo | null = null;
  @State showUpdateDialog: boolean = false;
  @State showForceDialog: boolean = false;
  @State errorMessage: string = '';

  aboutToAppear() {
    // 获取当前版本
    const version = this.updateManager.getCurrentVersion();
    this.currentVersion = version.name;

    // 注册状态回调
    this.updateManager.setOnStatusChange((status, progress) => {
      this.updateStatus = status;
      this.downloadProgress = progress;
    });

    // 自动检查更新
    this.checkForUpdate();
  }

  /**
   * 检查更新
   */
  private async checkForUpdate() {
    const versionInfo = await this.updateManager.checkUpdate();
    if (versionInfo) {
      this.versionInfo = versionInfo;

      // 根据更新策略显示不同的UI
      if (versionInfo.strategy === UpdateStrategy.FORCED) {
        this.showForceDialog = true;
      } else if (versionInfo.strategy === UpdateStrategy.PROMPT) {
        this.showUpdateDialog = true;
      } else {
        // 静默更新:直接下载
        this.startDownload();
      }
    }
  }

  /**
   * 开始下载
   */
  private async startDownload() {
    if (!this.versionInfo) return;
    await this.updateManager.downloadUpdate(this.versionInfo);
  }

  /**
   * 安装更新
   */
  private async installUpdate() {
    const success = await this.updateManager.installUpdate();
    if (success) {
      // 安装成功,提示用户重启
      this.showRestartPrompt();
    }
  }

  /**
   * 显示重启提示
   */
  private showRestartPrompt() {
    // 实际项目中应显示重启确认弹窗
    hilog.info(0x0001, 'UpdatePage', '更新安装完成,请重启应用');
  }

  build() {
    Column() {
      // 标题
      Text('应用更新')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 24 })

      // 当前版本信息
      Row() {
        Text('当前版本')
          .fontSize(14)
          .fontColor('#999999')
        Text(`v${this.currentVersion}`)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor('#333333')
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)
      .padding(16)
      .backgroundColor('#F5F5F5')
      .borderRadius(8)
      .margin({ bottom: 16 })

      // 更新状态展示
      this.UpdateStatusView()

      // 手动检查更新按钮
      if (this.updateStatus === UpdateStatus.IDLE) {
        Button('检查更新')
          .width('100%')
          .height(48)
          .backgroundColor('#4CAF50')
          .fontColor('#FFFFFF')
          .fontSize(16)
          .borderRadius(24)
          .margin({ top: 24 })
          .onClick(() => {
            this.checkForUpdate();
          })
      }
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .bindContentCover(this.showUpdateDialog, this.UpdateDialog(), {
      modalTransition: ModalTransition.DEFAULT
    })
    .bindContentCover(this.showForceDialog, this.ForceUpdateDialog(), {
      modalTransition: ModalTransition.NONE // 强制更新不可关闭
    })
  }

  @Builder
  UpdateStatusView() {
    Column() {
      when (this.updateStatus) {
        // 空闲状态
        if (this.updateStatus === UpdateStatus.IDLE && !this.versionInfo) {
          Text('当前已是最新版本')
            .fontSize(16)
            .fontColor('#4CAF50')
            .fontWeight(FontWeight.Medium)
        }
        
        // 检查中
        if (this.updateStatus === UpdateStatus.CHECKING) {
          LoadingProgress()
            .width(40)
            .height(40)
            .color('#4CAF50')
          Text('正在检查更新...')
            .fontSize(14)
            .fontColor('#666666')
            .margin({ top: 12 })
        }

        // 下载中
        if (this.updateStatus === UpdateStatus.DOWNLOADING) {
          Text(`正在下载更新 ${this.downloadProgress}%`)
            .fontSize(16)
            .fontWeight(FontWeight.Medium)
            .margin({ bottom: 12 })
          Progress({ value: this.downloadProgress, total: 100, type: ProgressType.Linear })
            .width('100%')
            .color('#4CAF50')
          Row() {
            Button('暂停')
              .height(36)
              .backgroundColor('#F5F5F5')
              .fontColor('#666666')
              .fontSize(14)
              .onClick(() => {
                this.updateManager.cancelDownload();
              })
          }
          .width('100%')
          .justifyContent(FlexAlign.End)
          .margin({ top: 12 })
        }

        // 下载完成
        if (this.updateStatus === UpdateStatus.DOWNLOAD_COMPLETE) {
          Text('下载完成,准备安装')
            .fontSize(16)
            .fontColor('#4CAF50')
            .fontWeight(FontWeight.Medium)
            .margin({ bottom: 12 })
          Button('立即安装')
            .width('100%')
            .height(44)
            .backgroundColor('#4CAF50')
            .fontColor('#FFFFFF')
            .fontSize(16)
            .borderRadius(22)
            .onClick(() => {
              this.installUpdate();
            })
        }

        // 安装中
        if (this.updateStatus === UpdateStatus.INSTALLING) {
          LoadingProgress()
            .width(40)
            .height(40)
            .color('#4CAF50')
          Text(`正在安装更新 ${this.downloadProgress}%`)
            .fontSize(14)
            .fontColor('#666666')
            .margin({ top: 12 })
        }

        // 安装完成
        if (this.updateStatus === UpdateStatus.INSTALL_COMPLETE) {
          Text('✓ 更新安装完成')
            .fontSize(16)
            .fontColor('#4CAF50')
            .fontWeight(FontWeight.Bold)
            .margin({ bottom: 12 })
          Text('请重启应用以完成更新')
            .fontSize(14)
            .fontColor('#666666')
        }

        // 错误状态
        if (this.updateStatus === UpdateStatus.ERROR) {
          Text('✗ 更新失败')
            .fontSize(16)
            .fontColor('#F44336')
            .fontWeight(FontWeight.Medium)
            .margin({ bottom: 8 })
          Text(this.errorMessage || '请检查网络连接后重试')
            .fontSize(14)
            .fontColor('#999999')
            .margin({ bottom: 12 })
          Button('重试')
            .height(36)
            .backgroundColor('#4CAF50')
            .fontColor('#FFFFFF')
            .fontSize(14)
            .onClick(() => {
              this.checkForUpdate();
            })
        }
      }
    }
    .width('100%')
    .padding(20)
    .alignItems(HorizontalAlign.Center)
  }

  @Builder
  UpdateDialog() {
    Column() {
      // 更新提示弹窗
      Text('发现新版本')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 8 })

      if (this.versionInfo) {
        Text(`v${this.versionInfo.versionName}`)
          .fontSize(16)
          .fontColor('#4CAF50')
          .margin({ bottom: 16 })

        Text(this.versionInfo.versionDescription)
          .fontSize(14)
          .fontColor('#666666')
          .lineHeight(22)
          .margin({ bottom: 24 })

        // 更新包大小提示
        Text(`更新包大小: ${(this.versionInfo.packageSize / 1024 / 1024).toFixed(1)}MB`)
          .fontSize(12)
          .fontColor('#999999')
          .margin({ bottom: 16 })
      }

      Row({ space: 12 }) {
        Button('稍后再说')
          .layoutWeight(1)
          .height(44)
          .backgroundColor('#F5F5F5')
          .fontColor('#666666')
          .fontSize(16)
          .borderRadius(22)
          .onClick(() => {
            this.showUpdateDialog = false;
          })
        Button('立即更新')
          .layoutWeight(1)
          .height(44)
          .backgroundColor('#4CAF50')
          .fontColor('#FFFFFF')
          .fontSize(16)
          .borderRadius(22)
          .onClick(() => {
            this.showUpdateDialog = false;
            this.startDownload();
          })
      }
    }
    .width('100%')
    .padding(24)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
  }

  @Builder
  ForceUpdateDialog() {
    Column() {
      // 强制更新弹窗(不可关闭)
      Text('⚠️ 需要更新')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FF9800')
        .margin({ bottom: 8 })

      Text('当前版本过旧,需要更新后才能继续使用')
        .fontSize(14)
        .fontColor('#666666')
        .margin({ bottom: 16 })

      if (this.versionInfo) {
        Text(this.versionInfo.versionDescription)
          .fontSize(14)
          .fontColor('#666666')
          .lineHeight(22)
          .margin({ bottom: 24 })
      }

      Button('立即更新')
        .width('100%')
        .height(44)
        .backgroundColor('#FF9800')
        .fontColor('#FFFFFF')
        .fontSize(16)
        .borderRadius(22)
        .onClick(() => {
          this.showForceDialog = false;
          this.startDownload();
        })
    }
    .width('100%')
    .padding(24)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
  }
}

// 导入 hilog(上方代码中使用了)
import { hilog } from '@kit.PerformanceAnalysisKit';

3.3 增量更新与热修复方案

import { update } from '@kit.UpdateKit';
import { bundleManager } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';

const TAG = '[IncrementalUpdate]';

/**
 * 增量更新管理器
 * 支持差分包下载和本地合成
 */
export class IncrementalUpdateManager {
  private static instance: IncrementalUpdateManager;

  static getInstance(): IncrementalUpdateManager {
    if (!IncrementalUpdateManager.instance) {
      IncrementalUpdateManager.instance = new IncrementalUpdateManager();
    }
    return IncrementalUpdateManager.instance;
  }

  /**
   * 检查增量更新是否可用
   * @param baseVersion 增量包的基准版本号
   * @returns 是否可以使用增量更新
   */
  canUseIncrementalUpdate(baseVersion: number): boolean {
    try {
      const bundleInfo = bundleManager.getBundleInfoForSelf(
        bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT
      );
      const currentCode = bundleInfo.versionCode;

      // 只有当前版本与基准版本完全匹配时,增量更新才可用
      if (currentCode === baseVersion) {
        hilog.info(0x0001, TAG, '增量更新可用');
        return true;
      }

      hilog.warn(0x0001, TAG, `增量更新不可用: 当前${currentCode}, 基准${baseVersion}`);
      return false;
    } catch (err) {
      hilog.error(0x0001, TAG, `检查增量更新失败: ${JSON.stringify(err)}`);
      return false;
    }
  }

  /**
   * 下载并应用增量更新
   */
  async applyIncrementalUpdate(
    patchDownloadUrl: string,
    patchSize: number,
    onProgress?: (percent: number) => void
  ): Promise<boolean> {
    try {
      const updater = update.getOnlineUpdater({
        vendor: 'com.example.app',
        moduleName: 'entry'
      });

      // 订阅下载进度
      updater.on('downloadProgress', (progress: update.ProgressInfo) => {
        const percent = Math.floor(progress.percent * 100);
        hilog.info(0x0001, TAG, `差分包下载进度: ${percent}%`);
        if (onProgress) {
          onProgress(percent);
        }
      });

      // 下载差分包
      const updateInfo: update.UpdateInfo = {
        version: 'incremental',
        size: patchSize,
        downloadUrl: patchDownloadUrl
      };

      await updater.download(updateInfo);
      hilog.info(0x0001, TAG, '差分包下载完成');

      // 安装(系统会自动合成新版本)
      await updater.upgrade();
      hilog.info(0x0001, TAG, '增量更新安装完成');

      return true;
    } catch (err) {
      const error = err as BusinessError;
      hilog.error(0x0001, TAG, `增量更新失败: ${error.code} - ${error.message}`);
      return false;
    }
  }
}

/**
 * 热修复管理器
 * 
 * ⚠️ 重要说明:
 * HarmonyOS 目前不原生支持类似 Android 的热修复框架(如 Tinker/Sophix)
 * 以下代码展示的是一种基于配置下发的"逻辑热修复"方案
 * 仅适用于修复业务逻辑错误,不能修改ArkTS代码本身
 */
export class HotFixManager {
  private static instance: HotFixManager;
  private fixConfig: Map<string, object> = new Map();

  static getInstance(): HotFixManager {
    if (!HotFixManager.instance) {
      HotFixManager.instance = new HotFixManager();
    }
    return HotFixManager.instance;
  }

  /**
   * 从服务器加载热修复配置
   * 配置中包含功能开关、参数覆盖、逻辑分支等
   */
  async loadHotFixConfig(): Promise<boolean> {
    try {
      // 从服务器获取修复配置(示意代码)
      const config = await this.fetchFixConfigFromServer();

      if (config) {
        // 解析并应用修复配置
        for (const [key, value] of Object.entries(config)) {
          this.fixConfig.set(key, value);
          // 写入 AppStorage,全局生效
          AppStorage.setOrCreate(key, value);
        }
        hilog.info(0x0001, TAG, `已加载 ${Object.keys(config).length} 项修复配置`);
        return true;
      }

      return false;
    } catch (err) {
      hilog.error(0x0001, TAG, `加载修复配置失败: ${JSON.stringify(err)}`);
      return false;
    }
  }

  /**
   * 获取修复配置值
   * 在业务代码中使用,实现逻辑热修复
   */
  getFixValue<T>(key: string, defaultValue: T): T {
    const value = this.fixConfig.get(key);
    if (value !== undefined) {
      return value as T;
    }
    return defaultValue;
  }

  /**
   * 检查功能开关
   * 通过配置下发控制功能的启用/禁用
   */
  isFeatureEnabled(featureKey: string): boolean {
    return this.getFixValue<boolean>(`feature_${featureKey}`, true);
  }

  /**
   * 获取修复后的参数
   * 通过配置下发覆盖错误的参数
   */
  getFixedParam(paramKey: string, defaultValue: string): string {
    return this.getFixValue<string>(`param_${paramKey}`, defaultValue);
  }

  /**
   * 从服务器获取修复配置(示意代码)
   */
  private async fetchFixConfigFromServer(): Promise<Record<string, object> | null> {
    // 实际项目中应调用自建修复配置服务
    // 返回格式示例:
    return {
      // 修复:禁用有Bug的功能
      'feature_advanced_edit': false,
      // 修复:覆盖错误的API地址
      'param_api_base_url': 'https://api-v2.example.com',
      // 修复:调整超时时间
      'param_timeout_ms': 30000,
      // 修复:切换到备用数据源
      'feature_use_backup_source': true
    };
  }
}

// ============ 在业务代码中使用热修复 ============

@Entry
@Component
struct HotFixAwarePage {
  @State apiBaseUrl: string = 'https://api.example.com';
  @State isAdvancedEditEnabled: boolean = true;
  @State useBackupSource: boolean = false;

  private hotFixManager: HotFixManager = HotFixManager.getInstance();

  aboutToAppear() {
    // 应用热修复配置
    this.applyHotFixConfig();
  }

  /**
   * 应用热修复配置
   */
  private applyHotFixConfig() {
    // 修复API地址
    this.apiBaseUrl = this.hotFixManager.getFixedParam('api_base_url', this.apiBaseUrl);

    // 修复功能开关
    this.isAdvancedEditEnabled = this.hotFixManager.isFeatureEnabled('advanced_edit');

    // 修复数据源切换
    this.useBackupSource = this.hotFixManager.isFeatureEnabled('use_backup_source');

    hilog.info(0x0001, TAG, `热修复配置已应用: api=${this.apiBaseUrl}`);
  }

  build() {
    Column() {
      Text('热修复演示')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 24 })

      // 显示当前修复配置
      Column({ space: 12 }) {
        this.ConfigItem('API地址', this.apiBaseUrl)
        this.ConfigItem('高级编辑', this.isAdvancedEditEnabled ? '已启用' : '已禁用(热修复)')
        this.ConfigItem('数据源', this.useBackupSource ? '备用源(热修复)' : '主数据源')
      }
      .width('100%')
    }
    .width('100%')
    .height('100%')
    .padding(16)
  }

  @Builder
  ConfigItem(label: string, value: string) {
    Row() {
      Text(label)
        .fontSize(14)
        .fontColor('#999999')
        .width(100)
      Text(value)
        .fontSize(14)
        .fontColor('#333333')
        .layoutWeight(1)
    }
    .width('100%')
    .padding(12)
    .backgroundColor('#F5F5F5')
    .borderRadius(8)
  }
}

import { hilog } from '@kit.PerformanceAnalysisKit';

四、踩坑与注意事项

4.1 增量更新的版本碎片问题

增量更新最大的坑是版本碎片。如果你的差分包是基于 v1.0 生成的,但用户已经更新到了 v1.1,那这个差分包就没法用了。

解决方案:为每个活跃版本都生成差分包。

v2.0 差分包:
  - v1.0 → v2.0 的差分包 (patch_v1_to_v2.patch)
  - v1.1 → v2.0 的差分包 (patch_v1.1_to_v2.patch)
  - v1.2 → v2.0 的差分包 (patch_v1.2_to_v2.patch)

服务器根据客户端的当前版本号返回对应的差分包。如果找不到匹配的差分包,退化为全量更新。

4.2 强制更新的用户体验

强制更新是一把双刃剑。用好了,能确保所有用户快速升级到安全版本;用不好,用户会觉得你在"绑架"他们。

最佳实践

  1. 强制更新只用于修复严重安全漏洞或合规问题
  2. 给用户一个宽限期(如 7 天),宽限期内提示但不强制
  3. 宽限期后,每次启动都弹出不可关闭的更新弹窗
  4. 提供更新包大小和 WiFi 下载提示

4.3 静默更新的时机选择

静默更新在后台下载更新包,下次启动时生效。但下载时机很重要:

// ✅ 好的时机:WiFi + 充电 + 空闲
if (isWiFi && isCharging && isIdle) {
  startSilentDownload();
}

// ❌ 坏的时机:移动数据 + 低电量
// 用户会投诉你偷跑流量、耗电

4.4 更新包校验

下载完成后必须校验更新包的完整性,防止中间人攻击或下载损坏:

// 校验MD5(示意代码)
async verifyPackage(filePath: string, expectedMd5: string): Promise<boolean> {
  // 计算下载文件的MD5
  const actualMd5 = await this.calculateMd5(filePath);
  if (actualMd5 !== expectedMd5) {
    hilog.error(0x0001, TAG, `MD5校验失败: 期望${expectedMd5}, 实际${actualMd5}`);
    return false;
  }
  return true;
}

4.5 热修复的局限性

HarmonyOS 的热修复方案(基于配置下发)有明显的局限性:

修复类型 是否支持 说明
功能开关 通过配置禁用有Bug的功能
参数覆盖 通过配置修正错误的参数
逻辑分支 ⚠️ 有限 需要预先埋好分支逻辑
ArkTS代码修复 无法动态修改已编译的代码
UI布局修复 无法动态修改布局文件
资源文件修复 无法动态替换资源

核心认知:热修复不是万能的,它只是 OTA 更新的补充。真正的代码修复还是需要走 OTA 更新流程。


五、HarmonyOS 6 适配

5.1 更新框架增强

HarmonyOS 6 对 @ohos.update 进行了重大升级:

新增能力 说明
getUpdaterForModule 支持模块级更新(只更新某个 HAP 模块)
verifyUpdatePackage 更新包签名校验接口
pauseDownload / resumeDownload 下载暂停/恢复接口
getUpdateStrategy 获取服务器下发的更新策略

5.2 模块化更新

HarmonyOS 6 支持模块级更新,不需要下载整个应用包:

// 只更新某个模块
const moduleUpdater = update.getOnlineUpdater({
  vendor: 'com.example.app',
  moduleName: 'feature_module'  // 只更新这个模块
});

await moduleUpdater.download(updateInfo);
await moduleUpdater.upgrade();

5.3 安全更新链

HarmonyOS 6 强化了更新安全链:

  1. 签名校验:更新包必须经过开发者签名
  2. 完整性校验:下载后自动校验 SHA256
  3. 来源校验:只接受来自可信源的更新包
  4. 回滚机制:安装失败自动回滚到旧版本

六、总结

mindmap
  root((应用更新))
    OTA全量更新
      下载完整HAP包
      替换安装
      需要重启
      无版本限制
    OTA增量更新
      下载差分包
      本地合成新包
      需要重启
      基准版本必须匹配
      版本碎片问题
    热修复
      配置下发
        功能开关
        参数覆盖
        逻辑分支
      局限性
        不能修改代码
        不能修改UI
        需要预先埋点
    更新策略
      静默更新
        WiFi+充电+空闲
        下次启动生效
      提示更新
        弹窗提示用户
        用户可选择拒绝
      强制更新
        不可关闭弹窗
        仅用于安全修复
        设置宽限期
    注意事项
      增量版本碎片
      强制更新体验
      静默更新时机
      更新包校验
      热修复局限性
    HarmonyOS 6
      模块级更新
      下载暂停/恢复
      签名校验增强
      安装失败回滚

核心知识点回顾

  1. 三种更新方式:全量更新(最可靠)、增量更新(最省流量)、热修复(最快但最受限)
  2. 增量更新关键:基准版本必须匹配,服务器需为每个活跃版本生成差分包
  3. 更新策略三级:静默更新(WiFi+充电)、提示更新(用户选择)、强制更新(安全修复)
  4. 热修复本质:基于配置下发的逻辑修复,不能修改代码和UI,需要预先埋点
  5. 安全校验:下载后必须校验 MD5/SHA256,防止中间人攻击
  6. HarmonyOS 6 新增:模块级更新、下载暂停/恢复、安装失败自动回滚

应用更新是连接开发者和用户的桥梁。一个好的更新体验,应该是"无感知"的——用户不需要关心版本号,不需要手动操作,一切都在后台默默完成。而一个糟糕的更新体验,会让用户觉得每次更新都是一次"冒险"。选择合适的更新策略,做好每一个细节,让更新成为提升体验的手段,而不是破坏体验的元凶。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。