
晚上睡觉前手机电量还有80%一觉醒来剩61%打开设置一看耗电排行榜里微博稳稳坐在第一位屏幕耗电才占了不到十分之一其余全是后台活动和网络连接。这个场景对做Android系统开发和App性能优化的同学来说应该不陌生。Android功耗优化的难点恰恰就在这种看起来什么都没干实际上CPU一直在被叫醒干活的场景里。熄屏待机下的频繁唤醒既是评测机构测试续航时最头疼的变量也是厂商做功耗调教时反复较劲的部分。这篇作为Android功耗系列专题的第一篇我就用微博这个典型的第三方高频唤醒案例把熄屏待机状态下的功耗分析思路、定位工具和优化手段完整拆一遍。这篇文章适合三类人看一是做Android系统稳定性与功耗优化的工程师二是负责微博这类有大量消息推送需求App的开发同学三是想要搞清楚自己手机为什么待机掉电快的普通用户。1. 熄屏待机功耗为什么是评判手机续航最要紧的尺子1.1 一个早晨电量和微博的故事国内很多用户有睡前充电的习惯第二天早上拔掉电源出门这时候电量就是一天续航的起点。如果一晚上待机只掉3%到5%那这块电池的底子就是健康的如果一晚上掉了15%以上甚至出现满电入睡、凌晨自动关机的情况那几乎可以断定某个应用或系统服务在后台搞事情了。我见过不少真实案例。有的用户反馈手机上装了微博、微信、淘宝、拼多多这一串常用应用晚上飞行模式不开、Wi-Fi连着早上起来电量掉得离谱。手动看耗电排行微博排第一占掉总耗电的28%左右微信排第二但只有10%。同样两部手机一部把微博的后台权限收紧一部保持默认一晚上待机耗电能差出近一倍。这个差异的根源就是熄屏状态下CPU被高频唤醒的频次和时长不一样。熄屏待机和亮屏使用在功耗分析上完全是两套逻辑。亮屏时屏幕本身就是最大耗电元CPU、GPU、网络模块的消耗反而容易被掩盖。熄屏后屏幕关闭系统进入深度休眠此时任何一次唤醒都是在打扰睡眠——每唤醒一次CPU要从低功耗idle状态切到active状态哪怕只持续几百毫秒也要付出可观的能量代价。所以待机功耗的核心指标就聚焦在两个数字上唤醒次数、单次唤醒的有效工作时间。1.2 熄屏不等于睡眠Android的Doze机制到底在做什么Android从6.0开始在系统层面引入了Doze机制。屏幕熄灭后如果设备处于静止状态且未插电系统会逐步进入低电耗状态先限制网络访问再延迟JobScheduler任务和Alarm闹钟最后深度休眠把App的CPU访问也掐掉。这个设计的初衷是好的——让系统替用户把后台活动管起来。但Doze不能解决所有问题。尤其是用户把设备拿在手里走动时加速度计检测到设备在运动Doze的深度会被打断插着充电器时Doze的很多限制也不会生效而且App如果申请了FCM等厂商推送通道或者持有某些系统白名单权限唤醒依然能穿透Doze的限制。更常见的情况是微博这类集成了大量第三方SDK的应用自建长连接、保活心跳、轮询任务都写在Alarm或者Handler里系统无法准确区分哪个唤醒是用户真正需要的消息、哪个唤醒只是为了确认服务器还活着。所以在分析熄屏待机功耗问题时你要做的不是问Doze为什么没拦住它而是反过来问它通过什么途径绕过了Doze。这就像看门禁系统——门禁本身设计得再严密也得知道进出的人走的是哪个门、按的是哪个铃。微博的唤醒路径就是这一篇要重点拆解的对象。2. 微博在熄屏后究竟做了什么高频唤醒的行为画像2.1 实验环境的搭建用一晚上的真实待机做数据采集先把测试环境说清楚。要复现微博熄屏后频繁唤醒这个问题最直接的办法就是搭一个干净场景一台Android手机系统版本建议用Android 10以上的原生或接近原生的ROM装好微博正式版登录账号开启通知权限把自己的网络环境和正常睡觉时保持一致。然后执行下面的命令把电量统计归零adb shell dumpsys batterystats --reset adb logcat -c adb shell date这一步很关键。batterystats的耗电数据是累积式的不reset的话你会把白天刷微博、刷视频的功耗全算进夜里结论自然就错了。reset之后把手机锁屏放一晚上中间不要碰它第二天早上再抓数据adb shell dumpsys batterystats bs_dump.txt adb shell dumpsys alarm alarm_dump.txt adb shell dumpsys power power_dump.txt adb shell cat /proc/wakelocks wakelocks_dump.txt我在测试中习惯把手机调成飞行模式关掉部分干扰也会单独做一组保留正常网络Wi-Fi连上、SIM卡在位的对照。两组对比能帮你判断唤醒是网络心跳引起的还是纯粹的本地定时器引起的。注意测试期间不要打开微博不要手动唤醒屏幕否则数据就不干净了。2.2 数据里看到的典型画面Alarm与WakeLock的合唱抓完数据先看batterystats里的排行。典型的微博高频唤醒案例里你会看到这样的统计项目数值微博总耗电占比20%~35%其中保持唤醒WakeLock30~60分钟CPU唤醒次数每小时80~150次单次唤醒平均时长50~200ms网络流量夜间可能高达几十MB如果每小时150次唤醒、每次平均算100ms CPU工作时间一晚上8小时就是150×8×0.1120秒的额外工作。120秒听着不算多但它的代价远不止这120秒本身——因为每次从idle切换到active都要带动总线、中断控制器、电源管理单元协同工作实际功耗是线性计算的好几倍。这就是为什么唤醒频次高比单次唤醒长更值得警惕因为频次高意味着CPU频繁地进出浅睡状态开销翻倍累积。再看dumpsys alarm的输出。微博包名下会挂着一串Alarm比较典型的有两类一类是com.sina.weibo自己的业务定时器比如每15分钟检查一次未读消息、每30分钟拉一次角标数量另一类是集成的第三方SDK注册的比如友盟统计、Bugly、腾讯推送SDK等各自的保活心跳。有些心跳的周期短到5分钟一次如果这些心跳之间没有对齐CPU就会像被定时闹钟连环轰炸一样整个晚上都在醒过来处理一下、再躺下、再醒过来。2.3 为什么偏偏是微博长连接与生态SDK的叠加效应微博和微信不同微信在Android上的推送通道大量依赖厂商推送服务如华为推送、小米推送自己只在必要的时候拉起进程。微博早年为了消息到达率选择了自建长连接的方式由App的主进程维护一条和服务器保持连接的Socket通道。长连接这种设计在消息实时性上当然有优势但它有个天然代价连接需要心跳保活。客户端要定期向服务器发心跳包告诉服务器我还活着请别断开我。每次心跳包发出前CPU得从睡眠状态醒过来打包数据、走网络栈、等服务器回包、再重新睡过去。更麻烦的是长连接可能不止一条。微博主进程可能有微博自己的推送通道同时集成者比如手机厂商或者渠道包又会塞进各种统计、广告、推送SDK每个SDK都有一套独立的保活逻辑。这就好像一个房间里同时开了好几个闹钟每个闹钟都在自己的时间点响虽然每个闹钟本身不是特别吵但叠加起来就是整晚不得安宁。我在测试中看到过的极端案例里微博进程里同时挂着三条Socket连接分别属于主业务、统计SDK和推送SDK三条心跳周期各不相同导致CPU 1小时内被唤醒了200多次。2.4 一次唤醒的功耗账本算清楚代价才能谈优化把一次典型的微博后台唤醒拆开看WakeLock持有开始系统从idle态唤醒App的主线程和Binder线程开始跑解析心跳包或者Alarm任务然后走一次网络收发最后超时释放WakeLock系统重新进入idle。整个过程大约100到300毫秒。如果用功耗测量仪器比如Monsoon或Power Monitor去量这个窗口内的平均电流可能在200到400mA对比纯待机状态下几十mA的底电流一次唤醒就要多消耗大约3到5微安时。一晚上8小时如果每小时被唤醒100次那就是800次唤醒额外消耗大约2.5到4毫安时。听起来不多但这只是CPU唤醒的直接开销。考虑到每次唤醒还会连带点亮屏幕背光的可能性如果出现了熄屏显示亮起的bug、触发网络模块从低功耗模式切换到全速模式、以及导致系统无法进入更深的C-state真实损耗往往要达到直接计算的2到3倍。所以每一次唤醒都很贵这个观点做功耗优化的人必须刻在脑子里。3. 完整排查链路从batterystats到内核wakeup_reasons3.1 第一层用batterystats圈定嫌疑范围排查待机功耗问题第一步不是打开Android Studio的Profiler而是先看batterystats。这条命令把系统的电量使用情况以文本形式dump出来里面有各App的耗电排行Estimated power use、WakeLock持有明细Kernel WakeLock和App WakeLock、网络使用量等关键信息。adb shell dumpsys batterystats bs_dump.txt打开bs_dump.txt优先看这两段。一段是Estimated power use它会按包名列出各应用估算的耗电毫安时和占比方便你快速锁定嫌疑对象另一段是Uid u0aXXX下面的WakeLock子项能看到具体是哪个标签的WakeLock被长时间持有标签里往往藏着业务线索比如WbService、PushClient、AlarmManager之类。这里要提醒一句batterystats里的耗电百分比是估算值不是精确测量值。它根据CPU时间、WakeLock时长、网络流量等因素加权算出来的排序可以参考一二但别拿它当定罪的铁证。我碰到过把问题App误判成系统Launcher的情况就是因为系统桌面在后台跑了一次壁纸轮播耗了不少CPU时间但实际上罪魁祸首是另一个App频繁拉起Launcher。所以batterystats只负责缩小范围最终定罪还得靠下面的层去做。3.2 第二层dumpsys alarm挖出高频定时器的真面目锁定微博之后就要看它通过哪些Alarm来反复唤醒系统。dumpsys alarm的输出里有一段Recent alarms或者Alarm Stats列出每个Alarm的触发次数、类型和对应的PendingIntent。重点关注两个指标一是触发次数排名前几的Alarm二是Type为0或2的闹钟即ELAPSED_REALTIME_WAKEUP和RTC_WAKEUP类型。RTC_WAKEUP类型的Alarm会不管系统睡没睡直接拉起来执行ELAPSED_REALTIME_WAKEUP则从开机以来计时同样会把系统从睡眠中唤醒。如果看到微博的某个Alarm一小时触发了四五十次而且每次触发后都有一段CPU运行记录那它基本就是夜间耗电的骨干力量。接下来顺着这个Alarm的代码路径去查它是在AndroidManifest里声明的Service还是Receiver注册的时候用的setRepeating还是setExactAndAllowWhileIdle前者在Doze模式下会被系统合并后者则拥有绕过Doze的权限属于高风险用法。顺带提一个容易被忽略的地方很多Alarm不是微博自己注册的而是微博调用了某个SDK之后SDK悄悄注册的。比如某些推送SDK为了保活会在初始化时注册一个每15分钟触发一次的心跳Alarm而且周期写得特别顽固。遇到这种情况光让微博App的开发者改逻辑可能没用还得推动SDK厂商配合或者由系统侧对特定SDK的Alarm做对齐和合并。3.3 第三层WakeLock与内核唤醒源交叉验证Alarm只是触发机制真正让CPU保持工作的则是WakeLock。这一步要结合dumpsys power和/proc/wakelocks来看。先执行adb shell dumpsys power power_dump.txt翻到WakeLock列表找出微博持有时间最长的锁。常见的微博后台WakeLock标签包括weibo、NetworkLink、PushService、MVController之类的名字。如果某个WakeLock在夜里被连续持有几十分钟那就说明App拿到锁之后一直在干活而不是唤醒一下立刻释放。这种情况比高频短唤醒更严重因为长时间持锁会把CPU钉在高频档位整夜都无法进入深度idle。再看内核层adb shell cat /proc/wakelocks或adb shell cat /sys/kernel/wakeup_reasons内核层能看到是哪个中断源或者电源管理事件最后把系统从睡眠状态唤醒比如abox_dsp、sensorhub、wlan_rx_wake等等。如果wakeup_reasons里反复出现wlan相关的唤醒源再结合batterystats里微博的网络流量很大基本可以确认是微博的网络心跳在拉动Wi-Fi网卡频繁收发数据。到了这一步用户态和内核态的证据就对上了根因基本锁死。3.4 第四层Perfetto抓取一次真实唤醒的全链路前几层解决的是谁在唤醒的问题第四层要解决唤醒之后做了什么的问题。用PerfettoAndroid 10及以上版本系统自带开发者选项里开启记录系统跟踪即可抓一段熄屏状态的trace设置抓取时长覆盖20分钟左右的待机窗口。抓完后在perfetto.ui或者trace分析工具里看这几条线CPU频率曲线有没有频繁的尖峰尖峰和微博进程的调度是否有对应关系调度器里sina.weibo线程的活动哪个线程在跑跑了多长时间Binder事务有没有高频的Binder调用调用对端是系统服务如ActivityManager、NetworkStats还是微博自己。一个典型的高频唤醒负面样本长这样CPU在freq那边出现一串密集短脉冲每个脉冲对应的调度段里微博的PushService线程从Running变到SleepingBinder对端是system_server里的AlarmManagerService。这个组合拳打下来结论就非常清晰了微博注册的Alarm到点触发系统从睡眠中醒来把微博的PushService线程调度到CPU上它发了一次网络心跳然后继续睡。整个过程循环往复构成了整夜的功耗噪声。3.5 排查中的两个数据陷阱踩过一次就长记性坑一batterystats的网络流量字段和CPU耗时字段要对齐才能说明问题。有些时候网络流量很小但WakeLock时间很长说明App可能在跑本地计算任务比如清理缓存、数据库压缩这类问题靠Alarm排查可能查不出来得看线程栈和Perfetto。坑二有些手机厂商会在system_server里加自己的调度逻辑和手机管家服务dumpsys的输出格式和原生Android略有出入。如果你拿一部搭载定制ROM的机器做排查先把系统版本和dumpsys输出的字段结构搞清楚别拿原生Android的解析脚本直接套容易误判。更强一点的建议是排查功耗问题尽量用God吗其实原生Pixel或root过的机器最方便因为能直接看/proc/wakelocks没root的手机这一步做不了太多只能靠应用层数据推断。4. 优化落地系统侧、App侧、用户侧分别能做什么4.1 App开发者视角从架构上卸载每15分钟醒一次的逻辑如果你是微博这类App的开发人员停用高频Alarm和长连接心跳确实会动到业务指标。消息到达率的KPI悬在头顶每小时唤醒一次检查未读这种逻辑看起来合理但实际优化空间很大。我最常给的几条建议按执行成本从低到高排列用JobScheduler或WorkManager替代Alarm。Doze模式下系统会智能合并Job的触发时机把多个任务攒到同一个窗口执行省掉的唤醒次数非常可观。WorkManager还能在App进程被杀后由系统代为调度不需要App自己保活。心跳做自适应退避。不要固定15分钟一次心跳可以做动态心跳网络连接稳定时逐渐拉长到30分钟、60分钟检测到服务器下发的消息间隔变长时同步拉长心跳间隔。类似TCP的拥塞控制思路但没有那么复杂。连接唤醒与业务对齐。很多唤醒发生在深夜用户根本不打开微博的时候推送通道完全可以在夜间切到厂商的离线推送如果集成了的话消费者要的只是早上能看到通知不是凌晨3点也要一秒送达。清理第三方SDK的重复心跳。把应用内的SDK保活逻辑做一次系统盘点能并的并能删的删很多SDK的保活其实可以用系统提供的统一推送服务替代。这些改动里面影响最大但往往被忽略的就是消息到达率与功耗的tradeoff。我实际测算过把后台心跳间隔从15分钟拉到1小时到达率约下降0.5%以内但后台耗电平均能降30%到40%。那个用户无法感知、系统却默默买单的1小时心跳可能是最有性价比的优化点。4.2 系统侧策略Doze的白名单管理和厂商ROM的调度整合如果问题发生在用户手机上而不是自己的开发机上系统侧能做的更多。Android的原生机制中用户可以在设置-应用-特殊访问权限-电池优化里把微博设为优化或不优化。但如果微信、微博这类高频社交应用全被列入不优化Doze就形同虚设。厂商ROM的功耗策略会比原生激进很多。比如国内主流手机管家的自启动管理会直接拦截微博在后台通过广播和Alarm拉起自己更激进一点的睡眠待机省电模式会在用户长时间不操作后冻结所有非白名单App的CPU访问和网络访问这时候微博的心跳直接发不出去也就谈不上耗电了。对系统开发工程师来说设计这种冻结策略的关键在于平衡既要保住消息类App的通知推送又不能让它频繁唤起系统思路通常是给推送通道单独开一个有限后台运行的豁免业务逻辑开关则保持不变。4.3 用户侧手段不root也能压住微博的夜间唤醒普通用户遇到微博夜间耗电高的问题有几个立竿见影的操作关闭微博的后台自启动权限MIUI的自启动管理、Flyme的后台管理、ColorOS的自启动管理都支持按应用单独控制在系统设置里把微博的后台高耗电权限关掉或者开启后台冻结类功能睡觉前用手机管家的睡眠模式或者手动切换到省电模式。省电模式下系统会自动收紧后台Alarm和网络访问微博的消息接收会延迟但换来的是稳定的夜间掉电如果手机支持直接一键卸载重装微博、登录后关掉发现页自动播放视频和智能推送开关减少了后台拉活的主要原因。实测下来把自启动和后台高耗电两项关掉后微博夜间唤醒次数能从每小时100多次降到不到20次一晚上掉电从15%降到5%左右代价只是通知到达略有延迟。对绝大多数不依赖微博做即时沟通的用户来说这个体验损失完全能接受。4.4 到达率与功耗的平衡照搬公式不可行做功耗优化总是会碰到产品经理拿着推送到达率99%来压人。这里有个容易被忽略的事实到达率统计的往往是一条消息发出去4小时内用户看到通知的比例而不是3秒内到达的比例。也就是说把后台心跳从15分钟拉到1小时对这条统计口径的影响几乎可以忽略不计。做App功耗优化的核心不是无脑砍掉所有后台任务而是把任务按用户可感知和用户不可感知分个类。用户可感知的比如推消息、同步聊天记录、刷新角标保留在合理周期内用户不可感知的比如统计上报、版本检查、广告拉取、无用连接保活全部砍掉或者交给系统调度。用这个标准去审视微博的夜间唤醒名单你会发现有相当一部分Alarm根本没有业务价值纯粹是为了活着而活着。5. 开头提到的那台手机最后是怎么治好的回到开头的那个场景。我帮朋友排查那台一晚上掉电15%的手机整个链路走下来batterystats锁定微博dumpsys alarm看到一个每10分钟触发一次的推送心跳AlarmWakeLock里微博的PushService持有时间累积超过40分钟Perfetto确认每次唤醒后微博都在做网络收发。根因清楚但系统是普通用户在用App侧的新版本线上还没推送。最终落地的是用户侧方案把微博的自启动权限和后台高耗电权限关掉通知依然保留只是从实时推送变成了打开App后同步拉取。一周后他告诉我现在晚上掉电不到5%微博的通知他也不在乎延迟反而觉得手机清净了。这个案例其实浓缩了Android功耗系列后面的很多内容。熄屏待机功耗分析的每一步都不是孤立的batterystats负责粗筛Alarm和WakeLock负责定位机制内核wakeup_reasons负责确认根因最后落到App架构或者用户设置上做优化。工具链条越长误判的机会就越少优化动作才越精确。下一篇我打算写进程被杀与拉起保活对功耗的影响核心问题是一个App被系统杀了之后又通过什么方式在几分钟内复活这个过程消耗的功耗往往比持续后台运行还要大。如果你正好遇到过后台应用越杀越耗电的怪象那一篇应该会对你有用。