HarmonyOS开发:预装应用开发——系统应用集成

举报
Jack20 发表于 2026/06/28 21:11:24 2026/06/28
【摘要】 HarmonyOS开发:预装应用开发——系统应用集成📌 核心要点:预装应用不是把APK往系统目录一扔就完了,从签名、目录、权限到更新机制,每一步都有规范约束,搞错一步就装不上或更新不了。 背景与动机你做设备出厂,手机开箱后桌面上的那些应用——设置、相机、电话、浏览器——它们不是用户从应用商店下载的,是出厂时就预装好的。预装应用跟普通应用有啥区别?表面上看只是安装位置不同,实际上区别大了去...

HarmonyOS开发:预装应用开发——系统应用集成

📌 核心要点:预装应用不是把APK往系统目录一扔就完了,从签名、目录、权限到更新机制,每一步都有规范约束,搞错一步就装不上或更新不了。

背景与动机

你做设备出厂,手机开箱后桌面上的那些应用——设置、相机、电话、浏览器——它们不是用户从应用商店下载的,是出厂时就预装好的。

预装应用跟普通应用有啥区别?表面上看只是安装位置不同,实际上区别大了去了:

  • 预装应用有系统签名,能调特权API
  • 预装应用装在只读分区,用户卸载不了(部分可以)
  • 预装应用的更新走系统更新通道,不走应用商店
  • 预装应用的数据可以在恢复出厂设置后保留

但问题来了:怎么把你的应用预装到系统里?预装到哪个目录?签名怎么处理?后续怎么更新?这些事不搞清楚,你的应用要么装不上去,要么装上去了更新不了,要么更新了又回退。

这篇就把预装应用从开发到集成到更新的全流程讲清楚。

核心原理

预装应用的目录结构

鸿蒙系统的预装应用放在以下目录:

/system/app/          ← 系统核心应用(不可卸载)
  ├── Settings/
  │   ├── Settings.hap
  │   └── module.json
  ├── Launcher/
  └── SystemUI/

/system/priv-app/     ← 特权预装应用(不可卸载,拥有更多权限)
  ├── Phone/
  ├── Camera/
  └── Bluetooth/

/vendor/app/          ← 厂商预装应用(可标记为可卸载)
  ├── CustomBrowser/
  └── AppStore/

/preload/app/         ← 预加载应用(首次开机安装,可卸载)
  ├── GameCenter/
  └── NewsApp/
目录 权限级别 可卸载 更新方式
/system/app/ 系统签名 不可 系统OTA
/system/priv-app/ 平台签名 不可 系统OTA
/vendor/app/ 系统签名 可配置 系统OTA/应用商店
/preload/app/ 普通签名 应用商店

预装应用的签名流程

flowchart TD
    A[开发预装应用] --> B{选择签名方式}
    B -->|源码编译| C[放入源码树]
    C --> C1[编译脚本自动签名]
    C1 --> C2[生成系统签名HAP]
    
    B -->|独立开发| D[获取平台密钥]
    D --> D1[开发阶段用调试证书]
    D1 --> D2[发布阶段用平台密钥签名]
    D2 --> C2
    
    C2 --> E{选择安装目录}
    E -->|核心系统应用| F[/system/app/]
    E -->|特权应用| G[/system/priv-app/]
    E -->|厂商应用| H[/vendor/app/]
    E -->|预加载应用| I[/preload/app/]
    
    F --> J[打包到系统镜像]
    G --> J
    H --> J
    I --> K[首次开机安装]
    
    classDef dev fill:#e8f5e9,stroke:#4caf50,color:#1b5e20
    classDef sign fill:#e3f2fd,stroke:#2196f3,color:#0d47a1
    classDef dir fill:#fff3e0,stroke:#ff9800,color:#e65100
    classDef deploy fill:#fce4ec,stroke:#f44336,color:#b71c1c
    
    class A,B dev
    class C,C1,D,D1,D2,C2 sign
    class E,F,G,H,I dir
    class J,K deploy

预装应用的更新机制

预装应用的更新是个大坑。因为预装应用在只读分区,直接覆盖安装是不行的。鸿蒙的更新机制是这样的:

  1. 系统OTA更新:更新整个系统镜像,预装应用跟着一起更新。这是最彻底的方式,但需要整包升级。
  2. 增量覆盖安装:新版本的HAP安装到/data/app/目录,覆盖只读分区的旧版本。用户数据保留。
  3. 应用商店更新:跟普通应用一样从应用商店下载更新。需要应用在应用商店上架。

