手表运动页面退出后传感器还在采样:一次“订阅泄漏”引发的功耗复盘
团队的运动手表 App 收到一批续航投诉:用户跑完步退到表盘,几小时后电量掉得异常快,但系统电池统计里 App 的占比并不高。抓日志发现,运动页面退出后,加速度计和心率的数据仍在持续回调——每次进入页面都 subscribe 了一次,退出时只停了 UI 定时器,底层订阅根本没有释放。穿戴设备的功耗预算比手机紧一个数量级,这种“订阅泄漏”积累几次,续航就肉眼可见地塌下去。
一、 先看清楚:泄漏是怎么发生的
典型的错误写法是在 aboutToAppear 里直接 sensor.on('accelerometer', callback),然后在 aboutToDisappear 里只清理自己维护的定时器,却忘了 sensor.off。更隐蔽的是,有些页面通过 router.pushUrl 反复进出,每次 aboutToAppear 都新增一个监听器,旧监听器因为闭包持有了页面上下文,既退不掉,又持续消耗传感器唤醒和 CPU 处理时间。华为开发者问答中也明确提到,同一传感器事件被多个 subscriber 监听会带来重复订阅和额外功耗风险,真正停止传感器要等 Unsubscribe 成功,仅靠功能层去重不能消除底层功耗。
二、 解决方式:把订阅收口成一个带引用计数的 Session
与其在页面的多个生命周期回调里散落 on / off,不如把订阅封装成独立的 SensorSession,内部维护引用计数,只有计数归零时才真正调用退订。任何“打开传感器”的入口都是 acquire(),任何“关闭”的入口都是 release(),即使某条路径漏掉了 release,计数模型也能在逻辑层暴露问题,而不是让泄漏静默累积。
typescript
class SensorSession {
private refs = 0;
private active = false;
acquire() {
this.refs++;
if (!this.active) {
sensor.on(sensor.SensorId.ACCELEROMETER, this.callback);
this.active = true;
}
}
release() {
this.refs = Math.max(0, this.refs - 1);
if (this.refs === 0 && this.active) {
sensor.off(sensor.SensorId.ACCELEROMETER, this.callback);
this.active = false;
}
}
}
release 必须放在 aboutToDisappear 和异常退出的 finally 路径里。验收标准很简单:重复进入同一个页面,底层始终只有一个订阅;正常返回后引用计数归零,传感器回调不再触发。可以用日志或 hdc shell 侧的命令验证,不要只靠肉眼判断“看起来不卡了”。
三、 频率不是越高越好:用“场景 + 电量 + 温升”做动态预算
退订修好之后,下一个问题是运动过程中的采样频率。固定高频当然能拿到更密的轨迹,但高频会同时增加传感器唤醒、计算、传输和存储成本,温升触发系统降频后,有效样本反而可能变少。穿戴场景的合理做法是按页面可见性、训练阶段、电量和温升动态输出频率,而不是一个静态值打天下。有实测参考显示,持续心率监测从固定高频切换到动态频率后,功耗降幅可以做到 45%~52% 的量级。
一个最小可用的预算策略可以写成这样:前台训练且温升正常时,运动传感器给到满足姿态解算的足够频率;后台或低电量时降到 1Hz 甚至暂停;高温时优先保护温升,主动降频。
typescript
function sensorHz(ctx: { foreground: boolean; training: boolean; battery: number; thermal: 'ok' | 'hot' }) {
if (ctx.thermal === 'hot' || ctx.battery < 15) return 1;
if (ctx.training && ctx.foreground) return 25;
if (ctx.training) return 10;
return 1;
}
四、 总结
穿戴传感器的功耗问题,根因往往不在“频率调得不够低”,而在订阅没有和页面生命周期严格对齐。先把 SensorSession 的引用计数收口做好,确保退出即退订;再在此基础上做场景化动态预算,把频率从“拍脑袋的静态值”变成“可解释、可验收的工程参数”。续航和轨迹精度并不是非要二选一,前提是泄漏先堵住。
- 点赞
- 收藏
- 关注作者
评论(0)