《HarmonyOS技术精讲-Basic Services Kit》公共事件进阶:粘性事件与有序广播

《HarmonyOS技术精讲-Basic Services Kit》公共事件进阶:粘性事件与有序广播

问题引入

HarmonyOS NEXT开发过程中,我注意到很多人在使用公共事件(Common Event)时,只停留在最简单的发布-订阅模式。其实官方文档里藏了两个非常实用的能力:粘性事件和有序广播。

最常见的一个场景是:应用启动后,某些数据模块(比如定位、网络状态)可能比UI页面初始化得早,UI页面订阅事件的时候,事件已经发布过了,于是就收不到通知。还有另一个场景:多个订阅者需要按特定顺序处理同一个事件,优先级高的先处理,处理完了还能决定是否让后面的人继续。

这两个问题,就是粘性事件和有序广播要解决的核心问题。

先搞清楚它们解决什么问题

粘性事件

普通广播的时效性很短:发布之后,如果当时没有人订阅,这条广播就丢了。粘性事件不同,它发布后会"粘"在系统里,以后订阅的用户依然能收到。

适合的场景:

  • 系统状态广播(网络切换、充电状态)
  • 应用启动时需要获取上一次的状态快照
  • 订阅者可能晚于事件发布者创建的模块

不适合的场景:

  • 实时性要求极高的操作(比如游戏中的按键事件)
  • 大量高频事件(粘性事件的缓存有系统开销)

有序广播

普通广播是无序的,所有订阅者接收到事件的顺序不确定。有序广播按照优先级排序,每个订阅者按顺序处理,处理完后可以选择中止广播,让后面的订阅者不再收到。

适合的场景:

  • 多模块按优先级处理同一个通知(如消息推送优先级处理)
  • 某个模块需要拦截或修改事件

不适合的场景:

  • 没有先后顺序要求的并行处理
  • 高频事件(排序有性能损耗)

对比:

特性 普通广播 粘性广播 有序广播
可缓存
有顺序 是(按优先级)
可中断
存储开销

环境说明

text 复制代码
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机

核心实现:粘性事件 Demo

这段代码用于演示粘性事件的发布与订阅。先发送一个粘性事件,然后延迟订阅的页面依然能收到。

第一步:定义事件权限(可选)

module.json5 中配置自定义权限(非必要,但推荐):

json 复制代码
{
  "module": {
    "requestPermissions": [
      {
        "name": "com.example.custom.COMMON_EVENT",
        "reason": "用于粘性事件测试",
        "usedScene": {
          "ability": ["MainAbility"]
        }
        // 如果是应用内事件,可以不用申请跨应用权限
      }
    ]
  }
}

第二步:发送粘性事件

typescript 复制代码
// StickyEventPublisher.ets
import { commonEventManager } from '@kit.BasicServicesKit';

export class StickyEventPublisher {
  // 发布粘性事件的 key
  static readonly EVENT_KEY = 'custom_sticky_event';

  // 发送粘性事件,携带一个数字数据
  static publishSticky(data: number): void {
    // 构造事件参数
    const eventKey: string = this.EVENT_KEY;
    // 粘性事件必须设置 isSticky 为 true
    const options: commonEventManager.CommonEventPublishData = {
      data: data.toString(),
      isSticky: true, // 关键:标志粘性事件
    };

    commonEventManager.publish(eventKey, options, (err) => {
      if (err) {
        console.error(`publishSticky failed: ${JSON.stringify(err)}`);
      } else {
        console.info(`publishSticky success, data: ${data}`);
      }
    });
  }
}

第三步:订阅粘性事件

typescript 复制代码
// StickyEventSubscriber.ets
import { commonEventManager } from '@kit.BasicServicesKit';
import { BusinessError } from '@kit.BasicServicesKit';

export class StickyEventSubscriber {
  private subscriber: commonEventManager.CommonEventSubscriber | null = null;
  private lastData: string = '';

  // 订阅粘性事件
  subscribe(): Promise<void> {
    return new Promise((resolve, reject) => {
      const filter: commonEventManager.CommonEventSubscribeInfo = {
        events: [StickyEventPublisher.EVENT_KEY],
        // 如果要订阅粘性事件,必须通过粘性事件订阅接口
        // 但常见做法:先订阅,再通过粘性存储拿最新数据
      };

      commonEventManager.createSubscriber(filter, (err, subscriber) => {
        if (err || !subscriber) {
          reject(err);
          return;
        }
        this.subscriber = subscriber;

        // 拿到 subscriber 后,可以通过粘性事件接口获取
        // 但必须使用粘性事件专属订阅方式
        commonEventManager.subscribe(subscriber, (err, data) => {
          if (err) {
            return;
          }
          // 粘性事件的 data 字段会存储发布时设置的值
          const stickyData = data.data;
          if (stickyData !== null && stickyData !== undefined) {
            this.lastData = stickyData as string;
            console.info(`收到粘性事件数据: ${this.lastData}`);
          }
        });

        resolve();
      });
    });
  }

