第13章 电量优化:Battery Historian分析、WakeLock与AlarmManager合理使用、后台任务限制
电量优化,说实话,是Android性能优化里最容易被忽视的一环。很多开发者觉得「只要功能跑通了,耗电多点无所谓」。但用户不这么想——他们发现某个App一装,手机半天就没电了,第一反应就是卸载。
我自己经历过一个惨痛的教训。几年前做一款社交App,上线后用户反馈「手机发烫、掉电快」。一查才发现,后台有个定时任务每30秒唤醒一次CPU,持续拉取消息。嗯,那会儿还没用WorkManager,代码写得比较糙。后来花了整整一周重构,才把电量消耗降下来。
所以这一章,咱们就聊聊电量优化的三板斧:怎么用Battery Historian定位问题,怎么管好WakeLock和AlarmManager,以及怎么用JobScheduler和WorkManager优雅地做后台任务。
13.1 Battery Historian:先找到「电老虎」
优化之前,你得先知道电都耗在哪了。Battery Historian就是干这个的。
它是Google开源的电池分析工具。你只需要从手机上导出一份bugreport文件,丢进Battery Historian的网页界面,就能看到一张时间轴图表。上面清清楚楚地标着:什么时候CPU在跑、什么时候WiFi开着、什么时候有WakeLock没释放。
我个人习惯是这么用的:
- 手机充满电,拔掉充电器
- 正常使用App 30分钟到1小时
- 执行
adb bugreport bugreport.zip导出报告 - 上传到Battery Historian(本地部署或在线版都行)
然后重点看这几个指标:
| 指标 | 说明 | 红线 |
|---|---|---|
| Kernel WakeLock | 内核级别的唤醒锁 | 持续超过30秒就要警惕 |
| Top App | 前台App的CPU使用 | 长时间高CPU占用 |
| Network | 网络活动(WiFi/移动数据) | 频繁断连重连 |
| Sync | 同步任务 | 同步周期太短 |
举个例子。我在分析一个地图App时,发现「Kernel WakeLock」那栏每隔5分钟就有一个小高峰。点开一看,是GPS定位的WakeLock没及时释放。说白了,就是定位回调里忘了调用 release()。这种问题,肉眼扫一眼图表就能定位。
核心思路:Battery Historian不是用来「修」问题的,而是用来「找」问题的。找到问题之后,再针对性地优化。
13.2 WakeLock:用的时候拿,用完马上还
WakeLock是Android提供的一种机制,让App在后台也能保持CPU运行。但滥用WakeLock,就是电量的头号杀手。
我记得有一次,一个同事在Service里持有一个WakeLock,结果Service因为异常崩溃了,WakeLock没释放。手机从此再也没法进入深度休眠。用户骂声一片。
所以使用WakeLock,我建议你记住三条铁律:
- 能不用就不用。大部分场景可以用其他方式替代,比如用Handler的延迟消息。
- 用时必须配对。acquire()和release()要成对出现,最好写在finally块里。
- 设置超时。用
acquire(long timeout)方法,防止意外泄漏。
代码示例:
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyApp:WakelockTag");
try {
wl.acquire(10_000); // 最多持10秒
// 执行你的后台任务
} finally {
if (wl.isHeld()) {
wl.release();
}
}
注意:WakeLock的tag一定要用「包名:具体用途」的格式。这样在Battery Historian里才能一眼看出是哪个模块在持锁。我曾经见过有人用"abc"当tag,查问题的时候简直想骂人。
13.3 AlarmManager:别让它变成「闹钟轰炸」
AlarmManager用来做定时任务。但很多人把它当成了「精确计时器」,每隔几秒就设一个闹钟。结果就是手机CPU频繁被唤醒,电量哗哗地掉。
Google其实早就意识到了这个问题。从Android 4.4开始,AlarmManager的 set() 方法就不再是精确的了。系统会把多个闹钟合并到一起,统一唤醒。如果你非要精确,得用 setExact(),但系统会限制调用频率。
我给你的建议是:
- 能用
set()就别用setExact()。大多数业务场景,允许几秒的误差完全没问题。 - 能用
setInexactRepeating()就别用setRepeating()。不精确的重复闹钟,系统可以灵活调度。 - 能用WorkManager就别用AlarmManager。这个后面会讲。
举个例子。我之前做一个新闻App,需要每天早上8点刷新内容。最初用的是 setRepeating(),结果发现手机在夜间也被频繁唤醒。后来改成 setInexactRepeating(),配合 setAndAllowWhileIdle(),问题就解决了。
AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(this, MyReceiver.class);
PendingIntent pi = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_IMMUTABLE);
// 不精确的重复闹钟,系统会合并唤醒
am.setInexactRepeating(AlarmManager.RTC_WAKEUP,
triggerAtMillis,
AlarmManager.INTERVAL_DAY,
pi);
小技巧:如果你的闹钟不需要精确到秒,可以加上 setAndAllowWhileIdle()。这样系统在低电量模式下,仍然会执行你的闹钟,但频率会被大幅降低。
13.4 后台任务限制:从JobScheduler到WorkManager
Android对后台任务的限制,是一步一步收紧的。从Android 6.0的Doze模式,到Android 8.0的后台执行限制,再到Android 12的精确闹钟权限。说白了,Google就是不想让App在后台乱来。
那怎么办?用官方推荐的后台任务方案。
13.4.1 JobScheduler:系统级的调度器
JobScheduler是Android 5.0引入的。它的核心思想是:你把任务「告诉」系统,系统根据电量、网络、时间等条件,在合适的时机执行。
我举个例子。假设你要在WiFi环境下,每天凌晨2点下载一个大的资源包。用JobScheduler可以这么写:
ComponentName serviceComponent = new ComponentName(this, MyJobService.class);
JobInfo jobInfo = new JobInfo.Builder(JOB_ID, serviceComponent)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) // 仅WiFi
.setRequiresCharging(true) // 仅充电时
.setPeriodic(24 * 60 * 60 * 1000) // 每24小时
.build();
JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
jobScheduler.schedule(jobInfo);
这样写的好处是,系统会把你的任务和其他App的任务合并到一起执行。既省电,又不会影响用户体验。
13.4.2 WorkManager:更现代的方案
WorkManager是Google在2018年推出的,它其实是JobScheduler的「升级版」。为什么这么说?因为WorkManager兼容到了API 14,而且在低版本上会自动切换到AlarmManager + BroadcastReceiver的实现。
我个人现在所有的新项目,后台任务一律用WorkManager。原因有三:
- 链式调用:可以轻松组合多个任务,比如「先下载,再解析,最后通知用户」。
- 约束条件丰富:网络、电量、存储空间、空闲状态,都能设。
- 生命周期感知:任务可以绑定到Activity或Fragment的生命周期,不会泄漏。
代码示例:
// 定义任务
class MyWorker extends Worker {
public MyWorker(@NonNull Context context, @NonNull WorkerParameters params) {
super(context, params);
}
@NonNull
@Override
public Result doWork() {
// 执行后台任务
return Result.success();
}
}
// 设置约束
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build();
// 创建任务请求
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(MyWorker.class)
.setConstraints(constraints)
.build();
// 提交任务
WorkManager.getInstance(context).enqueue(workRequest);
我的建议:除非你有非常特殊的需求(比如精确到秒的闹钟),否则一律用WorkManager。它帮你处理了系统兼容性、任务持久化、重试策略等一堆麻烦事。你只需要关心业务逻辑。
13.5 知识体系总览
下面这张图,把电量优化的核心脉络梳理了一下。你可以把它当成一个检查清单:
你看,整个电量优化的思路其实很清晰:先用Battery Historian找到问题,然后针对性地管理WakeLock和AlarmManager,最后用WorkManager这类现代API来替代手写的后台任务。每一步都有章可循。
最后说一句:电量优化不是一锤子买卖。每次发版前,我都习惯跑一遍Battery Historian,看看有没有新增的耗电问题。养成这个习惯,你的App在用户手机里才能「活」得更久。