HarmonyOS开发:应用内更新InAppUpdate

举报
Jack20 发表于 2026/06/25 20:43:05 2026/06/25
【摘要】 HarmonyOS开发:应用内更新InAppUpdate📌 核心要点:应用内更新让用户不用离开应用就能完成版本升级——检测更新、下载安装包、展示进度、触发安装,一气呵成。但HarmonyOS的安全机制决定了,安装环节必须用户手动确认,你不能静默升级。 背景与动机传统的更新流程是这样的:应用检测到新版本→弹窗提示→跳转到AppGallery→用户在AppGallery里下载安装→切回应用。...

HarmonyOS开发:应用内更新InAppUpdate

📌 核心要点:应用内更新让用户不用离开应用就能完成版本升级——检测更新、下载安装包、展示进度、触发安装,一气呵成。但HarmonyOS的安全机制决定了,安装环节必须用户手动确认,你不能静默升级。

背景与动机

传统的更新流程是这样的:应用检测到新版本→弹窗提示→跳转到AppGallery→用户在AppGallery里下载安装→切回应用。

这个流程有什么问题?用户流失。每多一步跳转,就有一部分用户放弃更新。尤其是那些网络不太好的用户,跳到AppGallery发现要下载100多MB,直接关了。

应用内更新(InAppUpdate)解决了这个问题。用户不用离开你的应用,在应用内就能完成下载,下载完一键安装。流程更短,转化率更高。

但要注意:HarmonyOS的安全机制不允许应用静默安装。下载完了,还得弹一个系统级的安装确认弹窗,用户点了"安装"才会真正更新。这是安全底线,不能绕过。

这篇文章,把InAppUpdate的API使用、下载管理、进度展示、安装触发全部讲清楚。

核心原理

InAppUpdate完整流程

graph TD
    A[应用启动/手动检查] --> B[调用InAppUpdate API检测]
    B --> C{有新版本?}
    C -->|| D[正常使用]
    C -->|| E[获取更新信息]
    E --> F[展示更新弹窗]
    F --> G{用户选择}
    G -->|拒绝| D
    G -->|同意更新| H[创建下载任务]
    H --> I[下载安装包]
    I --> J[展示下载进度]
    J --> K{下载完成?}
    K -->|失败| L[展示错误提示]
    L --> M{重试?}
    M -->|| H
    M -->|| D
    K -->|成功| N[触发安装]
    N --> O[系统安装确认弹窗]
    O --> P{用户确认?}
    P -->|| Q[安装并重启]
    P -->|| D

    classDef startStyle fill:#FF6B35,stroke:#D4551F,color:#fff,font-weight:bold
    classDef processStyle fill:#4ECDC4,stroke:#3BA99C,color:#fff
    classDef decisionStyle fill:#FFE66D,stroke:#D4B93C,color:#333,font-weight:bold
    classDef dangerStyle fill:#FF6B6B,stroke:#CC5555,color:#fff
    classDef successStyle fill:#96CEB4,stroke:#6DAF8E,color:#fff,font-weight:bold
    
    class A,B startStyle
    class D,E,F,H,I,J,N processStyle
    class C,G,K,M,O,P decisionStyle
    class L dangerStyle
    class Q successStyle

InAppUpdate vs 传统更新

维度 InAppUpdate 传统跳转AppGallery
用户离开应用 不需要 需要
下载进度展示 应用内自定义 AppGallery标准UI
下载中断恢复 可自定义 AppGallery处理
安装确认 系统弹窗 AppGallery处理
审核要求 需要说明更新机制 无特殊要求
适用场景 大型应用、高频更新 小型应用、低频更新

代码实战

基础用法:检测更新

// entry/src/main/ets/utils/InAppUpdateManager.ets
// 应用内更新管理器——检测、下载、安装一站式管理

import { hilog } from '@kit.PerformanceAnalysisKit';
import { common } from '@kit.AbilityKit';
import { bundleManager } from '@kit.AbilityKit';

