《HarmonyOS技术精讲-Basic Services Kit》线程通信利器:Emitter实现高效线程间消息传递

《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;
}

这段代码的核心流程:

  1. aboutToAppear中获取全局EventHub并注册事件回调。
  2. 点击按钮后,通过taskpool创建异步任务(这是HarmonyOS推荐的并发方案)。
  3. 任务执行完成,调用emit发送事件。
  4. 回调中更新@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 functionoff时无法匹配到同一个引用。正确的做法是把回调函数定义成成员变量。

解决方案

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的唯一性和生命周期的成对处理。不同设备上的行为可能存在差异,建议真机测试验证。

相关推荐
心中有国也有家2 小时前
AtomGit Flutter 鸿蒙客户端:Canvas 绘制进阶-路径、渐变与混合模式
android·javascript·flutter·华为·harmonyos
w139548564222 小时前
鸿蒙应用开发实战【93】— 实战短信批量导入完整流程
华为·harmonyos·鸿蒙系统
Georgewu4 小时前
【HarmonyOS 三方库 】Python 三方库鸿蒙化适配 PC 完整迁移实战
harmonyos
花开彼岸天~4 小时前
鸿蒙应用开发实战【94】— 实战多卡号场景管理最佳实践
android·华为·harmonyos·鸿蒙系统
花开彼岸天~4 小时前
鸿蒙应用开发实战【97】— 应用发布AGC上架全流程
华为·harmonyos·鸿蒙系统
绝世番茄4 小时前
HarmonyOS Next 深入解析:Scroll 组件与 onScroll 滚动监听实战
华为·harmonyos·鸿蒙
2501_919749034 小时前
华为鸿蒙免费铃声APP—小羊免费铃声
华为·harmonyos
不羁的木木4 小时前
《HarmonyOS技术精讲-NearLink Kit(星闪服务)》第3篇:数据收发——点对点数据传输实战
pytorch·华为·harmonyos
木木子224 小时前
[特殊字符] 种菜模拟器 — 鸿蒙ArkTS完整技术解析
microsoft·华为·harmonyos