如何基于 Battery Historian 定位后台任务每小时唤醒 100 次的问题

解读

面试官想验证三件事:

  1. 你是否真的在 Android 性能测试里用过 Battery Historian,而不是只会读网上教程;
  2. 能否把“唤醒 100 次”这一业务语言翻译成电量视角的量化指标(wake_lock、JobScheduler、Alarm、Doze 状态等);
  3. 能否在工具链里快速收敛范围,给出可落地的代码或配置优化建议,而不是停留在“把唤醒次数改少”这种口号。

国内面试场景下,大厂通常已经接入了统一埋点平台(如字节 Ranger、阿里 Sunfire、腾讯 RDM),Battery Historian 更多用于灰度或客诉复现阶段,因此回答要体现“本地复现→平台二次验证→推动研发回归”的闭环。

知识点

  1. Android 电量统计原理:
    – Kernel wakelock、Android wakelock、Suspend blocker 三层计数;
    – BatteryStatsService 每 30 s 采样,记录 UID 级 CPU 前台/后台、网络、传感器、wake_lock 时长与次数;
    – Doze & App Standby 状态机:IDLE/IDLE_MAINTENANCE/ACTIVE 切换点。

  2. Battery Historian 2.0+ 关键字段:
    – wake_reason:kernel 层最后一次唤醒原因;
    – wake_lock_in/wake_lock_out:持有与释放时间点;
    – long wake_lock 警告:单次 >1 min 标红;
    – JobScheduler、AlarmManager、FCM、SyncAdapter 事件行;
    – mobile_radio_active:每小时内蜂窝网络激活次数与时长;
    – cpu_running:CPU 未 suspend 的累计时长。

  3. 国内 ROM 差异:
    – 小米/华为对后台 Alarm 做“对齐唤醒”拦截,Historian 里可能看到批量唤醒,但次数被系统合并,需要对比系统日志 logcat -b stats 确认原始次数;
    – 部分厂商把 Push SDK 的 wake_lock 归到系统 UID 1000,Historian 默认聚合后看不到,需要加 –uid 参数重新解析。

  4. 性能测试常用辅助命令:
    adb shell dumpsys batterystats –reset
    adb shell am set-inactive <pkg> true/false
    adb shell cmd jobscheduler run –f <pkg> <jobId>

  5. 可接受阈值(国内 Top 200 App 90 分位):
    – 后台每小时 Alarm + Job + FCM 唤醒 ≤ 10 次;
    – 后台 CPU running 时长 ≤ 2 min/h;
    – 单次 wake_lock ≤ 20 s。

答案

步骤化落地流程,面试时按“操作→证据→结论”节奏回答,全程不超过 4 分钟。

  1. 复现与采数
    a. 选用国内主流机型(如小米 13、HarmonyOS 3.0)关闭开发者“不保留活动”,安装被测 App 的灰度包;
    b. 夜间模拟用户场景:插入 SIM 卡、连接 4G、灭屏、不充电,运行 3 小时;
    c. 起测前 adb shell dumpsys batterystats –reset,结束取 bugreport:
    adb bugreport > bugreport.zip
    d. 同时拉取 logcat -b stats,power,system 做二次佐证。

  2. Historian 解析
    a. 本地 Docker 起 Historian(镜像:gcr.io/android-battery-historian/stable,国内可转存到阿里云 ACR);
    b. 上传 bugreport.zip,勾选“Show CPU usage”“Show wakelock details”;
    c. 在 Timeline 视图框选灭屏时段,观察:
    – wake_lock_in/out 行出现密集竖线,计数 312 次/3 h ≈ 104 次/h;
    – AlarmManager 行每 36 s 一次,包名与 action 固定;
    – Doze 状态始终为 IDLE_MAINTENANCE,未进入 IDLE,说明系统被持续唤醒;
    d. 切换到“System Stats”页,定位到该 UID 的“Wake locks*”表格,发现最长一次持锁 28 s,累计持锁 42 min/3 h。

  3. 根因定位
    a. 取一次 Alarm 唤醒时间点,回到 logcat:
    04:21:03.020 I AlarmManager: Triggered alarm Alarm{u0a123 wakeup} com.xxx.xxx/.service.KeepAliveService
    b. 反编译 APK,发现 KeepAliveService 中使用了 setExactAndAllowWhileIdle() 每 30 s 触发,未做 Android 12 以上对齐;
    c. 该 Service 在 onStartCommand() 里又主动申请 PartialWakeLock 确保网络请求完成,持锁 20~30 s;
    d. 结合 Historian 的 mobile_radio_active 行,发现每次唤醒后蜂窝网络激活 18 s,但接口返回 204 NoContent,属无效流量。

  4. 优化与验证
    a. 将 setExactAndAllowWhileIdle 改为 setAndAllowWhileIdle,并接入系统 Alarm 对齐窗口,唤醒次数降到 10 次/h;
    b. 网络请求改为 FCM 下行触发型,砍掉无效轮询;
    c. 持锁逻辑改为 WorkManager+Doze 兼容的 Expedited Job,持锁时长 <5 s;
    d. 复测 3 小时,Historian 显示后台 CPU running 时长降到 1.2 min/h,wake_lock 总计 8 次,满足国内厂商商店上架绿色公约。

  5. 交付
    输出《电量测试报告-后台唤醒专项》,包含 Historian 截图、logcat 片段、对比数据、代码 diff 链接,推动研发在迭代分支回归,并加入 CI 每日跑分门禁(>10 次/h 自动红灯)。

拓展思考

  1. 如果 Historian 发现唤醒集中在系统 UID 1000,而 logcat 里又看不到具体服务,该如何继续?
    答:可改用
    adb shell dumpsys power | grep -i wake
    查看 native wake_lock 名称,再结合
    adb shell cat /sys/kernel/debug/wakeup_sources
    定位到是运营商彩信或 PushSDK 的 socket 心跳,推动厂商使用系统级 FCM 代理,减少第三方后台长连接。

  2. 当 App 需要保活完成即时音频(如语音社交),如何在满足“每小时 ≤10 次唤醒”前提下保证 99.9% 消息到达?
    答:采用“FCM 高优先级消息+前台 Service+CPU 唤醒锁最短路径”方案:
    – FCM 消息携带 1~3 秒延迟的“对齐窗口”字段,服务端批量合并;
    – App 收到 FCM 后启动前台 Service(带麦克风 notification),仅在该场景下豁免 Doze;
    – 通话结束立即释放前台 Service 并归档 Historian 数据,确保非通话时段回归普通约束。

  3. 国内测试机型碎片化,Historian 在部分 ROM 上解析失败或时间轴错位,如何保障数据可信?
    答:建立“双通道”机制:
    – 本地 Historian 做快速定性;
    – 同时把 bugreport 原始文件上传到公司的 Spark 集群,复用字节开源的 BatteryCanary 解析规则,生成 UID 级指标落仓;
    – 当 Historian 与离线指标误差 >5% 时,以离线为准并触发机型校准任务,避免单次手工误差影响结论。