// 更新信息
interface AppUpdateInfo {
  versionCode: number;          // 新版本号
  versionName: string;          // 新版本名
  updateLog: string;            // 更新日志
  packageSize: number;          // 安装包大小(字节)
  downloadUrl: string;          // 下载地址
  md5: string;                  // 安装包MD5校验
  isForceUpdate: boolean;       // 是否强制更新
  minSupportVersionCode: number; // 最低支持版本
}

// 更新状态
enum UpdateStatus {
  IDLE = 'idle',                 // 空闲
  CHECKING = 'checking',         // 检查中
  AVAILABLE = 'available',       // 有更新可用
  DOWNLOADING = 'downloading',   // 下载中
  DOWNLOADED = 'downloaded',     // 已下载
  INSTALLING = 'installing',     // 安装中
  FAILED = 'failed',            // 失败
  CANCELED = 'canceled'         // 已取消
}

export class InAppUpdateManager {
  private static instance: InAppUpdateManager;
  private context: common.Context | null = null;
  private updateInfo: AppUpdateInfo | null = null;
  private status: UpdateStatus = UpdateStatus.IDLE;
  private onStatusChange?: (status: UpdateStatus) => void;

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

  // 初始化
  async init(context: common.Context): Promise<void> {
    this.context = context;
    hilog.info(0x0000, 'InAppUpdate', '应用内更新管理器初始化完成');
  }

  // 检查更新
  async checkForUpdate(): Promise<{
    hasUpdate: boolean;
    updateInfo: AppUpdateInfo | null;
  }> {
    this.setStatus(UpdateStatus.CHECKING);

    try {
      // 获取当前版本
      const bundleInfo = await bundleManager.getBundleInfoForSelf(
        bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT
      );
      const currentVersionCode = bundleInfo.versionCode;

      // 从服务端获取更新信息
      const serverInfo = await this.fetchUpdateInfo();

      if (!serverInfo || serverInfo.versionCode <= currentVersionCode) {
        this.setStatus(UpdateStatus.IDLE);
        return { hasUpdate: false, updateInfo: null };
      }

      this.updateInfo = serverInfo;
      this.setStatus(UpdateStatus.AVAILABLE);

      hilog.info(0x0000, 'InAppUpdate', 
        `发现新版本: ${serverInfo.versionName}(${serverInfo.versionCode})`);

      return { hasUpdate: true, updateInfo: serverInfo };
    } catch (error) {
      hilog.error(0x0000, 'InAppUpdate', `检查更新失败: ${JSON.stringify(error)}`);
      this.setStatus(UpdateStatus.FAILED);
      return { hasUpdate: false, updateInfo: null };
    }
  }

  // 获取更新信息
  getUpdateInfo(): AppUpdateInfo | null {
    return this.updateInfo;
  }

  // 获取当前状态
  getStatus(): UpdateStatus {
    return this.status;
  }

  // 设置状态变更回调
  setOnStatusChange(callback: (status: UpdateStatus) => void): void {
    this.onStatusChange = callback;
  }

  // 从服务端获取更新信息(模拟)
  private async fetchUpdateInfo(): Promise<AppUpdateInfo | null> {
    // 实际项目中通过HTTP请求获取
    return {
      versionCode: 10500,
      versionName: '1.5.0',
      updateLog: '1. 修复了数据同步异常的问题\n2. 优化了页面加载速度\n3. 新增了深色模式支持',
      packageSize: 85 * 1024 * 1024, // 85MB
      downloadUrl: 'https://appgallery.huawei.cn/download/com.example.myapp',
      md5: 'a1b2c3d4e5f6g7h8i9j0',
      isForceUpdate: false,
      minSupportVersionCode: 10300
    };
  }

  // 更新状态
  private setStatus(status: UpdateStatus): void {
    const oldStatus = this.status;
    this.status = status;
    if (oldStatus !== status) {
      hilog.info(0x0000, 'InAppUpdate', `状态变更: ${oldStatus}${status}`);
      this.onStatusChange?.(status);
    }
  }
}

进阶用法:下载管理与进度展示

下载是InAppUpdate的核心环节。大文件下载需要考虑断点续传、网络切换、进度展示等问题。

// entry/src/main/ets/utils/UpdateDownloader.ets
// 更新下载器——支持断点续传、进度回调、网络切换处理

