《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 = true且data = '')来覆盖缓存 - 订阅器中加入版本号校验,避免重复处理同一事件
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 变更记录。粘性事件在较新的版本中行为有些微调整,建议先在小范围测试。