第13章 电量优化:从Battery Historian到WorkManager
电量优化这个话题,说实话,很多开发者容易忽视。我早期做项目时也是这样——功能跑通了,界面好看了,就急着上线。结果呢?用户反馈「手机发烫」、「半天就没电了」。嗯,那时候我才真正意识到,电量优化不是锦上添花,而是用户体验的底线。
这一章,我会带你系统梳理电量优化的核心工具和手段。从分析工具Battery Historian,到具体的代码治理——WakeLock、AlarmManager、JobScheduler、WorkManager,再到网络请求的合并策略。说白了,就是让你知道电都耗在哪了,然后怎么把它省回来。
核心观点:电量优化的本质,是减少不必要的CPU唤醒和网络通信。你想想看,手机最耗电的就是屏幕、CPU和无线模块。屏幕我们管不了太多,但CPU和网络,是我们可以控制的。
13.1 Battery Historian:先搞清楚电去哪了
做优化之前,你得先知道问题在哪。Battery Historian 是 Google 官方推出的电量分析工具。我个人习惯是,每次大版本发布前,都会跑一轮 Battery Historian 看看有没有异常耗电。
它的工作原理很简单:读取系统导出的 bugreport 文件,然后解析成可视化的时间轴。你能看到每个时间段,CPU 在忙什么、WiFi 在不在工作、屏幕亮没亮、哪个 App 在频繁唤醒系统。
使用步骤:
- 手机连接电脑,执行
adb bugreport bugreport.zip - 把生成的 zip 文件上传到 Battery Historian 网页端(或本地 Docker 部署)
- 在时间轴上找到「密集的 CPU 唤醒」区域,点进去看是哪个 App 的哪个服务
我在项目中遇到过这样一个案例:一个新闻类 App,用户反馈待机耗电严重。用 Battery Historian 一查,发现每隔 3 分钟就有一个 2 秒的 CPU 唤醒。点进去看,是某个第三方 SDK 在轮询广告配置。说白了,就是 SDK 写死了轮询间隔,没考虑用户是否在前台。
注意:Battery Historian 分析的是「系统级」数据,需要 Android 7.0 以上设备。另外,bugreport 文件可能包含敏感信息,分享时记得脱敏。
13.2 WakeLock:别让手机「假睡」
WakeLock 是一种让 CPU 保持唤醒的机制。你想想看,手机屏幕关了之后,系统会进入休眠状态,CPU 频率降低。但如果你的 App 持有 WakeLock,CPU 就会一直保持高功耗状态。
我见过最夸张的一个案例:某个音乐 App 在后台播放时,持有一个 PARTIAL_WAKE_LOCK 没释放。用户睡前听歌,早上起来手机只剩 10% 的电。其实播放音乐本身耗电不多,但 CPU 被唤醒了一整夜,这才是元凶。
正确的做法是:
- 能用前台服务(Foreground Service)就别用 WakeLock
- 必须用 WakeLock 时,使用
acquire(timeout)设置超时,防止忘记释放 - 在
onPause()或onDestroy()中确保释放
// 不推荐:无超时的 WakeLock
PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyApp:Tag");
wl.acquire();
// ... 如果这里发生异常,wl 永远不会释放
// 推荐:带超时的 WakeLock
PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyApp:Tag");
wl.acquire(10 * 60 * 1000L); // 最多持有10分钟
// 正常结束时调用 wl.release()
避坑指南:我曾经在定位一个线上耗电问题时,发现 WakeLock 的 tag 写的是 "MyApp:DoSomething"。但代码里搜不到这个 tag。后来才发现是某个第三方库内部申请的。所以,我建议所有 WakeLock 的 tag 都加上自己的包名前缀,方便排查。
13.3 AlarmManager:定时任务别太「勤快」
AlarmManager 是用来做定时任务的。很多 App 用它来拉取新数据、上报日志、刷新广告。但问题在于,如果每个 App 都按自己的时间表来唤醒 CPU,那手机就别想休息了。
从 Android 4.4(API 19)开始,set() 和 setRepeating() 就不再保证精确时间了。系统会把多个 Alarm 合并到一起触发。但 setExact() 和 setAlarmClock() 仍然会精确唤醒。
我的建议是:
- 能用
set()就别用setExact() - 能用
setInexactRepeating()就别用setRepeating() - 从 Android 6.0 开始,Doze 模式下 Alarm 会被延迟到下一个维护窗口
经验之谈:如果你需要「每隔15分钟做一次同步」,别用 AlarmManager 硬写。更好的做法是用 JobScheduler 或 WorkManager,让系统来决定什么时候执行。系统比你更清楚什么时候唤醒最省电。
13.4 JobScheduler:让系统帮你「排班」
JobScheduler 是 Android 5.0 引入的。它的核心思想是:你把任务告诉系统,系统根据当前状态(充电、WiFi、空闲等)来决定什么时候执行。
举个例子:你想在 WiFi 环境下下载一个大文件。如果用 AlarmManager + 网络监听,你得自己判断 WiFi 状态、电量状态。但用 JobScheduler,你只需要设置条件:
JobInfo jobInfo = new JobInfo.Builder(JOB_ID, new ComponentName(context, MyJobService.class))
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) // 非计费网络(WiFi)
.setRequiresCharging(true) // 充电时执行
.setMinimumLatency(1000 * 60 * 30) // 至少等30分钟
.build();
JobScheduler scheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
scheduler.schedule(jobInfo);
我在项目中遇到过一个问题:某个 App 用 JobScheduler 做日志上报,但设置了 setPeriodic(15 * 60 * 1000L)。结果发现日志上报非常不准时,有时候延迟了 1 小时。后来查文档才知道,setPeriodic() 的最小间隔是 15 分钟,而且系统会做 flex 调整。说白了,JobScheduler 的周期任务不是「精确的」,而是「尽量在周期内执行」。
注意:JobScheduler 在 Android 8.0 以上对后台执行限制更严格。如果你的 targetSdkVersion >= 26,JobScheduler 是后台任务的推荐方案,但别忘了处理 onStopJob() 回调,确保任务被中断后能恢复。
13.5 WorkManager:Google 的「终极方案」
WorkManager 是 Jetpack 的一部分,它兼容了 JobScheduler、Firebase JobDispatcher 和 AlarmManager。说白了,就是 Google 帮你封装了一层,让你不用关心不同 Android 版本的差异。
我个人非常推荐 WorkManager。原因有三:
- 链式调用:可以轻松实现「任务A完成后再执行任务B」
- 约束条件:网络、充电、空闲状态,一行代码搞定
- 持久化:任务被杀死后,下次启动会自动恢复
// 定义任务
class UploadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
// 执行上传操作
return if (success) Result.success() else Result.retry()
}
}
// 调度任务
val uploadRequest = OneTimeWorkRequestBuilder<UploadWorker>()
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresCharging(true)
.build()
)
.setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS)
.build()
WorkManager.getInstance(context).enqueue(uploadRequest)
经验之谈:我曾经把一个项目从 AlarmManager + IntentService 迁移到 WorkManager。迁移后,后台任务的耗电量降低了约 40%。原因很简单:WorkManager 会智能合并任务,不会像以前那样每个任务都单独唤醒 CPU。
13.6 网络请求合并:一次搞定,别分多次
网络请求是耗电大户。每次建立连接、DNS 解析、TLS 握手,都要消耗不少电量。你想想看,如果一个 App 启动时发了 10 个网络请求,那就是 10 次连接建立。如果能合并成 1 个请求,电量消耗能降低一个数量级。
常见的合并策略:
- 接口合并:把多个小接口合并成一个批量接口。比如「获取用户信息」和「获取用户设置」可以合并
- 请求排队:把短时间内的多个请求缓存起来,等 200ms 后一起发送。我习惯用 OkHttp 的拦截器来实现
- 数据压缩:使用 Gzip 压缩请求体和响应体,减少传输数据量
// OkHttp 请求排队拦截器示例
class BatchInterceptor : Interceptor {
private val pendingRequests = mutableListOf<Request>()
private val handler = Handler(Looper.getMainLooper())
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
// 如果是可合并的请求,加入队列
if (isBatchable(request)) {
pendingRequests.add(request)
// 等待 200ms 后统一发送
handler.postDelayed({ flush() }, 200)
// 这里需要返回一个占位 Response,实际项目中建议用异步方式
return chain.proceed(request)
}
return chain.proceed(request)
}
private fun flush() {
// 合并 pendingRequests 中的请求,批量发送
}
}
小技巧:如果你用 Retrofit + OkHttp,可以开启 OkHttp 的 retryOnConnectionFailure(true) 和 addInterceptor(HttpLoggingInterceptor)。前者在网络不稳定时自动重试,后者帮你观察请求是否真的被合并了。
好了,这一章的内容就到这里。电量优化不是一蹴而就的事,需要你持续用 Battery Historian 监控,逐步治理 WakeLock 和 AlarmManager,拥抱 JobScheduler 和 WorkManager,最后别忘了把网络请求合并起来。每一步都能省下一点电,积累起来就是用户体验的大幅提升。
本章小结:
| 工具/方案 | 适用场景 | 省电效果 |
|---|---|---|
| Battery Historian | 定位耗电问题 | 分析工具,不直接省电 |
| WakeLock 治理 | 后台 CPU 唤醒 | ★★★★★ |
| AlarmManager 优化 | 定时任务 | ★★★★ |
| JobScheduler | 条件触发的后台任务 | ★★★★★ |
| WorkManager | 兼容性后台任务 | ★★★★★ |
| 网络请求合并 | 减少连接次数 | ★★★★ |