import { hilog } from '@kit.PerformanceAnalysisKit';
import { common } from '@kit.AbilityKit';
import { request } from '@kit.BasicServicesKit';
import { fileIo as fs } from '@kit.CoreFileKit';

// 下载进度信息
interface DownloadProgress {
  downloadedSize: number;   // 已下载大小(字节)
  totalSize: number;        // 总大小(字节)
  progress: number;         // 进度百分比(0-100)
  speed: number;            // 下载速度(字节/秒)
  remainingTime: number;    // 预计剩余时间(秒)
}

// 下载配置
interface DownloadConfig {
  url: string;              // 下载地址
  fileName: string;         // 保存文件名
  header?: Record<string, string>; // 自定义请求头
  enableMetered: boolean;   // 是否允许计费网络下载
  enableRoaming: boolean;   // 是否允许漫游下载
}

export class UpdateDownloader {
  private context: common.Context | null = null;
  private downloadTask: request.DownloadTask | null = null;
  private isDownloading: boolean = false;
  private startTime: number = 0;
  private lastProgressTime: number = 0;
  private lastDownloadedSize: number = 0;

  // 回调
  onProgress?: (progress: DownloadProgress) => void;
  onComplete?: (filePath: string) => void;
  onError?: (error: string) => void;

  // 初始化
  init(context: common.Context): void {
    this.context = context;
  }

  // 开始下载
  async startDownload(config: DownloadConfig): Promise<void> {
    if (this.isDownloading) {
      hilog.warn(0x0000, 'Downloader', '已有下载任务在运行');
      return;
    }

    if (!this.context) {
      this.onError?.('上下文未初始化');
      return;
    }

    try {
      // 构建下载保存路径
      const filesDir = this.context.filesDir;
      const filePath = `${filesDir}/update/${config.fileName}`;

      // 确保目录存在
      const dirPath = `${filesDir}/update`;
      if (!fs.accessSync(dirPath)) {
        fs.mkdirSync(dirPath);
      }

      // 如果文件已存在,删除旧文件
      if (fs.accessSync(filePath)) {
        fs.unlinkSync(filePath);
      }

      // 创建下载任务
      const downloadConfig: request.DownloadConfig = {
        url: config.url,
        filePath: filePath,
        header: config.header || {},
        enableMetered: config.enableMetered,
        enableRoaming: config.enableRoaming,
        description: '应用更新包',
        networkType: config.enableMetered ? 1 : 0, // 0=WiFi, 1=任何网络
        title: '应用更新'
      };

      this.downloadTask = await request.downloadFile(this.context, downloadConfig);
      this.isDownloading = true;
      this.startTime = Date.now();
      this.lastProgressTime = Date.now();

      hilog.info(0x0000, 'Downloader', `下载任务已创建: ${config.url}`);

      // 监听下载进度
      this.downloadTask.on('progress', (receivedSize: number, totalSize: number) => {
        this.handleProgress(receivedSize, totalSize);
      });

      // 监听下载完成
      this.downloadTask.on('complete', () => {
        this.handleComplete(filePath);
      });

      // 监听下载失败
      this.downloadTask.on('fail', (err: number) => {
        this.handleError(`下载失败,错误码: ${err}`);
      });

    } catch (error) {
      this.handleError(`创建下载任务失败: ${JSON.stringify(error)}`);
    }
  }

  // 暂停下载
  async pauseDownload(): Promise<void> {
    if (!this.downloadTask || !this.isDownloading) return;

    try {
      // HarmonyOS的downloadTask支持suspend
      await this.downloadTask.suspend();
      this.isDownloading = false;
      hilog.info(0x0000, 'Downloader', '下载已暂停');
    } catch (error) {
      hilog.error(0x0000, 'Downloader', `暂停下载失败: ${JSON.stringify(error)}`);
    }
  }

