第13章 电量优化:从Battery Historian到WorkManager

电量优化这个话题,说实话,很多开发者容易忽视。我早期做项目时也是这样——功能跑通了,界面好看了,就急着上线。结果呢?用户反馈「手机发烫」、「半天就没电了」。嗯,那时候我才真正意识到,电量优化不是锦上添花,而是用户体验的底线。

这一章,我会带你系统梳理电量优化的核心工具和手段。从分析工具Battery Historian,到具体的代码治理——WakeLock、AlarmManager、JobScheduler、WorkManager,再到网络请求的合并策略。说白了,就是让你知道电都耗在哪了,然后怎么把它省回来。

核心观点:电量优化的本质,是减少不必要的CPU唤醒和网络通信。你想想看,手机最耗电的就是屏幕、CPU和无线模块。屏幕我们管不了太多,但CPU和网络,是我们可以控制的。

电量优化知识体系 电量优化 Battery Historian WakeLock 治理 AlarmManager JobScheduler WorkManager 网络请求合并 目标:减少CPU唤醒 + 减少网络通信

13.1 Battery Historian:先搞清楚电去哪了

做优化之前,你得先知道问题在哪。Battery Historian 是 Google 官方推出的电量分析工具。我个人习惯是,每次大版本发布前,都会跑一轮 Battery Historian 看看有没有异常耗电。

它的工作原理很简单:读取系统导出的 bugreport 文件,然后解析成可视化的时间轴。你能看到每个时间段,CPU 在忙什么、WiFi 在不在工作、屏幕亮没亮、哪个 App 在频繁唤醒系统。

使用步骤:

  1. 手机连接电脑,执行 adb bugreport bugreport.zip
  2. 把生成的 zip 文件上传到 Battery Historian 网页端(或本地 Docker 部署)
  3. 在时间轴上找到「密集的 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兼容性后台任务★★★★★
网络请求合并减少连接次数★★★★
公众号:蓝海资料掘金营,微信 deep3321