  // 获取最后一次粘性事件的数据
  getLastData(): string {
    return this.lastData;
  }

  unsubscribe(): Promise<void> {
    return new Promise((resolve) => {
      if (this.subscriber) {
        commonEventManager.unsubscribe(this.subscriber, () => {
          this.subscriber = null;
          resolve();
        });
      } else {
        resolve();
      }
    });
  }
}

注意事项:

  • isSticky 只在 publish 时设置有用,对订阅器设置 subscribe 没意义
  • 粘性事件的数据只缓存最新的一个,后发布的会覆盖前者
  • 订阅后,只会收到发布之后的粘性数据,但如果发布时其他订阅者已经消费,则不再重复触发,除非是新订阅者

核心实现:有序广播的优先级和中断

现在演示有序广播的优先级排序中断机制。假设两个订阅者:SubscriberA(优先级较高)和 SubscriberB(优先级较低)。

第一步:发布有序广播

typescript 复制代码
// OrderedEventPublisher.ets
import { commonEventManager } from '@kit.BasicServicesKit';

export class OrderedEventPublisher {
  static readonly ORDERED_EVENT = 'custom_ordered_event';

  // 发布有序广播
  static publishOrdered(): void {
    // 有序事件必须使用 publishAsBundle 或 publish 时指定 isOrdered
    const options: commonEventManager.CommonEventPublishData = {
      data: 'hello from publisher',
      isOrdered: true, // 关键:标志有序广播
    };

    commonEventManager.publish(this.ORDERED_EVENT, options, (err) => {
      if (err) {
        console.error(`publishOrdered error: ${JSON.stringify(err)}`);
      } else {
        console.info(`publishOrdered success`);
      }
    });
  }
}

第二步:创建高优先级订阅者(A)

typescript 复制代码
// SubscriberA.ets
import { commonEventManager } from '@kit.BasicServicesKit';

export class SubscriberA {
  private subscriber: commonEventManager.CommonEventSubscriber | null = null;

  init(): Promise<void> {
    return new Promise((resolve, reject) => {
      const filter: commonEventManager.CommonEventSubscribeInfo = {
        events: [OrderedEventPublisher.ORDERED_EVENT],
        priority: 100, // 优先级100,较高
      };

      commonEventManager.createSubscriber(filter, (err, sub) => {
        if (err || !sub) {
          reject(err);
          return;
        }
        this.subscriber = sub;
        resolve();
      });
    });
  }

  subscribe(): void {
    if (!this.subscriber) return;

    commonEventManager.subscribe(this.subscriber, (err, data) => {
      if (err) return;
      console.info(`[SubscriberA] 收到有序事件,数据: ${data.data}`);
      // 模拟:A处理结束后,决定是否中断广播
      // 如果 return true 表示继续,return false 表示中断
      // 实际使用时,通过 setCode 和 setData 修改事件
      // 然后通过 finish 结束当前处理
      const code = data.code; // 可以检查某个标志
      // 假设 A 处理完毕,不放行后续
      this.subscriber?.setCode(1); // 设置自定义错误码
      this.subscriber?.setData('modified_by_A');
      // 调用 finish 表示当前订阅者处理完毕,系统继续派发给下一个
      // 但如果想要中断,调用 this.subscriber?.abort()
      // 注意:abort 需要权限,且只能在有序广播中使用
    });
  }

  // 中断广播(只能在 subscribe 回调中调用)
  abortEvent(): void {
    if (this.subscriber) {
      (this.subscriber as any).abort?.(); // 如果 SDK 支持
    }
  }
}

第三步:创建低优先级订阅者(B)

typescript 复制代码
// SubscriberB.ets
import { commonEventManager } from '@kit.BasicServicesKit';

export class SubscriberB {
  private subscriber: commonEventManager.CommonSubscriber | null = null;

