鸿蒙 PC 应用开发实战:多进程架构与 IPC 通信

PC 应用场景与手机最大的不同,不只是窗口更大、鼠标键盘更丰富,更在于应用本身的复杂度上了量级。一个桌面下载工具可能同时跑着界面、后台下载引擎、云同步守护和本地数据库;一个 IDE 可能拆出编辑器进程、语言服务进程和终端进程。这些模块分布在不同的进程中,既是为了隔离崩溃、保障稳定性,也是为了利用多核 CPU 做并行计算。
跨进程协作的核心,就是 IPC(Inter-Process Communication,进程间通信)。在 HarmonyOS NEXT 的 Stage 模型里,IPC 不是某个单一 API,而是一套组合拳:Ability 之间的 Want 调用、ServiceExtensionAbility 的长连接、基于 rpc 的 Binder 调用、commonEventManager 的事件广播,以及 DataShareExtensionAbility 的数据共享。本文把这些能力串成一条完整的链路,带你从零搭一个跨进程下载中心。
全文代码基于 HarmonyOS NEXT(API 12+),Stage 模型,可直接在 DevEco Studio 5.0+ 中编译运行。建议边读边建工程,把每个 Ability 真正跑起来看效果。
一、Stage 模型下的进程与 Ability 拓扑
在 Stage 模型里,「进程」和「Ability」是两层东西。一个应用(bundle)可以包含多个 UIAbility、多个 ExtensionAbility,而它们默认跑在同一个进程里。但只要你愿意,就能通过 module.json5 的 process 字段把某个 Ability 单独丢进自己的进程——这正是多进程架构的物理基础。
1.1 为什么要把 Ability 拆到独立进程
只有一个进程时,后台下载任务卡死会连带把整个 UI 拖垮;后台常驻服务被系统回收,前台也跟着重启。把「下载引擎」放进独立进程后:
- UI 进程崩溃,下载进程继续跑,恢复后自动重连;
- 下载进程占满 CPU,不影响界面滑动的流畅度;
- 不同模块可以独立升级、独立权限管控。
在 PC 上,用户往往让应用「常驻后台」,独立进程还能通过 process 配置让后台服务在用户关闭窗口后继续存活。
1.2 用 process 字段声明独立进程
下面的 module.json5 定义了三个入口:主界面 MainAbility、设置界面 SettingsAbility(独立进程)、后台下载服务 DownloadService(service 类型 ExtensionAbility,独立进程),以及数据共享扩展 DownloadDataShare(dataShare 类型 ExtensionAbility)。
{
"app": {
"bundleName": "com.example.downloadcenter",
"vendor": "example",
"versionCode": 1000000,
"versionName": "1.0.0",
"icon": "$media:app_icon",
"label": "$string:app_name"
},
"module": {
"name": "entry",
"type": "entry",
"mainElement": "MainAbility",
"deviceTypes": ["default", "tablet", "2in1"],
"abilities": [
{
"name": "MainAbility",
"srcEntry": "./ets/abilities/MainAbility.ets",
"description": "$string:main_desc",
"icon": "$media:icon",
"label": "$string:main_label",
"startWindowIcon": "$media:icon",
"startWindowBackground": "$color:start_window_background",
"skills": [
{ "entities": ["entity.system.home"], "actions": ["action.system.home"] }
]
},
{
"name": "SettingsAbility",
"srcEntry": "./ets/abilities/SettingsAbility.ets",
"description": "$string:settings_desc",
"icon": "$media:icon",
"label": "$string:settings_label",
"startWindowIcon": "$media:icon",
"startWindowBackground": "$color:start_window_background",
"process": "com.example.downloadcenter.settings",
"launchType": "standard"
}
],
"extensionAbilities": [
{
"name": "DownloadService",
"srcEntry": "./ets/services/DownloadService.ets",
"description": "$string:service_desc",
"type": "service",
"process": "com.example.downloadcenter.download",
"exported": true
},
{
"name": "DownloadDataShare",
"srcEntry": "./ets/datashare/DownloadDataShare.ets",
"description": "$string:datashare_desc",
"type": "dataShare",
"exported": true,
"uri": "datashare://com.example.downloadcenter.download"
}
]
}
}
注意 SettingsAbility 和 DownloadService 都声明了独立的 process。系统启动它们时,会为各自 fork 出独立进程,getContext(this).applicationInfo 里的进程名也不同。
1.3 通信选型:选对武器
进程一旦隔离,内存不再共享。选择通信方式要看「调用语义」:
- 一次性带结果返回:用
startAbilityForResult(Call 机制)。 - 长生命周期、反复调用:用
connectServiceExtensionAbility+rpc。 - 一对多、解耦通知:用
commonEventManager事件总线。 - 结构化数据共享:用
DataShareExtensionAbility。
下面逐一拆解,每节都给完整可运行代码。
二、跨 Ability 调用:Want 与 Call 机制
最基础的 IPC 是「启动另一个 Ability 并传参」。Want 是鸿蒙里描述「我要去哪儿、带什么」的标准信封。单向传参用 startAbility,需要对方回结果时用 startAbilityForResult,这就是官方的「Call 调用」。
2.1 单向启动与参数传递
主界面里点击「设置」按钮,启动独立进程的设置 Ability,并通过 parameters 把当前主题传过去:
// ets/pages/MainPage.ets
import common from '@ohos.app.ability.common';
import Want from '@ohos.app.ability.Want';
@Entry
@Component
struct MainPage {
@State theme: string = 'dark';
private context = getContext(this) as common.UIAbilityContext;
build() {
Column() {
Text('跨进程下载中心')
.fontSize(24)
.margin(20)
Button('打开设置(独立进程)')
.onClick(() => this.openSettings())
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
private openSettings(): void {
const want: Want = {
bundleName: 'com.example.downloadcenter',
abilityName: 'SettingsAbility',
parameters: {
theme: this.theme,
from: 'MainAbility'
}
};
this.context.startAbility(want).catch((err: BusinessError) => {
console.error('startAbility failed: ' + err.message);
});
}
}

SettingsAbility 在自己的进程里 onCreate 时拆开信封拿到参数:
// ets/abilities/SettingsAbility.ets
import AbilityConstant from '@ohos.app.ability.AbilityConstant';
import Want from '@ohos.app.ability.Want';
import hilog from '@ohos.hilog';
import UIAbility from '@ohos.app.ability.UIAbility';
const TAG = 'SettingsAbility';
const DOMAIN = 0x1234;
export default class SettingsAbility extends UIAbility {
private incomingTheme: string = 'light';
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
const theme = want.parameters?.theme as string;
const from = want.parameters?.from as string;
if (theme) {
this.incomingTheme = theme;
}
hilog.info(DOMAIN, TAG, 'onCreate from %{public}s, theme=%{public}s', from, theme);
}
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/SettingsPage', (err) => {
if (err.code) {
hilog.error(DOMAIN, TAG, 'loadContent failed: %{public}s', JSON.stringify(err));
}
});
}
}
2.2 带回调的 Call:startAbilityForResult
单向调用解决不了「设置页改完主题,要把结果带回主界面」。这就是 Call 机制发挥作用的地方——用 startAbilityForResult 启动,对方用 terminateSelfWithResult 带着 Want 回来。
主界面发起调用:
// ets/pages/MainPage.ets(续)
private openSettingsWithResult(): void {
const want: Want = {
bundleName: 'com.example.downloadcenter',
abilityName: 'SettingsAbility',
parameters: { theme: this.theme }
};
this.context.startAbilityForResult(want).then((result) => {
const chosen = result.want?.parameters?.theme as string;
if (chosen) {
this.theme = chosen;
hilog.info(DOMAIN, 'MainPage', 'theme updated to %{public}s', chosen);
}
}).catch((err: BusinessError) => {
console.error('startAbilityForResult failed: ' + err.message);
});
}
设置页在用户点击「保存」时返回结果:
// ets/abilities/SettingsAbility.ets(续)
import common from '@ohos.app.ability.common';
private saveTheme(newTheme: string): void {
const abilityContext = this.context as common.UIAbilityContext;
const result: common.WantResult = {
resultCode: 0,
want: {
parameters: { theme: newTheme }
}
};
abilityContext.terminateSelfWithResult(result);
}
startAbilityForResult 的返回值 AbilityResult 里装着 want,参数就原路带回了主进程。terminateSelfWithResult 会关闭设置页并把结果塞回调用方。
小结:Call 机制适合「请求—响应」式的一次性交互。但它每次都要启动/销毁 Ability,不适合高频、长连接的场景。
三、后台 ServiceExtensionAbility 与前台通信
下载引擎必须常驻后台,且 UI 要随时查询进度、下发新任务。这种「长连接、反复调用」的场景,startAbilityForResult 就力不从心了——它该交给 ServiceExtensionAbility + rpc。
3.1 connectServiceExtensionAbility 建立长连接
ServiceExtensionAbility 没有界面,专门承载后台逻辑。前台 UIAbility 通过 connectServiceExtensionAbility 和它建立连接,拿到一个 IRemoteObject 代理(proxy),之后所有调用都走这个代理。
先看后台服务 DownloadService。它的核心是 onConnect 返回一个 rpc.RemoteObject 桩(stub),所有跨进程请求在这里落地:
// ets/services/DownloadService.ets
import ServiceExtensionAbility from '@ohos.app.ability.ServiceExtensionAbility';
import rpc from '@ohos.rpc';
import hilog from '@ohos.hilog';
import type Want from '@ohos.app.ability.Want';
const TAG = 'DownloadService';
const DOMAIN = 0x2000;
// 命令码
const CMD_START = 1;
const CMD_PAUSE = 2;
const CMD_QUERY = 3;
const CMD_REGISTER = 4;
// Binder 桩:跨进程请求在这里执行
class DownloadStub extends rpc.RemoteObject {
constructor() {
super('DownloadServiceStub');
}
onRemoteRequest(
code: number,
data: rpc.MessageParcel,
reply: rpc.MessageParcel,
options: rpc.MessageOption
): boolean {
switch (code) {
case CMD_START: {
const url = data.readString();
const id = DownloadServiceCore.getInstance().startTask(url);
reply.writeInt(id);
hilog.info(DOMAIN, TAG, 'startTask url=%{public}s id=%{public}d', url, id);
return true;
}
case CMD_PAUSE: {
const id = data.readInt();
DownloadServiceCore.getInstance().pauseTask(id);
reply.writeInt(0);
return true;
}
case CMD_QUERY: {
const id = data.readInt();
const progress = DownloadServiceCore.getInstance().queryProgress(id);
reply.writeInt(progress);
return true;
}
case CMD_REGISTER: {
// 注册回调代理,用于进度推送
const callback = data.readRemoteObject();
DownloadServiceCore.getInstance().registerCallback(callback);
reply.writeInt(0);
return true;
}
default:
return false;
}
}
}
// 简单的下载引擎核心(真实项目替换为网络请求)
class DownloadServiceCore {
private static instance: DownloadServiceCore;
private progressMap: Map<number, number> = new Map();
private callback: rpc.IRemoteObject | null = null;
private seq = 0;
static getInstance(): DownloadServiceCore {
if (!DownloadServiceCore.instance) {
DownloadServiceCore.instance = new DownloadServiceCore();
}
return DownloadServiceCore.instance;
}
startTask(url: string): number {
const id = ++this.seq;
this.progressMap.set(id, 0);
this.simulate(id);
return id;
}
pauseTask(id: number): void {
this.progressMap.set(id, this.progressMap.get(id) ?? 0);
}
queryProgress(id: number): number {
return this.progressMap.get(id) ?? -1;
}
registerCallback(cb: rpc.IRemoteObject): void {
this.callback = cb;
}
private simulate(id: number): void {
let p = 0;
const timer = setInterval(() => {
p += 5;
this.progressMap.set(id, p);
this.pushToUi(id, p);
if (p >= 100) {
clearInterval(timer);
}
}, 400);
}
private pushToUi(id: number, progress: number): void {
if (!this.callback) {
return;
}
const data = rpc.MessageParcel.create();
const reply = rpc.MessageParcel.create();
const option = new rpc.MessageOption();
data.writeInt(id);
data.writeInt(progress);
this.callback
.sendRequest(100, data, reply, option)
.then(() => {
data.reclaim();
reply.reclaim();
})
.catch(() => {
data.reclaim();
reply.reclaim();
});
}
}
export default class DownloadService extends ServiceExtensionAbility {
private stub: rpc.RemoteObject | null = null;
onCreate(want: Want): void {
hilog.info(DOMAIN, TAG, 'DownloadService onCreate');
}
onConnect(want: Want): rpc.RemoteObject {
hilog.info(DOMAIN, TAG, 'onConnect');
if (!this.stub) {
this.stub = new DownloadStub();
}
return this.stub;
}
onDisconnect(want: Want): void {
hilog.info(DOMAIN, TAG, 'onDisconnect');
}
onDestroy(): void {
hilog.info(DOMAIN, TAG, 'onDestroy');
}
}
3.2 前台 UI 连接服务并发起 rpc 调用
前台 MainAbility 在 onWindowStageCreate 之后,用 connectServiceExtensionAbility 建立连接,保存 proxy,之后所有按钮都通过 proxy 发 sendRequest。
// ets/abilities/MainAbility.ets
import AbilityConstant from '@ohos.app.ability.AbilityConstant';
import common from '@ohos.app.ability.common';
import rpc from '@ohos.rpc';
import Want from '@ohos.app.ability.Want';
import hilog from '@ohos.hilog';
import UIAbility from '@ohos.app.ability.UIAbility';
const TAG = 'MainAbility';
const DOMAIN = 0x1000;
export default class MainAbility extends UIAbility {
private serviceProxy: rpc.IRemoteObject | null = null;
private connectId: number = -1;
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(DOMAIN, TAG, 'onCreate');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/MainPage', (err) => {
if (err.code) {
hilog.error(DOMAIN, TAG, 'loadContent failed: %{public}s', JSON.stringify(err));
}
});
this.connectDownloadService();
}
private connectDownloadService(): void {
const want: Want = {
bundleName: 'com.example.downloadcenter',
abilityName: 'DownloadService'
};
const conn: common.IAbilityConnection = {
onConnect: (elementName, proxy) => {
hilog.info(DOMAIN, TAG, 'service connected: %{public}s', elementName);
this.serviceProxy = proxy;
// 连接成功后注册 UI 回调桩,接收进度推送
this.registerUiCallback();
},
onDisconnect: (elementName) => {
hilog.warn(DOMAIN, TAG, 'service disconnected');
this.serviceProxy = null;
},
onFailed: (code) => {
hilog.error(DOMAIN, TAG, 'connect failed code=%{public}d', code);
}
};
this.context
.connectServiceExtensionAbility(want, conn)
.then((id: number) => {
this.connectId = id;
hilog.info(DOMAIN, TAG, 'connect id=%{public}d', id);
})
.catch((err: BusinessError) => {
console.error('connectServiceExtensionAbility failed: ' + err.message);
});
}
private registerUiCallback(): void {
if (!this.serviceProxy) {
return;
}
const data = rpc.MessageParcel.create();
const reply = rpc.MessageParcel.create();
const option = new rpc.MessageOption();
// 把 UI 侧的 RemoteObject 作为回调传给服务
data.writeRemoteObject(this.createUiCallbackStub());
this.serviceProxy
.sendRequest(4, data, reply, option)
.then(() => {
data.reclaim();
reply.reclaim();
})
.catch(() => {
data.reclaim();
reply.reclaim();
});
}
// UI 侧回调桩:服务会通过它把进度推回来
private createUiCallbackStub(): rpc.RemoteObject {
return new rpc.RemoteObject('UiCallbackStub') {
onRemoteRequest(
code: number,
data: rpc.MessageParcel,
reply: rpc.MessageParcel,
options: rpc.MessageOption
): boolean {
if (code === 100) {
const id = data.readInt();
const progress = data.readInt();
AppStorage.setOrCreate('downloadProgress_' + id, progress);
hilog.info(DOMAIN, TAG, 'progress id=%{public}d p=%{public}d', id, progress);
return true;
}
return false;
}
};
}
onDestroy(): void {
if (this.connectId !== -1) {
this.context.disconnectServiceExtensionAbility(this.connectId).catch(() => {});
}
}
}
UI 页面拿到 proxy 后发起下载:
// ets/pages/MainPage.ets(续)
@StorageLink('downloadProgress_1') @State progress: number = 0;
private startDownload(url: string): void {
const mainAbility = this.context.getHostContext() as MainAbility;
// 实际项目中把 proxy 暴露成单例/AppStorage,这里简化
const proxy = (getContext(this) as any).serviceProxy as rpc.IRemoteObject | null;
if (!proxy) {
console.error('service not connected');
return;
}
const data = rpc.MessageParcel.create();
const reply = rpc.MessageParcel.create();
const option = new rpc.MessageOption();
data.writeString(url);
proxy
.sendRequest(1, data, reply, option)
.then(() => {
const taskId = reply.readInt();
hilog.info(DOMAIN, 'MainPage', 'task created id=%{public}d', taskId);
data.reclaim();
reply.reclaim();
})
.catch((err: BusinessError) => {
console.error('sendRequest failed: ' + err.message);
data.reclaim();
reply.reclaim();
});
}
小结:
connectServiceExtensionAbility建立的是一条长连接,proxy 反复调用零启动开销;rpc.MessageParcel负责序列化参数,sendRequest是异步的,天然适配高频进度查询与任务下发。
四、事件总线:commonEventManager 解耦广播
rpc 是「点对点」调用,但有时候 UI 不在乎是谁触发的——只要「有人下载完了」就通知我。这种一对多、弱耦合的通知,用 commonEventManager 最合适。它基于系统公共事件,天然跨进程。
4.1 订阅与发布
下载服务每完成一个任务,就发布一条自定义事件;主界面、设置页、桌面小组件都可以订阅同一条事件,互不知道彼此存在。
服务侧发布进度/完成事件(在 DownloadServiceCore.pushToUi 之外,再补一个事件广播):
// ets/services/DownloadService.ets(续)
import commonEventManager from '@ohos.commonEventManager';
class DownloadServiceCore {
// ... 既有方法省略 ...
private publishComplete(id: number, url: string): void {
const event: commonEventManager.CommonEventPublishData = {
bundleName: 'com.example.downloadcenter',
code: 0,
data: { taskId: id, url: url, status: 'done' }
};
commonEventManager.publish('com.example.downloadcenter.COMPLETE', event, (err) => {
if (err) {
hilog.error(DOMAIN, TAG, 'publish failed: %{public}s', err.message);
}
});
}
}
UI 侧在页面 aboutToAppear 订阅,拿到事件后刷新本地状态:
// ets/pages/MainPage.ets(续)
import commonEventManager from '@ohos.commonEventManager';
private subscriber: commonEventManager.CommonEventSubscriber | null = null;
async aboutToAppear(): Promise<void> {
const subscribeInfo: commonEventManager.CommonEventSubscribeInfo = {
events: ['com.example.downloadcenter.COMPLETE']
};
try {
this.subscriber = await commonEventManager.createSubscriber(subscribeInfo);
commonEventManager.subscribe(this.subscriber, (err, data) => {
if (err) {
console.error('subscribe error: ' + err.message);
return;
}
const taskId = data.data?.taskId as number;
const status = data.data?.status as string;
hilog.info(DOMAIN, 'MainPage', 'event task=%{public}d status=%{public}s', taskId, status);
// 更新 UI 状态
AppStorage.setOrCreate('lastCompleteTask', taskId);
});
} catch (e) {
console.error('createSubscriber failed: ' + (e as Error).message);
}
}
aboutToDisappear(): void {
if (this.subscriber) {
commonEventManager.unsubscribe(this.subscriber).catch(() => {});
this.subscriber = null;
}
}
小结:
commonEventManager是解耦利器。它不要求收发双方建立连接,也不关心对方是否存活——发布者发完即走,订阅者按需接收,非常适合「下载完成」「网络变化」「配置更新」这类系统级广播。
五、数据共享:DataShareExtensionAbility
当多个 Ability(甚至多个应用)需要读写同一份结构化数据——比如下载历史、任务队列——用 Want 传参太笨重,用 rpc 又得自己写查询语法。这时候该上 DataShareExtensionAbility:它像内置的小型数据库服务,对外暴露 query/insert/update/delete。
5.1 实现 DataShare 提供方
下面用关系型数据库 relationalStore 做一个「下载历史」数据提供方。它继承 DataShareExtensionAbility,实现增删改查。
// ets/datashare/DownloadDataShare.ets
import DataShareExtensionAbility from '@ohos.application.DataShareExtensionAbility';
import relationalStore from '@ohos.data.relationalStore';
import hilog from '@ohos.hilog';
import type Want from '@ohos.app.ability.Want';
const TAG = 'DownloadDataShare';
const DOMAIN = 0x3000;
const STORE_NAME = 'download.db';
export default class DownloadDataShare extends DataShareExtensionAbility {
private rdbStore: relationalStore.RdbStore | null = null;
onCreate(want: Want): void {
hilog.info(DOMAIN, TAG, 'onCreate');
const config: relationalStore.StoreConfig = {
name: STORE_NAME,
securityLevel: relationalStore.SecurityLevel.S1
};
relationalStore.getRdbStore(this.context, config, (err, store) => {
if (err) {
hilog.error(DOMAIN, TAG, 'getRdbStore failed: %{public}s', err.message);
return;
}
this.rdbStore = store;
store.executeSql(
'CREATE TABLE IF NOT EXISTS history (id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT, progress INTEGER, ts INTEGER)'
).catch((e) => hilog.error(DOMAIN, TAG, 'create table failed: %{public}s', e.message));
});
}
insert(uri: string, valueBucket: relationalStore.ValuesBucket): number {
if (!this.rdbStore) {
return -1;
}
const id = this.rdbStore.insert('history', valueBucket);
hilog.info(DOMAIN, TAG, 'insert row id=%{public}d', id);
return id;
}
query(
uri: string,
predicates: relationalStore.RdbPredicates,
columns: string[]
): relationalStore.ResultSet {
if (!this.rdbStore) {
return {} as relationalStore.ResultSet;
}
return this.rdbStore.query(predicates, columns);
}
update(
uri: string,
predicates: relationalStore.RdbPredicates,
valueBucket: relationalStore.ValuesBucket
): number {
if (!this.rdbStore) {
return -1;
}
return this.rdbStore.update(valueBucket, predicates);
}
delete(uri: string, predicates: relationalStore.RdbPredicates): number {
if (!this.rdbStore) {
return -1;
}
return this.rdbStore.delete(predicates);
}
}
5.2 消费端:dataShare Helper 读写
任意 Ability 都可以用 dataShare.createDataShareHelper 连接这个提供方,像访问数据库一样读写:
// ets/pages/HistoryPage.ets
import dataShare from '@ohos.data.dataShare';
import relationalStore from '@ohos.data.relationalStore';
import common from '@ohos.app.ability.common';
const URI = 'datashare://com.example.downloadcenter.download/history';
@Entry
@Component
struct HistoryPage {
@State records: Array<{ url: string; progress: number }> = [];
private helper: dataShare.DataShareHelper | null = null;
aboutToAppear(): void {
const context = getContext(this) as common.UIAbilityContext;
dataShare.createDataShareHelper(context, URI, (err, helper) => {
if (err) {
console.error('createDataShareHelper failed: ' + err.message);
return;
}
this.helper = helper;
this.loadHistory();
});
}
private loadHistory(): void {
if (!this.helper) {
return;
}
const predicates = new relationalStore.RdbPredicates('history');
this.helper.query(URI, predicates, ['url', 'progress'], (err, resultSet) => {
if (err) {
console.error('query failed: ' + err.message);
return;
}
const list: Array<{ url: string; progress: number }> = [];
while (resultSet.goToNextRow()) {
list.push({
url: resultSet.getString(resultSet.getColumnIndex('url')),
progress: resultSet.getLong(resultSet.getColumnIndex('progress'))
});
}
resultSet.close();
this.records = list;
});
}
private addRecord(url: string, progress: number): void {
if (!this.helper) {
return;
}
const value: relationalStore.ValuesBucket = {
url: url,
progress: progress,
ts: new Date().getTime()
};
this.helper.insert(URI, value, (err, id) => {
if (!err) {
this.loadHistory();
}
});
}
build() {
Column() {
List() {
ForEach(this.records, (item: { url: string; progress: number }) => {
ListItem() {
Row() {
Text(item.url).fontSize(13).layoutWeight(1)
Text(item.progress + '%').fontSize(13).fontColor('#888888')
}
.padding(12)
}
})
}
.width('100%')
.layoutWeight(1)
}
.width('100%')
.height('100%')
.padding(16)
}
}
小结:
DataShareExtensionAbility让结构化数据有了「统一收口」。下载服务只管写,历史页只管读,谁都不用关心数据库细节,更不需要把数据库连接塞进每个进程。
六、综合实战:跨进程下载中心
把上面四块拼起来,就是一个完整的多进程下载中心架构:
┌─────────────────────┐ startAbilityForResult ┌─────────────────────┐
│ MainAbility │ ───────────────────────────────────▶ │ SettingsAbility │
│ (UI 主进程) │ ◀──────────── Want 结果 ───────────── │ (settings 进程) │
│ │ └─────────────────────┘
│ ┌───────────────┐ │
│ │ MainPage │ │ connectServiceExtensionAbility
│ │ (UI 进度展示) │ │ ───────────────────────────────────▶ ┌─────────────────────┐
│ └───────────────┘ │ │ DownloadService │
│ │ │ rpc sendRequest (start/pause/query) │ (download 进程) │
│ │ proxy │ ───────────────────────────────────▶ │ - DownloadStub │
│ ▼ │ ◀──── rpc 回调推送进度 ────────────── │ - ServiceCore │
│ ┌───────────────┐ │ └──────────┬──────────┘
│ │ HistoryPage │ │ │ 写
│ └───────────────┘ │ ┌──────────▼──────────┘
│ │ │ commonEventManager 广播完成事件 │ DownloadDataShare │
│ ▼ │ ───────────────────────────────────▶ │ (dataShare 进程) │
│ AppStorage 状态 │ │ relationalStore │
└─────────────────────┘ └─────────────────────┘
调用链一句话总结:UI 用 Call 机制拉起设置页拿回配置;用 connectServiceExtensionAbility 建立长连接,通过 rpc 下发任务、查询进度;服务用 rpc 回调和 commonEventManager 双通道回传;下载历史统一收口到 DataShareExtensionAbility。
主页面把进度、事件、历史三者真正联动起来的关键代码:
// ets/pages/MainPage.ets(整合版)
@Entry
@Component
struct MainPage {
@StorageLink('downloadProgress_1') @State progress: number = 0;
@State lastComplete: number = 0;
@State url: string = 'https://example.com/bigfile.zip';
build() {
Column({ space: 16 }) {
Text('下载中心').fontSize(24)
TextField('下载链接', this.url)
.onChange((v) => { this.url = v; })
.width('80%')
Button('开始下载')
.onClick(() => this.startDownload(this.url))
Progress({ value: this.progress, total: 100, type: ProgressType.Linear })
.width('80%')
Text('当前进度:' + this.progress + '%')
.fontSize(14)
.fontColor('#007AFF')
if (this.lastComplete > 0) {
Text('最近完成任务 ID:' + this.lastComplete)
.fontSize(12)
.fontColor('#888888')
}
Button('打开历史')
.onClick(() => this.context.startAbility({
bundleName: 'com.example.downloadcenter',
abilityName: 'HistoryAbility'
}))
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
progress 来自 rpc 回调写入的 AppStorage,lastComplete 来自 commonEventManager 的完成事件——两条通道互不干扰,UI 只管读状态,不关心数据怎么来的。
七、调试与避坑
多进程调试比单进程麻烦,但掌握几招就顺了。
进程确认:用 hdc shell 连上设备,执行 ps -ef | grep downloadcenter,能看到 MainAbility、settings、download 三个进程名各自独立。如果没分开,先检查 module.json5 的 process 字段拼写是否与 bundleName 同前缀。
hdc shell ps -ef | grep downloadcenter
# u0_a123 ... com.example.downloadcenter
# u0_a123 ... com.example.downloadcenter.settings
# u0_a123 ... com.example.downloadcenter.download
rpc 序列化顺序:MessageParcel 的 write 和 read 必须严格配对、顺序一致。写完 writeString 后对方必须先 readString,再 writeInt/readInt,错位一步就会读到乱码甚至崩溃。调试时建议给每个命令码单独写一对编解码函数。
连接生命周期:connectServiceExtensionAbility 返回的 connectId 要保存到 onDestroy 里 disconnect,否则后台服务可能被反复拉起。服务侧的 onConnect 返回的 RemoteObject 必须是单例,否则每次连接拿到不同桩,注册过的回调就失效了。
DataShare 权限:DataShareExtensionAbility 的 exported 设为 true 后,别的应用也能访问。正式发布前用 uri 权限校验限制调用方 bundleName,避免数据泄露。
事件命名规范:commonEventManager 的自定义事件名建议用「反向域名 + 语义」格式(如 com.example.downloadcenter.COMPLETE),避免和其他应用冲突;订阅后要记得 unsubscribe,否则页面销毁后回调还在跑,造成内存泄漏。
结语
PC 应用的「专业感」,很大程度来自它对复杂系统的治理能力。把界面、后台、数据拆进不同进程,再用 Want、rpc、commonEventManager、DataShare 把它们织成一张协作网,应用才能在长时间运行、高负载、多任务并行的桌面场景里稳如磐石。
本文从 Stage 模型的进程拓扑出发,依次拆解了 Call 调用、ServiceExtensionAbility 长连接、事件总线与数据共享四大 IPC 武器,最终用一个跨进程下载中心把它们全部串起。所有代码基于 HarmonyOS NEXT(API 12+)编写,建议你把它们真正跑一遍——当你看到三个进程各自独立、却通过代理与事件严丝合缝地协同工作时,对「桌面级应用」的理解会再上一个台阶。
更多推荐




所有评论(0)