HarmonyOS开发:预装应用开发——系统应用集成
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
预装应用的更新机制
预装应用的更新是个大坑。因为预装应用在只读分区,直接覆盖安装是不行的。鸿蒙的更新机制是这样的:
- 系统OTA更新:更新整个系统镜像,预装应用跟着一起更新。这是最彻底的方式,但需要整包升级。
- 增量覆盖安装:新版本的HAP安装到
/data/app/目录,覆盖只读分区的旧版本。用户数据保留。 - 应用商店更新:跟普通应用一样从应用商店下载更新。需要应用在应用商店上架。
关键点:增量覆盖安装后,如果用户恢复出厂设置,/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在预装应用方面有几个变化:
-
预装应用分区调整:新增
/product/app/目录,用于产品线定制的预装应用。优先级高于/vendor/app/,低于/system/app/。 -
预装应用AOT编译:HarmonyOS 6支持预装应用的AOT(Ahead-Of-Time)编译。在系统镜像构建时,预装应用的ArkTS代码会被预编译为机器码,首次启动速度大幅提升。
// 构建配置中启用AOT
{
"module": {
"aotCompile": true,
"aotOutputPath": "aot/"
}
}
-
预装应用灰度更新:新增预装应用的灰度更新机制。系统OTA更新预装应用时,可以按用户比例灰度发布,降低全量更新的风险。
-
预装应用卸载增强:
/vendor/app/下的预装应用可以标记为"可卸载但不可清除数据",卸载后用户数据保留,重新安装时恢复。
适配建议:新项目优先使用/product/app/目录,利用AOT编译提升启动速度,配置灰度更新策略降低更新风险。
总结
预装应用开发不只是"把应用放到系统目录"这么简单。从签名、目录选择、权限配置到更新机制,每一步都有讲究。搞错任何一步,要么装不上,要么更新不了,要么更新了又回退。
核心要点回顾:
- 预装目录分四级:
/system/app/、/system/priv-app/、/vendor/app/、/preload/app/ - 放在系统目录不等于有系统签名,签名要单独处理
- 增量覆盖安装后恢复出厂会回退到预装版本
- 预装应用数据要做版本兼容,防止回退后数据不兼容
- preload目录的应用首次开机安装,HAP有问题会静默失败
| 维度 | 评价 |
|---|---|
| 学习难度 | ⭐⭐⭐⭐ 涉及系统分区、签名、更新机制,概念较多 |
| 使用频率 | ⭐⭐⭐ 做设备定制必用,普通应用开发用不到 |
| 重要程度 | ⭐⭐⭐⭐⭐ 设备出厂的核心环节,出错影响全量设备 |
- 点赞
- 收藏
- 关注作者
评论(0)