ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

HarmonyOS EntryAbility全局监听器实战:网络状态与前后台切换

HarmonyOS EntryAbility全局监听器实战:网络状态与前后台切换 刚接触Stage模型的时候我踩过一个很典型的坑把每个页面需要用的监听逻辑都写在对应页面里结果网络抖动时所有页面一起弹Toast前后台来回切换时草稿保存逻辑重复执行往外跳转的参数在冷启动和热启动场景下还总对不上。后来才想明白应用级的状态监听不应该散落各地必须收口到一个全局的位置。而HarmonyOS里最合适的收口位置就是EntryAbility。这篇基于实际项目经验把EntryAbility里实现全局监听器的完整思路、三种实现方式、一个可复用的网络状态前后台切换案例以及我踩过的坑一次讲清楚。适合已经开始用Stage模型做鸿蒙应用开发、想把全局逻辑做得更规范的开发者。1. EntryAbility里实现全局监听器的整体思路1.1 先搞清楚EntryAbility在生命周期里的位置在HarmonyOS的Stage模型中应用由一个或多个HAP组成每个HAP里可以声明多种Ability。而EntryAbility是入口HAP中唯一被标记为Entry的UIAbility承担着应用主入口的职责。它和普通页面的Ability最大的区别在于它几乎是整个应用进程最早被创建、最晚被销毁的UIAbility。来看一下完整的生命周期顺序冷启动onCreate→onWindowStageCreate→onForeground热启动应用已在后台再次拉起onNewWant→onForeground退到后台onBackground销毁onWindowStageDestroy→onDestroy冷启动时的onCreate在整个Ability实例生命周期里只执行一次这个“一次”非常关键。它决定了你把全局监听器注册在onCreate里是安全的不会因为页面反复打开而重复注册。但同时也要意识到如果监听器在进程运行过程中失效了它也只能等下一次冷启动才会重新注册回来所以注销和异常恢复逻辑要想清楚。EntryAbility一般会重写这几个生命周期回调但很多项目只用来加载首页全局逻辑完全没有利用起来。实际上它就是天然的“全局监听器容器”——所有需要在应用级别感知的事件都从这里进、从这里出。1.2 哪些监听器真正属于“全局”不是所有事件都适合放进EntryAbility。我在项目里尝试过之后把真正值得放在这里的全局监听器拆成三类。监听类型典型事件推荐实现方式应用级状态冷启动/热启动/前后台切换、启动参数解析覆盖Ability生命周期回调系统级事件网络状态变化、公共事件、配置变化commonEventManager、connection、onConfigurationUpdate业务级事件跨模块通信、全局消息广播emitter、EventHub先说第一类应用级状态监听。比如区分冷启动和热启动、应用退到后台时需要统一保存草稿、应用回到前台时需要刷新数据。这类事件系统已经给出了固定回调位置直接覆盖对应生命周期方法即可。第二类是系统级事件比如网络状态、低电量、时区变化。这类事件的特点是页面组件根本拿不到统一回调只有在一个所有页面共享的位置注册才能保证任意页面都能感知到。这个位置就是EntryAbility。第三类是业务级事件比如用户登录状态变化、全局弹窗请求、消息中心通知。这类事件本质上是给业务总线注册回调emitter和EventHub是主要工具。把这三类搞清楚了就不会再把页面滚动、组件生命周期这类页面级事件也塞进全局监听器导致代码链路无谓地变长。1.3 什么场景不适合放在EntryAbility有些事情从设计上看是“全局的”但实际不适合放在Ability入口里。第一页面专属的监听。某个列表的滚动状态、某个自定义组件的可见性变化、某个弹窗的显示时机这些和具体页面强绑定的状态放进全局只会让回调判断变得复杂。你在全局收到事件后还得判断“当前到底哪个页面在栈顶”远不如页面自己处理来得直接。第二生命周期和页面绑定的监听。如果一个监听器只在某个页面存活期间有意义放在页面里更可控页面销毁随之注销不需要Ability层去维护复杂的生命周期对应关系。第三需要频繁读写UI状态的监听。全局回调里如果需要直接操作某个页面组件还必须通过路由或状态管理去定位目标页面链路很长。这种情况更适合用页面内部的处理机制。我见过一种反向极端的代码所有事件都订阅在Ability层页面里的所有交互都通过全局总线传递最后代码里充斥着一堆“当前是否处于某个页面”的标记排查问题极其痛苦。全局监听器的正确姿势是“收口全局事件、放行页面事件”这个边界先想明白后续实现才不会乱。2. EntryAbility里实现全局监听器的三种方式2.1 方式一直接覆盖生命周期回调处理应用级状态最直接、最不容易出错的“全局监听器”其实是Ability本身的生命周期方法。它们天然就是全局的并且系统保证回调顺序。import { Ability, Want, AbilityConstant } from kit.AbilityKit; import { hilog } from kit.PerformanceAnalysisKit; const DOMAIN 0x0000; const TAG GlobalListener; export default class EntryAbility extends Ability { onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { hilog.info(DOMAIN, TAG, onCreate, launchReason: %{public}d, launchParam.launchReason); if (launchParam.launchReason AbilityConstant.LaunchReason.COLD) { // 冷启动首次打开需要做数据迁移、版本检查、隐私弹窗等 this.handleColdStart(want); } else if (launchParam.launchReason AbilityConstant.LaunchReason.CONTINUE) { // 从任务流转恢复可能需要恢复页面状态 this.handleContinueLaunch(want); } } onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void { // 应用已经存在被其他入口重新拉起时回调 this.handleAppReload(want); } onForeground(): void { // 回到前台统一解锁、刷新标记 } onBackground(): void { // 退到后台统一保存草稿、暂停播放 } onDestroy(): void { // 注销所有全局监听留给其他方式用 } private handleColdStart(want: Want): void { // 解析want参数执行冷启动初始化 } private handleAppReload(want: Want): void { // 解析want参数转发给目标页面 } }这个方式最大的优点是不需要手动维护“订阅/取消订阅”的配对关系系统自动管理。它解决的是“应用什么时候启动、什么时候切换前后台”这类基础状态问题是整个全局监听体系的地基。2.2 方式二用EventHub在Ability内部做事件分发EventHub是UIAbility内置的公共事件总线通过context.eventHub获取。它解决的场景是同一个Ability实例内部多个模块之间的事件共享比如页面A通知页面B刷新或者某个子模块上报状态给页面。它的生命周期和Ability绑定不需要引入额外依赖。import { common } from kit.AbilityKit; // 在Ability的onCreate里获取EventHub并注册 const eventHub this.context.eventHub; eventHub.on(network_state_change, (state: boolean) { hilog.info(DOMAIN, TAG, network state change: %{public}s, state); }); // 某个子模块触发 eventHub.emit(network_state_change, true);EventHub的优势有两个一是轻量不需要额外依赖二是事件不会出Ability隔离性好不容易被应用外的组件误触发。不过它也有明显的边界无法跨Ability实例。如果你的应用是单Ability多页面架构用EventHub就够了。一旦涉及多个UIAbility比如跳转到了另一个Ability页面或者需要和应用外组件通信就得用emitter。用EventHub还有一个关键细节on注册的回调里如果引用了页面对象页面销毁时必须手动off否则会持有已销毁的页面导致内存泄漏。稳妥的做法是Ability层只放不依赖具体页面的监听依赖页面的监听交给页面自己管理。2.3 方式三用emitter注册全局业务事件监听emitter是系统提供的事件总线接口可以理解为跨组件、跨Ability的事件发布订阅中心。和EventHub相比它支持粘性事件这个能力在全局监听场景下非常实用。import { emitter } from kit.BasicServicesKit; import { BusinessError } from kit.BasicServicesKit; interface NetworkStatusData { isOnline: boolean; networkType: string; } // 在onCreate里注册 private onNetworkChange (data: emitter.EventData) { const eventData data.data as NetworkStatusData; hilog.info(DOMAIN, TAG, network change: %{public}s, JSON.stringify(eventData)); }; emitter.on(NETWORK_STATUS_CHANGE, this.onNetworkChange); // 发送端 emitter.emit(NETWORK_STATUS_CHANGE, { data: { isOnline: true, networkType: WIFI } });emitter的粘性事件特性值得单独说明。把某个状态事件设为粘性后新订阅方注册时会立刻收到最近一次的事件数据。相当于给监听器做了一层“快照缓存”。比如网络状态是全局的页面在aboutToAppear里订阅时就能立刻拿到当前网络状态不需要等下一次网络变化事件也不需要单独请求一次查询接口。使用时有两个注意点。一是事件key是普通字符串两侧写错一个字符都不会有编译报错但订阅方永远收不到事件。最好把所有事件key收敛到常量文件里统一管理。二是同一个key反复on会堆叠多个回调导致一个事件触发多次响应建议在注册前先off一次或者用函数引用而不是匿名函数。三种方式其实是层层递进的生命周期回调处理系统状态EventHub处理Ability内事件emitter处理跨模块、跨Ability业务事件。实际项目很少只用一种下面用一个完整案例展示组合用法。3. 实现案例网络状态 前后台切换的全局监听这个案例来自我维护的一个资讯类App需求有三条应用内任意页面都能感知网络状态断网时全局提示恢复时自动隐藏。应用退到后台自动保存草稿回到前台刷新首页数据。页面之间不能互相调用所有状态变化走统一事件入口。3.1 在EntryAbility里注册网络状态监听网络状态监听使用kit.NetworkKit的connection模块。在onCreate里创建NetConnection注册回调回调里主动查询一次当前网络能力再把结果通过emitter广播出去。import { connection } from kit.NetworkKit; import { emitter } from kit.BasicServicesKit; const NETWORK_STATUS_KEY NETWORK_STATUS_CHANGE; export default class EntryAbility extends Ability { private netConnection: connection.NetConnection | null null; onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { this.initGlobalNetworkListener(); } private initGlobalNetworkListener(): void { this.netConnection connection.createNetConnection(); // 注册网络变化回调 this.netConnection.register(() { // 网络发生变化重新查询当前网络能力 this.netConnection?.getNetCapabilities((err, capabilities) { if (!err capabilities) { const isOnline capabilities.bearerTypes.length 0; // 根据bearerTypes判断网络类型 let networkType UNKNOWN; if (capabilities.bearerTypes.includes(connection.NetBearType.BEARER_WIFI)) { networkType WIFI; } else if (capabilities.bearerTypes.includes(connection.NetBearType.BEARER_CELLULAR)) { networkType CELLULAR; } // 广播给所有订阅了该事件的模块 emitter.emit(NETWORK_STATUS_KEY, { data: { isOnline, networkType } }); } }); }); } onDestroy(): void { if (this.netConnection) { this.netConnection.unregister(); this.netConnection null; } super.onDestroy(); } }这里有两点要说清楚。第一网络变化的register回调通常只是一个信号告诉你“网络发生了变化”具体变化成什么样需要重新查询网络能力获取。这个查询动作放在回调里执行能拿到最新的网络状态用bearerTypes判断是否连接、是Wi-Fi还是移动数据。第二注销动作放在onDestroy里执行。UIAbility的onDestroy在正常的页面栈清空时会触发但系统杀进程时不保证执行。所以如果你的监听器持有的资源非常重要比如需要释放一个系统级订阅还要额外关注onStop等进程级回调单纯依赖onDestroy是不够的。3.2 前后台切换的全局感知前后台切换不需要额外注册直接覆盖onForeground和onBackground在方法里把状态广播出去。这个设计的好处是页面侧不需要自己感知生命周期只需要订阅事件。export default class EntryAbility extends Ability { private isAppInForeground false; onForeground(): void { super.onForeground(); this.isAppInForeground true; emitter.emit(APP_STATE_CHANGE, { data: { state: FOREGROUND } }); } onBackground(): void { super.onBackground(); this.isAppInForeground false; // 统一保存全局草稿 this.saveGlobalDraft(); emitter.emit(APP_STATE_CHANGE, { data: { state: BACKGROUND } }); } private saveGlobalDraft(): void { // 收集全局状态写入PersistentStorage或数据库 } }这里体现了一个核心设计前后台状态本身就是全局监听器的监听对象只不过它的注册位置是固定的——Ability生命周期。你只需要把状态变化广播出去页面的刷新和暂停都由页面订阅后自行处理。有一个细节需要注意onForeground和onBackground只代表UIAbility的前后台状态应用中如果有多个UIAbility每个UIAbility都会触发自己的回调。所以如果要做“整个应用的前后台”感知建议只依赖EntryAbility的这两个回调不要在其他Ability里做同样的广播否则同一个状态变化可能被广播多次。3.3 页面侧如何订阅以首页Index为例演示订阅方式import { emitter } from kit.BasicServicesKit; Entry Component struct Index { State isOnline: boolean true; aboutToAppear(): void { // 注册网络状态订阅 emitter.on(NETWORK_STATUS_CHANGE, this.onNetworkChange); } aboutToDisappear(): void { emitter.off(NETWORK_STATUS_CHANGE, this.onNetworkChange); } onNetworkChange (data: emitter.EventData) { const eventData data.data as { isOnline: boolean; networkType: string }; if (this.isOnline ! eventData.isOnline) { this.isOnline eventData.isOnline; if (!this.isOnline) { // 网络断开弹全局提示 this.showOfflineToast(); } } }; build() { // 页面UI } }页面侧的关键细节是aboutToAppear和aboutToDisappear必须配对注册和注销。很多线上问题都出在只注册不注销页面销毁了回调还残留。虽然此时回调不会导致崩溃但事件触发时会执行一份“死代码”造成无意义的资源消耗。如果全局事件比较多建议做一个统一的订阅管理工具。页面在aboutToAppear时调用注册方法在aboutToDisappear时调用注销方法至少保证所有订阅点在一个文件里能看到排查时不用翻遍所有页面。4. 常见问题与排查技巧实录4.1 为什么onCreate只执行了一次监听器却重复触发如果应用只有一个EntryAbility冷启动的onCreate确实只执行一次理论上不会重复订阅。但实际项目中onCreate里还可能调用其他初始化方法这些方法如果被多个外部入口触发就可能出现重复注册。还有一种情况应用中如果有多个UIAbility每个UIAbility都有自己独立的实例入口Ability的onCreate执行一次不代表其他Ability的onCreate不被调用。如果你在某个公共模块里做了订阅操作就可能出现多个订阅方。排查技巧是给注册处打印堆栈看谁触发了重复注册。更稳妥的做法是在emitter.on之前先无条件emitter.off一次借用“先注销再注册”的办法规避。这个习惯看起来有些保守但实际避免了很多线上问题。4.2 页面收不到网络状态更新怎么办最常见的原因有两个。第一网络回调没有触发。connection.register后的回调是异步的回调里如果出现异常又没有捕获后续逻辑就全部中断。建议在回调最开头先打日志确认系统确实回调了再往下执行。第二回调触发了但没传到页面。这大概率是key不匹配。emitter的key是普通字符串两侧写错一个字符都不会有报错订阅方就是收不到。把所有事件key放到独立的常量文件里集中管理能彻底杜绝这类问题。export const EventKeys { NETWORK_STATUS_CHANGE: NETWORK_STATUS_CHANGE, APP_STATE_CHANGE: APP_STATE_CHANGE, LOGIN_STATUS_CHANGE: LOGIN_STATUS_CHANGE } as const;4.3 onNewWant和onCreate处理业务参数的边界外部拉起应用的入口不止桌面图标还有Widget、深链、通知等。冷启动时参数通过onCreate的want进来应用已经在后台时参数则走onNewWant。很多人只处理了onCreate的参数结果应用在后台时点击Widget无法跳转。正确姿势是onCreate和onNewWant都解析want参数然后通过同一个事件key广播给页面onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void { const targetPage want.parameters?.targetPage as string; if (targetPage) { emitter.emit(EventKeys.NAVIGATE_REQUEST, { data: { targetPage } }); } }页面侧订阅NAVIGATE_REQUEST事件后根据targetPage执行跳转。这样无论冷启动还是热启动参数都能统一送达目标页面逻辑是幂等的。4.4 注销时机和内存泄漏全局监听器的生命周期和EntryAbility一致理论上onDestroy时注销就够。但UIAbility的onDestroy很多时候不是显式触发的尤其是系统杀进程的时候根本来不及执行。所以不要指望onDestroy处理所有事情。这里有三条经验监听器回调里持有页面引用的必须在页面销毁时注销。最典型的例子是页面aboutToDisappear里忘记off导致页面对象一直被监听器持有无法释放。不持有页面引用的全局监听器在Ability销毁时注销即可这个由onDestroy负责。谨慎使用匿名函数。匿名函数在日志里看不到具体名称排查持有关系非常困难。给回调命名除了代码整洁更是为了能通过日志定位到具体回调是谁注册的。4.5 网络监听在低电耗模式下的行为差异系统低电耗模式下后台网络回调会被延迟。也就是说应用进后台后网络变化可能不会立即回调而是等应用回到前台才通知。这会让“退到后台时保存草稿”的逻辑看起来没有及时执行。我的做法是把保存动作放在onBackground里主动执行不依赖网络回调。网络回调只负责更新状态保存动作由生命周期统一管理。这样两个监听各司其职互相不干扰任何一方出问题都不会影响整体逻辑。5. 全局监听器的进一步扩展公共事件与配置变化5.1 用commonEventManager订阅系统公共事件如果应用需要监听系统级的公共事件比如低电量、时区变化、屏幕解锁可以用commonEventManager。它订阅的是系统应用的公共事件广播和业务事件的维度不同。import { commonEventManager } from kit.BasicServicesKit; const subscriberInfo: commonEventManager.CommonEventSubscribeInfo { events: [usual.event.POWER_SAVE_MODE_CHANGED] }; commonEventManager.createSubscriber(subscriberInfo, (err, subscriber) { if (err.code ! 0) { hilog.error(DOMAIN, TAG, createSubscriber failed: %{public}d, err.code); return; } subscriber.on(receive, (data) { const state data.parameters?.commonEventValue; // 处理低电量模式变化 }); this.commonEventSubscriber subscriber; });公共事件订阅需要申请ohos.permission.GET_COMMON_EVENTS权限而且订阅的是系统事件不同系统版本的事件名和参数可能有差异。这类监听器最好和业务监听器分开管理同样在onDestroy里注销。权限声明需要加到module.json5的requestPermissions里。5.2 用onConfigurationUpdate监听配置变化如果应用需要感知系统配置变化比如语言切换、屏幕方向变化可以用onConfigurationUpdate回调。它本身是Ability级别的回调天然是全局监听器onConfigurationUpdate(newConfig: Configuration): void { super.onConfigurationUpdate(newConfig); if (newConfig.direction Configuration.Direction.DIRECTION_HORIZONTAL) { // 全局通知页面横屏 } }这种回调的频率可能很高比如方向变化时多次触发回调里不能放重量级操作只做状态广播和标记更新。5.3 分层管理全局监听器我个人的习惯是把全局监听器按三层管理这个习惯在多个项目里都验证过可维护性。第一层生命周期层。onCreate、onNewWant、onForeground、onBackground、onConfigurationUpdate、onDestroy这些系统固定的回调在EntryAbility里直接整理成独立方法比如handleColdStart、handleForeground。每个方法只做一件事名称要直白。第二层系统事件层。网络监听、公共事件订阅、配置变化统一放在一个初始化方法里注册一个销毁方法里注销。这一层通常不需要页面参与页面只消费事件结果。第三层业务事件层。emitter订阅全部用常量key管理页面侧通过一个统一的注册函数来订阅避免散落。按这个分层写EntryAbility的可读性会好很多也不会漏掉注销。再加上回调的幂等性原则——无论回调触发多少次效果都一样——全局监听器的大部分坑都能避开。用实际项目里的体会来收个尾EntryAbility不是万能的收容所但全局事件必须有统一的收口。选对实现方式、管好生命周期、养成幂等习惯这个功能就能写得既简单又稳。新接手鸿蒙项目的朋友建议先从这三个层面梳理自己的全局逻辑很快就能感受到好处。
返回列表