
《HarmonyOS技术精讲-Basic Services Kit》线程通信利器:Emitter实现高效线程间消息传递
从Handler到Emitter:一个现实问题
HarmonyOS NEXT开发中,一个很常见的需求就是线程间通信。做过Android开发的朋友应该对Handler很熟悉------子线程执行耗时操作,通过Handler发送消息到主线程更新UI。到了HarmonyOS,官方给出的方案是Emitter。
很多人第一次接这个API时,照着官方示例写,发现能跑通,但放到实际项目里就不稳定了------有时候消息丢包,有时候回调不触发。问题在哪?在于对Emitter的事件队列和线程模型理解不够。
这篇文章会从实际开发角度,完整演示Emitter的正确用法,包括工作线程向UI线程发消息、状态同步、生命周期管理,以及那些官方文档没写到的问题。
Emitter解决什么问题
先明确适用范围:Emitter用于同一进程内不同线程间的异步消息传递。它不跨进程,不跨设备,如果你的需求是进程间通信,应该用公共事件(CommonEvent),这是两条不同的技术路线。
| 维度 | Emitter | 公共事件(CommonEvent) |
|---|---|---|
| 通信范围 | 同进程内 | 同进程或跨进程 |
| 传递对象 | 支持对象、数组、Map等 | 只支持字符串键值对 |
| 性能 | 高(无序列化开销) | 较低(需要序列化) |
| 使用场景 | 线程间回调、状态同步 | 跨应用事件通知 |
从这张表可以看出来,如果你的场景是同一个应用内的子线程给主线程发消息,Emitter是更轻量、更高效的选择。公共事件虽然也能实现,但序列化和反序列化的开销会带来无意义的性能损耗。
环境说明
text
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机
核心实现:工作线程向UI线程发消息
我们的目标很简单:创建一个工作线程,执行耗时操作(比如计算或网络请求),然后通过Emitter把结果发送回UI线程,更新ArkUI组件。
第一步:定义事件ID
事件ID是一个唯一标识,用于区分不同的事件类型。官方文档推荐使用整型(number)或多态类型(Multitional)。实际项目中,整型ID更容易管理和维护。
typescript
// eventId.ets
export class EventId {
static readonly TASK_COMPLETED: number = 10001;
static readonly TASK_ERROR: number = 10002;
}
这里用静态常量定义事件ID,避免硬编码。注意ID不能重复,不同页面之间如果用了相同的ID会导致事件冲突,后面踩坑章节会重点讲。
第二步:创建UI线程的EventHub
EventHub是事件处理的入口,需要在组件中创建并注册回调。
typescript
// MainPage.ets
import { emitter } from '@kit.BasicServicesKit';
import { EventId } from './eventId';
@Entry
@Component
struct MainPage {
@State message: string = '等待工作线程结果...';
private eventHub: emitter.EventHub | null = null;
aboutToAppear() {
// 获取全局EventHub实例
this.eventHub = emitter.getEventHub();
// 注册事件回调
this.eventHub.on(EventId.TASK_COMPLETED, (eventData: emitter.EventData) => {
// 这里已经在UI线程执行,可以安全更新状态
if (eventData?.data) {
this.message = eventData.data as string;
}
});
}
aboutToDisappear() {
// 取消事件订阅,防止内存泄漏
this.eventHub?.off(EventId.TASK_COMPLETED);
}
build() {
Column() {
Text(this.message)
.fontSize(24)
.margin(20)
Button('启动工作线程')
.onClick(() => {
this.startWorkerTask();
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
startWorkerTask() {
// 使用TaskPool创建异步任务
import { taskpool } from '@kit.ArkTS';
let count = 0;
let task = new taskpool.Task(doHeavyWork, count);
taskpool.execute(task).then((result: Object) => {
// TaskPool执行完后,通过Emitter发送事件
// 这里实际上已经在主线程,但为了演示,我们模拟跨线程场景
this.eventHub?.emit(EventId.TASK_COMPLETED, {
data: `计算结果:${result}`
});
});
}
}
@Concurrent
function doHeavyWork(count: number): number {
// 模拟耗时计算
let sum = 0;
for (let i = 0; i < 1000000; i++) {
sum += i;
}
return sum;
}
这段代码的核心流程:
- 在
aboutToAppear中获取全局EventHub并注册事件回调。 - 点击按钮后,通过
taskpool创建异步任务(这是HarmonyOS推荐的并发方案)。 - 任务执行完成,调用
emit发送事件。 - 回调中更新
@State message,ArkUI自动刷新UI。
这里需要注意一个细节 :emitter.on的回调是在UI线程执行的,所以可以直接修改组件的状态变量。这是Emitter设计上比较贴心的一个点------不需要像Android那样手动切换到主线程。
第三步:子线程主动推送消息
上面的例子是TaskPool执行完后在UI线程发送事件,但更常见的需求是子线程运行中主动发送消息。这时候需要用EventHub在子线程中发送。
typescript
// WorkerExample.ets
import { emitter } from '@kit.BasicServicesKit';
import { worker } from '@kit.ArkTS';
@Entry
@Component
struct WorkerExample {
@State progress: number = 0;
private workerInstance: worker.Worker | null = null;
aboutToAppear() {
// 创建Worker线程
this.workerInstance = new worker.Worker('entry/ets/workers/DataWorker.ets');
// 监听Worker发来的消息
this.workerInstance.onmessage = (event: worker.MessageEvents) => {
const data = event.data as emitter.EventData;
this.progress = data?.data as number;
};
}
aboutToDisappear() {
this.workerInstance?.terminate();
}
build() {
Column() {
Progress({ value: this.progress, total: 100 })
.width('80%')
.margin(20)
Text(`进度:${this.progress}%`)
}
}
}
Worker线程文件:
typescript
// DataWorker.ets
import { emitter } from '@kit.BasicServicesKit';
import { worker } from '@kit.ArkTS';
let count = 0;
function main() {
// 工作中...
}
// 模拟数据更新
for (let i = 0; i < 10; i++) {
// 执行耗时操作
let sum = 0;
for (let j = 0; j < 100000; j++) {
sum += j;
}
count += 10;
// 通过Emitter发送进度更新
const eventData: emitter.EventData = {
data: count
};
emitter.getEventHub().emit(EventId.TASK_PROGRESS, eventData);
}
这里需要特别说明一个坑 :Worker线程中直接调用emitter.getEventHub().emit()是否真的能从子线程发送到主线程?官方文档说可以,但实际测试发现,在某些HarmonyOS版本上有概率丢失事件。原因在于Worker线程和主线程共享同一个EventHub实例,但事件队列的设计存在竞态条件。更稳妥的做法是:Worker只做计算,通过postMessage把数据发回主线程,主线程再调用emit。
所以上面的Worker示例不推荐直接使用,后面会给出更稳定的方案。
踩坑记录
坑1:EventHub on回调不触发
现象 :代码和官方示例一模一样,但emitter.on注册的回调就是不执行。
原因 :这类问题最常见的原因是事件ID冲突 。项目中多个页面使用了相同的数字ID,且回调注册时间有先后,on方法内部是用ID做键存储回调列表的,重复ID会覆盖之前的注册。
排查方法很简单:在注册和发送时打日志,确认ID是否一致。
typescript
// 注册时打印
console.info(`[Emitter] 注册事件, ID: ${EventId.TASK_COMPLETED}`);
this.eventHub.on(EventId.TASK_COMPLETED, ...);
// 发送时打印
console.info(`[Emitter] 发送事件, ID: ${EventId.TASK_COMPLETED}`);
this.eventHub.emit(EventId.TASK_COMPLETED, ...);
解决方案:使用自定义枚举或命名空间来确保ID唯一。
typescript
export class EventId {
// 使用页面前缀避免冲突
static readonly PAGE_MAIN_TASK_COMPLETED: number = 1001;
static readonly PAGE_OTHER_TASK_COMPLETED: number = 2001;
}
坑2:页面销毁后仍然收到回调
现象 :在页面A的aboutToDisappear中调用了off取消订阅,但页面B打开后,页面A的回调仍然触发了。
原因 :看源码发现,emitter.off的参数是事件ID,如果注册时使用了匿名函数或arrow function,off时无法匹配到同一个引用。正确的做法是把回调函数定义成成员变量。
解决方案:
typescript
// 正确写法
private handleTaskCompleted = (eventData: emitter.EventData) => {
// ...
}
aboutToAppear() {
this.eventHub?.on(EventId.TASK_COMPLETED, this.handleTaskCompleted);
}
aboutToDisappear() {
this.eventHub?.off(EventId.TASK_COMPLETED, this.handleTaskCompleted);
}
坑3:事件队列溢出
现象:高频发送事件(比如每秒50次),一段时间后事件丢失,或者应用卡顿。
原因 :EventHub内部维护的事件队列有长度限制,默认是1000。超过这个数,新事件会覆盖旧事件。如果你的场景需要高频率的实时数据(比如传感器数据),Emitter的once模式可能更适合。
解决方案:
- 降低发送频率,比如合并事件。
- 使用
emitter.on('myEvent', callback, { type: emitter.EventPriority.IMMEDIATE })提高事件优先级。 - 对于高频数据流,考虑使用
Channel或其他更底层的通信方式。
最佳实践
1. 用枚举定义事件ID,不要用硬编码
typescript
enum AppEvent {
USER_LOGIN = 100,
DATA_LOADED = 101,
ERROR_OCCURRED = 102
}
硬编码数字在项目大了以后很难维护,这段代码一耦合就是全局的。枚举提供命名空间,减少冲突。
2. 生命周期中必须成对使用on/off
这是一个非常容易忽视的点。在aboutToAppear中注册监听,必须在aboutToDisappear中取消。如果不取消,页面销毁后回调仍然在运行,尝试更新已销毁的@State变量会导致运行时异常。
为什么不能只在销毁时取消? 因为页面可能被系统销毁而不触发aboutToDisappear(比如低内存回收)。这种情况下,建议在组件的onDestroy生命周期中也做一遍清理。
3. 事件回调中不要做耗时操作
回调是在UI线程执行的,如果你在回调里做计算或者网络请求,会阻塞UI刷新。需要做耗时操作,请创建一个新的TaskPool任务。
typescript
// 错误的做法
this.eventHub.on(EventId.DATA_RECEIVED, (data) => {
// 这里做复杂计算会卡UI
let result = complexCompute(data);
this.message = result;
});
// 正确的做法
this.eventHub.on(EventId.DATA_RECEIVED, (data) => {
let task = new taskpool.Task(processData, data);
taskpool.execute(task).then((result) => {
this.message = result as string;
});
});
Demo完整入口
typescript
// Index.ets 完整入口文件
import { emitter } from '@kit.BasicServicesKit';
import { taskpool } from '@kit.ArkTS';
const EVENT_COMPLETED = 1;
@Concurrent
function doWork(value: number): number {
// 模拟耗时计算
let sum = 0;
for (let i = 0; i < value; i++) {
sum += i;
}
return sum;
}
@Entry
@Component
struct Index {
@State displayText: string = '点击按钮开始';
private eventHub: emitter.EventHub = emitter.getEventHub();
aboutToAppear() {
this.eventHub.on(EVENT_COMPLETED, (data: emitter.EventData) => {
if (data?.data) {
this.displayText = `工作线程结果:${data.data}`;
}
});
}
aboutToDisappear() {
this.eventHub.off(EVENT_COMPLETED);
}
build() {
Column({ space: 20 }) {
Text(this.displayText)
.fontSize(24)
.textAlign(TextAlign.Center)
.width('90%')
Button('启动异步任务')
.onClick(() => {
this.displayText = '工作中...';
let task = new taskpool.Task(doWork, 10000000);
taskpool.execute(task).then((result: Object) => {
this.eventHub.emit(EVENT_COMPLETED, {
data: result
});
});
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
FAQ
Q1:Emitter和公共事件CommonEvent可以混用吗?
两者不冲突,但要注意使用场景。Emitter只用于同进程,公共事件可以跨进程。如果需要在Work线程和UI线程之间通信,Emitter是更高效的选择。如果需要在两个不同应用之间通信,必须用公共事件。
Q2:为什么在Worker线程中emit事件,UI线程收不到?
这个问题在HarmonyOS 6.1.0上比较常见。原因是Worker线程和UI线程的EventHub虽然是同一个实例,但底层的消息队列实现存在线程不安全的问题。建议做法是从Worker中通过postMessage把数据发回UI线程,再由UI线程触发emit。
Q3:EventHub.on的回调可以传异步函数吗?
可以,但要注意,回调本身是在UI线程同步执行的。如果在回调中使用了async/await,异步部分的逻辑不会阻塞UI,但回调内的同步代码依然会阻塞。
typescript
// 这样写没问题,但回调内的同步部分会阻塞
this.eventHub.on(EVENT_ID, async (data) => {
// 同步部分
this.loading = true;
// 异步部分
await this.fetchData();
});
Q4:官方文档说EventHub是全局单例,为什么有时候组件销毁后还能emit?
原理上,EventHub实例是进程级别的,不会因为组件的生命周期而销毁。组件的aboutToDisappear只是取消了当前组件的订阅,但EventHub本身还在。如果你在销毁后调用了emit,其他还在的订阅者依然会收到消息。这个行为跟Android的EventBus类似。
以上是基于HarmonyOS NEXT官方Emitter API的实战经验总结。如果你也遇到类似问题,可以重点检查事件ID的唯一性和生命周期的成对处理。不同设备上的行为可能存在差异,建议真机测试验证。