关键点:增量覆盖安装后,如果用户恢复出厂设置,/data/app/被清空,应用会回退到只读分区的旧版本。这个行为你必须知道。

代码实战

基础用法:预装应用配置

预装应用需要在module.json5中声明系统应用标识和安装信息:

// src/main/module.json5 - 预装应用配置
{
  "app": {
    "bundleName": "com.example.presetapp",
    "vendor": "example",
    "versionCode": 1000000,
    "versionName": "1.0.0",
    "icon": "$media:app_icon",
    "label": "$string:app_name",
    // 关键:声明为系统应用
    "systemApp": true,
    // 关键:声明预装信息
    "targetAPIVersion": 12,
    "apiReleaseType": "Release",
    "debug": false
  },
  "module": {
    "name": "PresetApp",
    "type": "entry",
    "deviceTypes": ["default"],
    "systemApp": true,
    // 关键:声明安装方式
    "installationFree": false,
    "distro": {
      "deliveryWithInstall": true,
      "moduleName": "PresetApp",
      "moduleType": "entry"
    },
    "requestPermissions": [
      {
        "name": "ohos.permission.GET_BUNDLE_INFO",
        "reason": "$string:permission_reason"
      }
    ]
  }
}

进阶用法:预装应用信息检测与更新

预装应用运行时需要知道自己的安装来源和版本状态:

// common/PresetAppManager.ets - 预装应用管理工具
import bundleManager from '@ohos.bundle.bundleManager';
import installer from '@ohos.bundle.installer';
import { BusinessError } from '@ohos.base';

class PresetAppManager {
  private tag: string = 'PresetAppManager';
  private bundleName: string = 'com.example.presetapp';

  // 获取当前应用安装信息
  async getAppInfo(): Promise<bundleManager.BundleInfo | null> {
    try {
      const bundleInfo = await bundleManager.getBundleInfoForSelf(
        bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT
      );
      console.info(this.tag, `应用版本: ${bundleInfo.versionName}`);
      console.info(this.tag, `版本号: ${bundleInfo.versionCode}`);
      console.info(this.tag, `安装时间: ${bundleInfo.installTime}`);
      return bundleInfo;
    } catch (err) {
      const error = err as BusinessError;
      console.error(this.tag, `获取应用信息失败: ${error.message}`);
      return null;
    }
  }

  // 检查是否为预装应用
  async isPresetApp(): Promise<boolean> {
    try {
      const bundleInfo = await bundleManager.getBundleInfoForSelf(
        bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_HAP_MODULE
      );
      // 预装应用的appInfo里有isSystemApp标记
      const appInfo = bundleInfo.appInfo;
      if (appInfo) {
        return appInfo.systemApp;
      }
      return false;
    } catch (err) {
      return false;
    }
  }

  // 获取应用更新来源
  async getUpdateSource(): Promise<string> {
    try {
      const bundleInfo = await bundleManager.getBundleInfoForSelf(
        bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT
      );
      
      // 判断是否被覆盖更新过
      const installTime = bundleInfo.installTime;
      const updateTime = bundleInfo.updateTime;
      
      if (updateTime > installTime) {
        return '已覆盖更新';
      }
      return '预装原始版本';
    } catch (err) {
      return '未知';
    }
  }

  // 静默更新应用(需要系统签名 + INSTALL_BUNDLE权限)
  async silentUpdate(hapPath: string): Promise<boolean> {
    try {
      const bundleInstaller = installer.getBundleInstaller();
      const installParam: installer.InstallParam = {
        userId: 100,
        installFlag: installer.InstallFlag.REPLACE_EXISTING,
        isKeepData: true  // 保留用户数据
      };

      await new Promise<void>((resolve, reject) => {
        bundleInstaller.install([hapPath], installParam, (err) => {
          if (err) {
            reject(err);
          } else {
            resolve();
          }
        });
      });

      console.info(this.tag, '应用更新成功');
      return true;
    } catch (err) {
      const error = err as BusinessError;
      console.error(this.tag, `应用更新失败: ${error.message}`);
      return false;
    }
  }

  // 检查版本更新
  async checkUpdate(localVersionCode: number, remoteVersionCode: number): Promise<boolean> {
    return remoteVersionCode > localVersionCode;
  }
}

export default new PresetAppManager();

完整示例:预装应用状态页面

// pages/PresetStatusPage.ets - 预装应用状态页面
import PresetAppManager from '../common/PresetAppManager';
import promptAction from '@ohos.promptAction';

