第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没释放。

我个人习惯是这么用的:

  1. 手机充满电,拔掉充电器
  2. 正常使用App 30分钟到1小时
  3. 执行 adb bugreport bugreport.zip 导出报告
  4. 上传到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 后台任务限制 导出bugreport 分析时间轴图表 定位WakeLock/CPU/网络问题 acquire/release 成对 设置超时时间 AlarmManager 用setInexactRepeating JobScheduler 系统调度 WorkManager 现代方案 约束条件:网络/电量/空闲 核心原则:能省则省,能合并则合并,能用系统API就别自己造轮子

你看,整个电量优化的思路其实很清晰:先用Battery Historian找到问题,然后针对性地管理WakeLock和AlarmManager,最后用WorkManager这类现代API来替代手写的后台任务。每一步都有章可循。

最后说一句:电量优化不是一锤子买卖。每次发版前,我都习惯跑一遍Battery Historian,看看有没有新增的耗电问题。养成这个习惯,你的App在用户手机里才能「活」得更久。

公众号:蓝海资料掘金营,微信deep3321