  // 恢复下载
  async resumeDownload(): Promise<void> {
    if (!this.downloadTask || this.isDownloading) return;

    try {
      await this.downloadTask.resume();
      this.isDownloading = true;
      this.lastProgressTime = Date.now();
      hilog.info(0x0000, 'Downloader', '下载已恢复');
    } catch (error) {
      hilog.error(0x0000, 'Downloader', `恢复下载失败: ${JSON.stringify(error)}`);
    }
  }

  // 取消下载
  async cancelDownload(): Promise<void> {
    if (!this.downloadTask) return;

    try {
      await this.downloadTask.remove();
      this.isDownloading = false;
      this.downloadTask = null;
      hilog.info(0x0000, 'Downloader', '下载已取消');
    } catch (error) {
      hilog.error(0x0000, 'Downloader', `取消下载失败: ${JSON.stringify(error)}`);
    }
  }

  // 处理下载进度
  private handleProgress(receivedSize: number, totalSize: number): void {
    const now = Date.now();
    const elapsed = (now - this.lastProgressTime) / 1000; // 秒
    const downloadedDelta = receivedSize - this.lastDownloadedSize;
    
    // 计算下载速度(平滑处理,避免抖动)
    const speed = elapsed > 0 ? downloadedDelta / elapsed : 0;
    
    // 计算剩余时间
    const remainingSize = totalSize - receivedSize;
    const remainingTime = speed > 0 ? remainingSize / speed : 0;

    const progress: DownloadProgress = {
      downloadedSize: receivedSize,
      totalSize: totalSize,
      progress: totalSize > 0 ? Math.floor((receivedSize / totalSize) * 100) : 0,
      speed: speed,
      remainingTime: Math.ceil(remainingTime)
    };

    this.lastProgressTime = now;
    this.lastDownloadedSize = receivedSize;

    this.onProgress?.(progress);
  }

  // 处理下载完成
  private handleComplete(filePath: string): void {
    this.isDownloading = false;
    this.downloadTask = null;
    hilog.info(0x0000, 'Downloader', `下载完成: ${filePath}`);
    this.onComplete?.(filePath);
  }

  // 处理下载错误
  private handleError(message: string): void {
    this.isDownloading = false;
    this.downloadTask = null;
    hilog.error(0x0000, 'Downloader', message);
    this.onError?.(message);
  }