@Entry
@Component
struct PresetStatusPage {
  @State isPreset: boolean = false;
  @State appVersion: string = '加载中...';
  @State versionCode: number = 0;
  @State updateSource: string = '加载中...';
  @State installTime: string = '';
  @State isChecking: boolean = false;

  async aboutToAppear() {
    await this.loadAppStatus();
  }

  async loadAppStatus() {
    this.isPreset = await PresetAppManager.isPresetApp();
    
    const bundleInfo = await PresetAppManager.getAppInfo();
    if (bundleInfo) {
      this.appVersion = bundleInfo.versionName;
      this.versionCode = bundleInfo.versionCode;
      this.installTime = this.formatTime(bundleInfo.installTime);
    }
    
    this.updateSource = await PresetAppManager.getUpdateSource();
  }

  formatTime(timestamp: number): string {
    const date = new Date(timestamp);
    return `${date.getFullYear()}-${this.padZero(date.getMonth() + 1)}-${this.padZero(date.getDate())} ` +
      `${this.padZero(date.getHours())}:${this.padZero(date.getMinutes())}`;
  }

  padZero(num: number): string {
    return num < 10 ? `0${num}` : `${num}`;
  }

  build() {
    Scroll() {
      Column() {
        // 应用状态卡片
        Column() {
          Row() {
            Text('应用状态')
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
            Badge({
              count: 0,
              position: BadgePosition.Right,
              style: { badgeSize: 8, badgeColor: this.isPreset ? '#4caf50' : '#ff9800' }
            }) {
              Text(this.isPreset ? '系统预装' : '用户安装')
                .fontSize(14)
                .fontColor(this.isPreset ? '#4caf50' : '#ff9800')
            }
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .margin({ bottom: 20 })

          this.StatusRow('应用版本', this.appVersion)
          this.StatusRow('版本号', `${this.versionCode}`)
          this.StatusRow('更新来源', this.updateSource)
          this.StatusRow('安装时间', this.installTime)
        }
        .width('100%')
        .padding(20)
        .backgroundColor('#ffffff')
        .borderRadius(12)
        .margin({ bottom: 16 })

        // 预装应用说明
        Column() {
          Text('预装应用说明')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .margin({ bottom: 12 })

          if (this.isPreset) {
            this.NoticeItem('🔒', '此应用为系统预装,不可通过常规方式卸载')
            this.NoticeItem('🔄', '恢复出厂设置将回退到预装版本')
            this.NoticeItem('📦', '系统OTA更新会同步更新此应用')
          } else {
            this.NoticeItem('ℹ️', '此应用为用户安装,可正常卸载')
            this.NoticeItem('🏪', '通过应用商店获取更新')
          }
        }
        .width('100%')
        .padding(20)
        .backgroundColor('#ffffff')
        .borderRadius(12)
        .margin({ bottom: 16 })

        // 操作按钮
        Button('刷新状态')
          .width('100%')
          .height(48)
          .backgroundColor('#1976d2')
          .borderRadius(8)
          .onClick(() => this.loadAppStatus())

        Button('检查更新')
          .width('100%')
          .height(48)
          .backgroundColor('#616161')
          .borderRadius(8)
          .margin({ top: 12 })
          .enabled(!this.isChecking)
          .onClick(async () => {
            this.isChecking = true;
            // 模拟检查更新
            const hasUpdate = await PresetAppManager.checkUpdate(this.versionCode, this.versionCode + 1);
            if (hasUpdate) {
              promptAction.showToast({ message: '发现新版本' });
            } else {
              promptAction.showToast({ message: '已是最新版本' });
            }
            this.isChecking = false;
          })
      }
      .padding(24)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#f5f5f5')
  }

  @Builder
  StatusRow(label: string, value: string) {
    Row() {
      Text(label)
        .fontSize(14)
        .fontColor('#999999')
        .width(80)
      Text(value)
        .fontSize(14)
        .fontColor('#333333')
        .layoutWeight(1)
    }
    .width('100%')
    .margin({ bottom: 12 })
  }

  @Builder
  NoticeItem(icon: string, text: string) {
    Row() {
      Text(icon)
        .fontSize(16)
        .margin({ right: 8 })
      Text(text)
        .fontSize(13)
        .fontColor('#666666')
        .layoutWeight(1)
    }
    .width('100%')
    .margin({ bottom: 8 })
  }
}

踩坑与注意事项

坑一:预装HAP没有系统签名

你把HAP放到了/system/app/目录下,但启动后发现特权API调不通。原因是HAP没有系统签名。

放在系统目录下不等于有系统签名。签名是在编译打包阶段完成的,不是安装位置决定的。如果你的HAP是通过手动push到系统目录的,它不会有系统签名。

正确做法:通过源码编译集成,或者用hapsigner手动签名后再push。

坑二:预装应用数据目录权限问题

预装应用首次启动时,系统会为它创建数据目录。但如果你在module.json5里声明了systemApp: true,数据目录的SELinux上下文跟普通应用不一样。

如果你的预装应用需要跟其他应用共享数据,需要额外配置SELinux策略和文件共享权限。否则其他应用读不到你的数据目录。

坑三:预装应用更新后回退

用户通过应用商店更新了预装应用,一切正常。然后用户恢复出厂设置,应用回退到了预装版本,但用户数据还在(如果用了isKeepData: true)。这时候可能出现新版数据结构跟旧版代码不兼容的问题。

解决办法:在数据存储层做版本兼容处理,旧版代码能识别新版数据格式,至少不崩溃。

// 数据版本兼容示例
class DataMigrator {
  private currentDataVersion: number = 2;

  migrate(storedVersion: number, data: Record<string, Object>): Record<string, Object> {
    let result = data;
    // 从存储版本逐步迁移到当前版本
    for (let v = storedVersion; v < this.currentDataVersion; v++) {
      result = this.migrateStep(v, result);
    }
    return result;
  }

  migrateStep(fromVersion: number, data: Record<string, Object>): Record<string, Object> {
    switch (fromVersion) {
      case 1:
        // v1 -> v2: 新增字段
        data['newField'] = 'default_value';
        break;
    }
    return data;
  }
}

坑四:preload目录的应用首次开机安装失败

/preload/app/目录下的应用是在首次开机时由系统自动安装的。如果你的HAP包有问题(签名不对、架构不匹配、module.json5配置错误),安装会静默失败,不会有任何提示。

排查方法:查看首次开机日志,搜索PreloadAppInstaller相关的错误信息。

坑五:预装应用的ABI兼容性

预装HAP必须包含设备支持的ABI架构。如果你的HAP只编译了arm64-v8a,但设备只支持armeabi-v7a,安装就会失败。

在源码编译时,确保build-profile.json5里配置了正确的ABI:

{
  "app": {
    "products": [
      {
        "name": "default",
        "compatibleSdkVersion": "12.0.0",
        "runtimeOS": "HarmonyOS",
        "targetSdkVersion": "12.0.0",
        "signingConfig": "default",
        "output": {
          "abi": ["arm64-v8a", "armeabi-v7a"]
        }
      }
    ]
  }
}

HarmonyOS 6适配说明

HarmonyOS 6在预装应用方面有几个变化:

  1. 预装应用分区调整:新增/product/app/目录,用于产品线定制的预装应用。优先级高于/vendor/app/,低于/system/app/

  2. 预装应用AOT编译:HarmonyOS 6支持预装应用的AOT(Ahead-Of-Time)编译。在系统镜像构建时,预装应用的ArkTS代码会被预编译为机器码,首次启动速度大幅提升。

// 构建配置中启用AOT
{
  "module": {
    "aotCompile": true,
    "aotOutputPath": "aot/"
  }
}
  1. 预装应用灰度更新:新增预装应用的灰度更新机制。系统OTA更新预装应用时,可以按用户比例灰度发布,降低全量更新的风险。

  2. 预装应用卸载增强/vendor/app/下的预装应用可以标记为"可卸载但不可清除数据",卸载后用户数据保留,重新安装时恢复。

适配建议:新项目优先使用/product/app/目录,利用AOT编译提升启动速度,配置灰度更新策略降低更新风险。

总结

预装应用开发不只是"把应用放到系统目录"这么简单。从签名、目录选择、权限配置到更新机制,每一步都有讲究。搞错任何一步,要么装不上,要么更新不了,要么更新了又回退。

核心要点回顾:

  • 预装目录分四级:/system/app//system/priv-app//vendor/app//preload/app/
  • 放在系统目录不等于有系统签名,签名要单独处理
  • 增量覆盖安装后恢复出厂会回退到预装版本
  • 预装应用数据要做版本兼容,防止回退后数据不兼容
  • preload目录的应用首次开机安装,HAP有问题会静默失败
维度 评价
学习难度 ⭐⭐⭐⭐ 涉及系统分区、签名、更新机制,概念较多
使用频率 ⭐⭐⭐ 做设备定制必用,普通应用开发用不到
重要程度 ⭐⭐⭐⭐⭐ 设备出厂的核心环节,出错影响全量设备
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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