  init(): Promise<void> {
    return new Promise((resolve, reject) => {
      const filter: commonEventManager.CommonEventSubscribeInfo = {
        events: [OrderedEventPublisher.ORDERED_EVENT],
        priority: 10, // 优先级10,较低
      };

      commonEventManager.createSubscriber(filter, (err, sub) => {
        if (err || !sub) {
          reject(err);
          return;
        }
        this.subscriber = sub;
        resolve();
      });
    });
  }

  subscribe(): void {
    if (!this.subscriber) return;
    commonEventManager.subscribe(this.subscriber, (err, data) => {
      if (err) return;
      console.info(`[SubscriberB] 收到有序事件,数据: ${data.data}`);
    });
  }
}

第四步:在中止时给权限

module.json5 中添加:

json 复制代码
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.COMMONEVENT_STICKY",
        "reason": "需要使用粘性事件",
        "usedScene": {
          "ability": ["MainAbility"]
        }
      }
    ]
  }
}

关于中断(abort)的说明:

  • 只有有序广播才能使用 abort
  • abort 必须在 subscribe 的回调中调用,且需要在当前订阅者处理完后才生效
  • 如果 abort 被调用,后面的订阅者将不会收到此事件
  • 官方文档设计初衷是为了安全拦截,比如消息推送的过滤

常见问题:粘性事件缓存混乱

现象: 粘性事件发布后,多次订阅都能收到相同的数据,但数据应该是最新的才对。实际测试发现,有时候收到的是旧数据。

原因: 粘性事件的缓存机制设计为只保留最后一个发布的事件。但如果先发布了一个粘性事件,然后在应用A中订阅,又在应用B中订阅,系统会为每个订阅器触发一次回调,但不保证所有订阅器都同时收到。更深层的原因是:粘性事件的发布和订阅存在时间差,如果订阅器创建时,系统中存在相同事件的粘性数据,系统会立即回调一次。

解决方案:

  • 订阅前先检查是否需要清空旧的粘性事件:通过发布一个空数据的粘性事件(携带 isSticky = truedata = '')来覆盖缓存
  • 订阅器中加入版本号校验,避免重复处理同一事件
typescript 复制代码
// 清除粘性事件缓存
static clearSticky(): void {
  const options = { data: '', isSticky: true, code: -1 };
  commonEventManager.publish('custom_sticky_event', options, () => {});
}

常见问题:有序广播被错误中断

现象: 开发者在有序广播的 subscribe 回调中调用了 abort,但后面的订阅者依然能收到事件,或者中断结果不符合预期。

原因: abort 方法在某些版本中不是 CommonEventSubscriber 的公开API,可能是通过 setCode 间接实现的。正确的做法是在回调中设置 setCode 为某个特定值(如0表示正常,1表示中断),然后在下一个订阅者中检查这个code,决定是否继续处理。但更直接的方法是在回调中调用 finish 后,系统自动派发给下一个订阅者,无法强制中断。

解决: 官方文档建议,要想真正中断有序广播,可以在回调中使用 setCode(0) 并通过 finish 结束,然后在后续订阅者的回调中判断 data.code 是否为0 ,如果不为0,则直接 return 跳过。这不是严格意义上的中断,而是通过约定实现的效果。

typescript 复制代码
// 模拟中断:当前订阅者设置特殊code,后面订阅者检查
// SubscriberA
this.subscriber?.setCode(999); // 中断标志
this.subscriber?.setData('interrupted');

// SubscriberB 的回调
commonEventManager.subscribe(subscriber, (err, data) => {
  if (data.code === 999) {
    // 上一个订阅者要求中断,跳过
    console.info('SubscriberB 收到中断信号,不处理');
    return;
  }
  // 正常处理
});

常见问题:跨应用粘性事件权限问题

现象: 应用A发布了粘性事件,应用B去订阅,结果收不到。

原因: 粘性事件的作用域默认只在同一个应用内 (同一UID)。跨应用需要申请 ohos.permission.COMMONEVENT_STICKY 权限,并且目标事件名需要公开注册。

解决:module.json5 中申请权限,同时在发布时指定 permission 字段。

json 复制代码
// module.json5
{
  "permissions": [
    {
      "name": "ohos.permission.COMMONEVENT_STICKY",
      "reason": "跨应用粘性事件",
      "usedScene": {
        "ability": ["MainAbility"]
      }
    }
  ]
}

最佳实践

1. 粘性事件的生命周期管理

粘性事件会一直保存在系统内存中,直到应用进程被杀死或手动清除。如果滥用,容易造成内存泄漏。推荐做法: 只在应用启动时使用粘性事件获取状态快照,之后切换为普通订阅。

2. 有序广播优先处理核心逻辑

