事件总线:实现发布/订阅模式的通信库(233)
在鸿蒙(HarmonyOS)原生应用开发中,随着业务模块的增多,组件间、页面间以及线程间的通信需求变得日益复杂。如果采用传统的层层回调或状态透传,会导致代码高度耦合且难以维护。为此,鸿蒙官方提供了一套完善的发布/订阅事件通信机制。
针对不同的通信范围,开发者主要使用以下三种核心方案:
一、 同UIAbility内的轻量级通信:EventHub
EventHub 是鸿蒙提供的一种基于发布-订阅模式的事件中心模块,类似于前端开发中的 EventBus。
- 作用范围:仅适用于同一 UIAbility 内主线程各组件间的通信(如页面间的数据同步、状态回传)。
- 核心机制:每个 UIAbility 的 Context 拥有独立的 EventHub 实例,不同 UIAbility 的 EventHub 互不共享。
- API 极简:仅暴露
on(订阅)、off(取消订阅)和emit(触发事件)三个方法,通过字符串类型的事件名(TAG)进行匹配,无需权限管控,极其轻量。
二、 跨线程的异步事件处理:Emitter
当应用涉及多线程并发(如 Worker、TaskPool)时,EventHub 无法跨线程通信,此时需要使用 Emitter。
- 作用范围:用于同一进程内相同线程或不同线程间的事件处理。
- 核心机制:Emitter 内部维护了一个事件队列,发布事件后,任务队列会串行执行并调用订阅者的回调方法,事件是异步执行的。
- 优先级控制:支持为事件设置优先级(如 IMMEDIATE、HIGH、LOW、IDLE),系统会根据优先级顺序分发事件。
三、 跨应用与系统级事件:CommonEventManager
对于需要跨进程或响应系统状态的场景,需要使用 CommonEventManager。
- 作用范围:用于订阅系统级公共事件(如低电量、网络变化、账号登出)或实现跨应用、跨进程的自定义事件通信。
- 权限管控:支持应用间的安全通信,部分系统事件需要申请相应权限才能订阅。
- 兼容性:同时兼容 FA 模型与 Stage 模型。
四、 EventHub 实战:同 Ability 内的跨组件状态同步
场景:在复杂的页面层级中(如首页卡片、侧边栏、弹窗),组件之间没有直接的父子关系。通过 EventHub 实现一处修改,全局多处实时响应。
import { common } from '@kit.AbilityKit'; // 1. 触发事件的组件(如弹窗内的开关) @Component struct PopupComponent { private eventHub: common.EventHub = getContext(this).eventHub; build() { Button('切换状态').onClick(() => { // 广播事件并携带业务数据 this.eventHub.emit('TOGGLE_STATUS', { status: true }); }) } } // 2. 接收事件的组件(如首页卡片) @Component struct HomeCard { @State status: boolean = false; private eventHub: common.EventHub = getContext(this).eventHub; aboutToAppear(): void { // 订阅事件 this.eventHub.on('TOGGLE_STATUS', (data: Record<string, Object>) => { this.status = data['status'] as boolean; }); } aboutToDisappear(): void { // 【关键】组件销毁时务必取消订阅,防止内存泄漏 this.eventHub.off('TOGGLE_STATUS'); } }五、 Emitter 实战:Worker 子线程与 UI 线程的异步通信
场景:在后台 Worker 线程中进行耗时的数据计算或文件读取,处理完成后将结果安全地传递给 UI 线程进行渲染。
import { emitter } from '@kit.BasicServicesKit'; // 1. UI 线程订阅子线程消息 @Component struct MainPage { @State result: number = 0; aboutToAppear(): void { // 订阅事件,支持优先级设置 const event: emitter.InnerEvent = { eventId: 1001, priority: emitter.EventPriority.HIGH }; emitter.on(event, (eventData: emitter.EventData) => { this.result = eventData.data?.['value'] as number; }); } aboutToDisappear(): void { emitter.off(1001); } } // 2. Worker 线程发布消息(在 worker.ts 中) import { emitter } from '@kit.BasicServicesKit'; const event: emitter.InnerEvent = { eventId: 1001 }; const eventData: emitter.EventData = { data: { value: 99 } }; emitter.emit(event, eventData); // 异步投递到 UI 线程队列六、CommonEventManager 实战:监听系统级公共事件
场景:应用需要实时感知设备的网络状态变化或电量情况,以便在弱网或低电量下动态调整业务策略(如暂停高清视频加载)。
import { commonEventManager } from '@kit.BasicServicesKit'; export class SystemEventMonitor { private subscriber?: commonEventManager.CommonEventSubscriber; // 订阅系统网络变化事件 async subscribeNetworkChange(callback: (isConnected: boolean) => void): Promise<void> { const subscribeInfo: commonEventManager.CommonEventSubscribeInfo = { events: ['usual.event.NETWORK_STATE_UPDATE'] // 系统预定义的网络事件 }; this.subscriber = await commonEventManager.createSubscriber(subscribeInfo); commonEventManager.subscribe(this.subscriber, (err, data) => { if (err) return; // 解析网络状态并回调 const isConnected = data.data === 'CONNECTED'; callback(isConnected); }); } // 释放订阅资源 unsubscribe(): void { if (this.subscriber) { commonEventManager.unsubscribe(this.subscriber); } } }七、 EventHub 进阶:UIAbility 与 UI 界面的双向数据同步
场景:在鸿蒙的 Stage 模型中,UIAbility 与 UI 界面是分离的。当 Ability 层接收到外部消息(如系统通知、跨进程通信)时,需要将其精准推送给正在显示的 UI 界面进行渲染。
// 1. 在 UIAbility 中订阅事件并转发给 UI import { UIAbility, Want } from '@kit.AbilityKit'; export default class EntryAbility extends UIAbility { onCreate(want: Want): void { // Ability 监听外部事件 this.context.eventHub.on('externalDataSync', (data: string) => { // 将数据通过同一个 eventHub 转发给 UI 层 this.context.eventHub.emit('updateUI', data); }); } } // 2. 在 UI 界面中接收并更新状态 @Entry @Component struct SyncPage { @State displayText: string = '等待同步...'; aboutToAppear(): void { const context = getContext(this) as common.UIAbilityContext; context.eventHub.on('updateUI', (data: string) => { this.displayText = data; }); } aboutToDisappear(): void { const context = getContext(this) as common.UIAbilityContext; context.eventHub.off('updateUI'); } build() { Text(this.displayText).fontSize(20) } }八、 Emitter 进阶:基于 Sendable 的跨线程零拷贝对象传递
场景:在多线程并发场景下,如果通过 Emitter 传递庞大的复杂对象,常规的序列化/反序列化会带来严重的性能开销。结合鸿蒙的@Sendable机制,可实现跨线程的高效数据流转。
import { emitter } from '@kit.BasicServicesKit'; // 1. 定义支持跨线程共享的 Sendable 对象 @Sendable class SensorData { public x: number = 0; public y: number = 0; constructor(x: number, y: number) { this.x = x; this.y = y; } } // 2. 封装 Emitter 发送逻辑 class SensorEmitter { static emitSensorData(data: SensorData): void { const innerEvent: emitter.InnerEvent = { eventId: 2001 }; const eventData: emitter.EventData = { data: data }; emitter.emit(innerEvent, eventData); } } // 3. 在 UI 线程接收(无需 JSON 解析,直接获取对象属性) emitter.on({ eventId: 2001 }, (eventData: emitter.EventData) => { const sensor = eventData.data as SensorData; console.info(`接收到传感器数据: X=${sensor.x}, Y=${sensor.y}`); });九、CommonEventManager 进阶:跨应用定向通信与防泄漏
场景:在电商或物联网生态中,主应用需要向特定的桌面卡片(Widget)或子应用发送“订单支付成功”事件。为防止设备上的无关应用监听到该广播导致信息泄漏,必须使用包名定向发布。
import { commonEventManager } from '@kit.BasicServicesKit'; // 发送方:限定接收方的 bundleName export function publishOrderPaid(targetBundleName: string, orderId: string): void { const publishData: commonEventManager.CommonEventPublishData = { bundleName: targetBundleName, // 核心:仅此包名的应用可收到 data: JSON.stringify({ orderId, ts: Date.now() }) }; commonEventManager.publish('com.shop.ORDER_PAID', publishData, (err) => { if (err) { console.error(`发布订单事件失败: ${err.message}`); } }); } // 接收方:普通订阅即可(系统会自动校验 bundleName 是否匹配) const subscribeInfo: commonEventManager.CommonEventSubscribeInfo = { events: ['com.shop.ORDER_PAID'] }; const subscriber = await commonEventManager.createSubscriber(subscribeInfo); commonEventManager.subscribe(subscriber, (err, data) => { if (!err && data) { const payload = JSON.parse(data.data!); console.log(`卡片收到订单: ${payload.orderId}`); } });在落地事件总线机制时,开发者需特别注意以下工程陷阱:
- 严格的生命周期绑定:无论是 EventHub 还是 Emitter,都必须在组件的
aboutToDisappear或页面的onDestroy中调用off取消订阅。未取消的订阅会导致组件实例无法被 GC 回收,引发严重的内存泄漏。 - 跨线程数据序列化限制:使用 Emitter 跨线程传递数据时,传递的对象必须是可序列化的普通对象,严禁传递包含 UI 装饰器(如
@State、@Link)的复杂类型或闭包函数,否则会导致运行时崩溃。 - 避免事件风暴与死循环:在事件回调中再次触发同类事件极易导致无限递归。对于高频触发的事件(如滚动、滑动),必须在业务层加入防抖(Debounce)或节流(Throttle)机制。
- 权限与隐私合规:使用 CommonEventManager 订阅系统级事件时,务必在
module.json5中检查并声明所需的系统权限(如ohos.permission.GET_NETWORK_INFO),否则订阅将静默失败。