  // 格式化文件大小
  static formatSize(bytes: number): string {
    if (bytes < 1024) return `${bytes}B`;
    if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
    if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
    return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}GB`;
  }

  // 格式化下载速度
  static formatSpeed(bytesPerSecond: number): string {
    if (bytesPerSecond < 1024) return `${bytesPerSecond.toFixed(0)}B/s`;
    if (bytesPerSecond < 1024 * 1024) return `${(bytesPerSecond / 1024).toFixed(1)}KB/s`;
    return `${(bytesPerSecond / (1024 * 1024)).toFixed(1)}MB/s`;
  }

  // 格式化剩余时间
  static formatTime(seconds: number): string {
    if (seconds < 60) return `${seconds}`;
    if (seconds < 3600) return `${Math.floor(seconds / 60)}${seconds % 60}`;
    return `${Math.floor(seconds / 3600)}${Math.floor((seconds % 3600) / 60)}`;
  }
}

完整示例:应用内更新UI组件

把检测、下载、进度、安装整合成一个完整的UI组件。

// entry/src/main/ets/components/InAppUpdateComponent.ets
// 应用内更新完整UI组件——从检测到安装的一站式体验

import { hilog } from '@kit.PerformanceAnalysisKit';
import { common } from '@kit.AbilityKit';
import { InAppUpdateManager, UpdateStatus } from '../utils/InAppUpdateManager';
import { UpdateDownloader, DownloadProgress } from '../utils/UpdateDownloader';

@Component
export struct InAppUpdateComponent {
  @State status: UpdateStatus = UpdateStatus.IDLE;
  @State progress: number = 0;           // 下载进度
  @State downloadedSize: string = '';     // 已下载大小
  @State totalSize: string = '';         // 总大小
  @State downloadSpeed: string = '';     // 下载速度
  @State remainingTime: string = '';     // 剩余时间
  @State updateLog: string = '';         // 更新日志
  @State newVersion: string = '';        // 新版本号
  @State errorMessage: string = '';      // 错误信息
  @State isForceUpdate: boolean = false; // 是否强制更新

  private updateManager: InAppUpdateManager = InAppUpdateManager.getInstance();
  private downloader: UpdateDownloader = new UpdateDownloader();

  aboutToAppear() {
    // 初始化
    const context = getContext(this) as common.Context;
    this.downloader.init(context);

    // 监听更新状态变化
    this.updateManager.setOnStatusChange((status) => {
      this.status = status;
    });

    // 监听下载进度
    this.downloader.onProgress = (progress: DownloadProgress) => {
      this.progress = progress.progress;
      this.downloadedSize = UpdateDownloader.formatSize(progress.downloadedSize);
      this.totalSize = UpdateDownloader.formatSize(progress.totalSize);
      this.downloadSpeed = UpdateDownloader.formatSpeed(progress.speed);
      this.remainingTime = UpdateDownloader.formatTime(progress.remainingTime);
    };

    // 监听下载完成
    this.downloader.onComplete = (filePath: string) => {
      hilog.info(0x0000, 'UpdateUI', `下载完成: ${filePath}`);
      this.status = UpdateStatus.DOWNLOADED;
      // 触发安装
      this.installUpdate(filePath);
    };

    // 监听下载错误
    this.downloader.onError = (error: string) => {
      this.errorMessage = error;
      this.status = UpdateStatus.FAILED;
    };

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

  // 检查更新
  async checkUpdate() {
    const result = await this.updateManager.checkForUpdate();
    if (result.hasUpdate && result.updateInfo) {
      this.updateLog = result.updateInfo.updateLog;
      this.newVersion = result.updateInfo.versionName;
      this.isForceUpdate = result.updateInfo.isForceUpdate;
      this.totalSize = UpdateDownloader.formatSize(result.updateInfo.packageSize);
    }
  }

  // 开始下载
  startDownload() {
    const updateInfo = this.updateManager.getUpdateInfo();
    if (!updateInfo) return;

    this.status = UpdateStatus.DOWNLOADING;
    this.downloader.startDownload({
      url: updateInfo.downloadUrl,
      fileName: `update_${updateInfo.versionCode}.hap`,
      enableMetered: false, // 默认WiFi下载
      enableRoaming: false
    });
  }

  // 暂停下载
  pauseDownload() {
    this.downloader.pauseDownload();
  }

  // 恢复下载
  resumeDownload() {
    this.downloader.resumeDownload();
  }

  // 安装更新
  async installUpdate(filePath: string) {
    this.status = UpdateStatus.INSTALLING;
    try {
      // 调用系统安装接口
      // HarmonyOS需要通过bundleManager或者隐式Want触发安装
      hilog.info(0x0000, 'UpdateUI', `开始安装: ${filePath}`);
      // 实际安装逻辑需要根据HarmonyOS版本适配
    } catch (error) {
      hilog.error(0x0000, 'UpdateUI', `安装失败: ${JSON.stringify(error)}`);
      this.errorMessage = '安装失败,请稍后重试';
      this.status = UpdateStatus.FAILED;
    }
  }

  build() {
    Column({ space: 16 }) {
      // 根据状态展示不同UI
      if (this.status === UpdateStatus.AVAILABLE) {
        this.buildUpdateAvailable()
      } else if (this.status === UpdateStatus.DOWNLOADING) {
        this.buildDownloading()
      } else if (this.status === UpdateStatus.DOWNLOADED) {
        this.buildDownloaded()
      } else if (this.status === UpdateStatus.FAILED) {
        this.buildFailed()
      } else if (this.status === UpdateStatus.INSTALLING) {
        this.buildInstalling()
      }
    }
    .width('100%')
    .padding(20)
  }

  // 有更新可用
  @Builder
  buildUpdateAvailable() {
    Column({ space: 16 }) {
      Text('发现新版本')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)

      Text(`v${this.newVersion}`)
        .fontSize(14)
        .fontColor('#999999')

      // 更新日志
      Scroll() {
        Text(this.updateLog)
          .fontSize(14)
          .fontColor('#666666')
          .lineHeight(22)
      }
      .constraintSize({ maxHeight: 150 })
      .width('100%')

      Text(`安装包大小: ${this.totalSize}`)
        .fontSize(13)
        .fontColor('#999999')

      Row({ space: 12 }) {
        if (!this.isForceUpdate) {
          Button('稍后再说')
            .width('45%')
            .backgroundColor('#F5F5F5')
            .fontColor('#666666')
            .onClick(() => {
              // 关闭弹窗
            })
        }

        Button('立即更新')
          .width(this.isForceUpdate ? '100%' : '45%')
          .backgroundColor('#007DFF')
          .onClick(() => this.startDownload())
      }
      .width('100%')
    }
    .padding(20)
    .backgroundColor(Color.White)
    .borderRadius(16)
  }

  // 下载中
  @Builder
  buildDownloading() {
    Column({ space: 12 }) {
      Text('正在下载更新...')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)

      // 进度条
      Progress({ value: this.progress, total: 100, type: ProgressType.Linear })
        .width('100%')
        .color('#007DFF')

      // 下载信息
      Row() {
        Text(`${this.downloadedSize} / ${this.totalSize}`)
          .fontSize(12)
          .fontColor('#999999')
        
        Text(`${this.downloadSpeed}`)
          .fontSize(12)
          .fontColor('#999999')
        
        Text(`剩余 ${this.remainingTime}`)
          .fontSize(12)
          .fontColor('#999999')
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)

      // 暂停按钮
      Button('暂停下载')
        .width('100%')
        .backgroundColor('#F5F5F5')
        .fontColor('#666666')
        .onClick(() => this.pauseDownload())
    }
    .padding(20)
    .backgroundColor(Color.White)
    .borderRadius(16)
  }

  // 下载完成
  @Builder
  buildDownloaded() {
    Column({ space: 16 }) {
      Text('下载完成')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor('#4ECDC4')

      Text('更新包已下载完成,点击安装以完成更新')
        .fontSize(14)
        .fontColor('#666666')

      Button('立即安装')
        .width('100%')
        .backgroundColor('#007DFF')
        .onClick(() => {
          // 触发安装
        })
    }
    .padding(20)
    .backgroundColor(Color.White)
    .borderRadius(16)
  }

  // 下载失败
  @Builder
  buildFailed() {
    Column({ space: 16 }) {
      Text('更新失败')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FF6B6B')

      Text(this.errorMessage || '下载失败,请检查网络后重试')
        .fontSize(14)
        .fontColor('#666666')

      Row({ space: 12 }) {
        Button('稍后再说')
          .width('45%')
          .backgroundColor('#F5F5F5')
          .fontColor('#666666')

        Button('重试')
          .width('45%')
          .backgroundColor('#007DFF')
          .onClick(() => this.startDownload())
      }
      .width('100%')
    }
    .padding(20)
    .backgroundColor(Color.White)
    .borderRadius(16)
  }

  // 安装中
  @Builder
  buildInstalling() {
    Column({ space: 16 }) {
      LoadingProgress()
        .width(48)
        .height(48)
        .color('#007DFF')

      Text('正在安装更新...')
        .fontSize(16)
        .fontColor('#666666')

      Text('安装完成后应用将自动重启')
        .fontSize(13)
        .fontColor('#999999')
    }
    .padding(20)
    .backgroundColor(Color.White)
    .borderRadius(16)
  }
}

踩坑与注意事项

坑1:下载目录选择不当

把安装包下载到cache目录——系统清理缓存时可能把你的安装包删了,用户下载了半天白下载。

正确做法:下载到应用的filesDir下,系统不会自动清理。安装完成后再手动删除。

坑2:没有做MD5校验

下载完了直接安装——万一下载过程中文件损坏了呢?安装失败还算好的,万一安装了一个被篡改的包呢?

正确做法:下载完成后校验MD5,确认文件完整后再安装。

// MD5校验示例
import { hash } from '@kit.BasicServicesKit';

async function verifyMd5(filePath: string, expectedMd5: string): Promise<boolean> {
  try {
    const actualMd5 = await hash.hash(filePath, 'md5');
    return actualMd5 === expectedMd5;
  } catch (error) {
    hilog.error(0x0000, 'Verify', `MD5校验失败: ${JSON.stringify(error)}`);
    return false;
  }
}

坑3:移动网络下载大文件

用户在4G/5G网络下下载了一个100MB的更新包——流量费谁出?用户可能投诉你。

正确做法:默认只允许WiFi下载。如果用户在移动网络下,弹窗提示"当前使用移动网络,下载可能消耗流量,是否继续?"

坑4:下载过程中应用被杀

下载到50%的时候,用户切到别的应用,系统把你的应用杀了——下载进度丢失。

正确做法:使用系统下载服务(request.downloadFile),即使应用被杀,下载任务仍在系统层面继续。

坑5:安装后没有清理安装包

安装完成了,但100MB的安装包还躺在存储里。用户存储空间越来越少,还不知道是什么占的。

正确做法:安装完成后(或者应用重启后检测到已更新),删除安装包文件。

// 安装后清理
aboutToAppear() {
  // 检查是否有残留的更新包
  this.cleanOldUpdateFiles();
}

private cleanOldUpdateFiles(): void {
  try {
    const updateDir = `${this.context.filesDir}/update`;
    // 遍历并删除旧更新包
    // fs.listFileSync(updateDir).forEach(file => {
    //   fs.unlinkSync(`${updateDir}/${file}`);
    // });
  } catch (error) {
    // 忽略清理错误
  }
}

坑6:并发下载

用户点了"立即更新",但按钮没有及时禁用,用户又点了一次——创建了两个下载任务,浪费流量和存储。

正确做法:下载开始后禁用按钮,或者用状态锁防止重复下载。

HarmonyOS 6适配说明

HarmonyOS 6对应用内更新做了几项调整:

  1. InAppUpdate API正式发布:HarmonyOS 6提供了官方的InAppUpdate Kit,包含完整的检测、下载、安装API。不再需要自己实现下载逻辑。
// HarmonyOS 6 InAppUpdate Kit 使用示例
import { inAppUpdate } from '@kit.AppKit';

async function checkAndUpdate(): Promise<void> {
  try {
    // 检测更新
    const updateInfo = await inAppUpdate.checkUpdate();
    
    if (updateInfo.hasUpdate()) {
      // 获取更新详情
      const details = updateInfo.getUpdateInfo();
      hilog.info(0x0000, 'InAppUpdate', `新版本: ${details.versionName}`);
      
      // 展示更新弹窗
      // ...
      
      // 开始下载
      const downloadTask = await inAppUpdate.startDownload(details);
      
      // 监听进度
      downloadTask.on('progress', (progress) => {
        hilog.info(0x0000, 'InAppUpdate', `下载进度: ${progress}%`);
      });
      
      // 等待下载完成
      await downloadTask.awaitComplete();
      
      // 触发安装
      await inAppUpdate.installUpdate(downloadTask.getFilePath());
    }
  } catch (error) {
    hilog.error(0x0000, 'InAppUpdate', `更新失败: ${JSON.stringify(error)}`);
  }
}
  1. 安装确认增强:HarmonyOS 6的安装确认弹窗会显示更新包的签名信息,帮助用户确认安装包来源可信。

  2. 后台下载限制:应用在后台时,下载任务会被系统暂停。应用回到前台后自动恢复。

  3. 存储空间检查:InAppUpdate Kit在下载前会自动检查设备剩余空间,空间不足时给出提示。

  4. 断点续传支持:官方API内置断点续传能力,下载中断后可以自动恢复。

总结

应用内更新是提升更新转化率的有效手段。核心记住三点:

  1. 下载要稳:支持断点续传、MD5校验、WiFi/移动网络区分
  2. 进度要透明:实时展示下载进度、速度、剩余时间
  3. 安装要安全:校验文件完整性,安装后清理安装包
维度 评价
学习难度 ⭐⭐⭐ 下载管理需要处理多种边界情况
使用频率 ⭐⭐⭐⭐ 每次版本更新都会涉及
重要程度 ⭐⭐⭐⭐ 直接影响用户更新转化率
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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