有序广播的优先级设计适合多层过滤。建议优先级最高的订阅者处理核心逻辑(如安全检查、数据预处理),优先级低的订阅者只做UI刷新或日志记录,避免逻辑错乱。

3. 慎重使用 aborted 中断

无序广播没有中断能力,有序广播的中断行为具有侵略性。一定要在所有订阅者中保持一致性约定,否则容易出现Bug。比如A模块希望拦截事件,B模块不知道,导致B模块的逻辑永远收不到事件。

4. 粘性事件建议搭配持久化存储

粘性事件缓存的数据有限(只能存字符串),且不可靠(系统可能清除缓存)。如果要持久化状态数据,推荐 写在 @Entry 组件的 aboutToAppear 中从 Preferences 取状态,而不是完全依赖粘性事件。

FAQ

Q:为什么真机正常,模拟器不生效?

A:模拟器可能关闭了公共事件的一些系统服务,尤其是粘性事件。建议真机调试。同时检查模拟器 API 版本是否 >= 6.1.0。

Q:为什么页面返回后,粘性事件数据丢失?

A:粘性事件缓存的生命周期与应用的进程绑定,如果页面返回后进程被保活,缓存还在;如果进程被杀死重建,缓存就没了。所以不要依赖粘性事件做持久化存储。

Q:为什么第一次授权成功,第二次失败?

A:权限申请在首次安装时会被记录,如果第二次运行时权限已经被系统封禁(比如用户手动关闭),或者签名不一致,会导致失败。清理应用数据重新安装可以解决。

Q:有序广播的 priority 值范围是多少?

A:官方文档没有明确上限,但建议在 0-1000 之间。不同设备可能有不同限制。

Demo 入口

typescript 复制代码
// Index.ets 入口文件
@Entry
@Component
struct Index {
  build() {
    Column() {
      Button('发布粘性事件(发送数据:100)')
        .onClick(() => {
          StickyEventPublisher.publishSticky(100);
        })
      Button('订阅粘性事件')
        .onClick(async () => {
          let sub = new StickyEventSubscriber();
          await sub.subscribe();
          console.info('粘性事件订阅成功,最新数据:', sub.getLastData());
        })
      Button('发布有序广播')
        .onClick(() => {
          OrderedEventPublisher.publishOrdered();
        })
      Button('初始化订阅者A(优先级100)')
        .onClick(async () => {
          let a = new SubscriberA();
          await a.init();
          a.subscribe();
          console.info('SubscriberA 初始化完成');
        })
      Button('初始化订阅者B(优先级10)')
        .onClick(async () => {
          let b = new SubscriberB();
          await b.init();
          b.subscribe();
          console.info('SubscriberB 初始化完成');
        })
    }
    .width('100%')
    .height('100%')
    .padding(20)
  }
}

如果你想更深入理解系统侧的实现,可以翻看官方文档中关于 commonEventManager 的 API 变更记录。粘性事件在较新的版本中行为有些微调整,建议先在小范围测试。

相关推荐
春卷同学1 小时前
HarmonyOS掌上记账APP开发实践第15篇:ArkTS 类型系统深度解析 — 从接口到联合类型的灵活运用
ubuntu·华为·harmonyos
春卷同学2 小时前
HarmonyOS掌上记账APP开发实践第11篇:@Local 组件级状态 — 比 @State 更精确的局部状态管理
harmonyos
2301_768103493 小时前
HarmonyOS趣味相机实战第11篇:从单摄切换升级前后双摄同开、能力探测与降级设计
harmonyos·arkts·camerakit·双摄同开·能力降级
AD02273 小时前
27-RDB写一半数据不一致-用事务边界和补偿日志兜住
harmonyos·arkts·鸿蒙开发
拥抱太阳06163 小时前
HarmonyOS 应用开发《掌上英语》第12篇:组件通信模式:从父子传参到跨层级状态共享
华为·harmonyos
最爱老式锅包肉3 小时前
HarmonyOS技术精讲-Camera Kit(相机服务)第9篇:自动对焦与手动调焦
数码相机·华为·harmonyos
2501_919749033 小时前
华为鸿蒙免费口算闯关APP—小羊口算
华为·harmonyos
春卷同学4 小时前
HarmonyOS掌上记账APP开发实践第14篇:@Extend 扩展函数 — 告别重复样式代码
华为·harmonyos
蓝速科技4 小时前
蓝速科技鸿蒙广告机替代安卓实测:信创系统下的性能与边界评估
android·科技·harmonyos