ARTICLE · 1055968
低功耗和长续航漫谈(二十四):软件省电(中)——再问能不能等会儿做
低功耗和长续航漫谈(二十四):软件省电(中)——再问能不能等会儿做
一件活被认定该做,只说明它不该消失,不说明它得现在做。
这两件事经常被混成一件:定时器一设,就成了"每 X 分钟必跑一次";数据一变就立刻上报;用户一打开应用,能先取的都先取一遍。这些做法背后是同一个默认——活要干,就得马上干。
而真正的约束往往只到"最终会做完"。"什么时候做完"这一层,多数任务并没有人要求过。
把这两层分开,省下来的不是活本身,是每次醒来那一整套进出的开销:供回电域、恢复时钟、调度任务、重新初始化外设。一套走完,可能比真正干活的那几毫秒还费电。
这就是第二问:它管的不是做不做,是做的时刻和批次。
它还有个别处没有的特点:大头不用自己动手。延后、攒批、限流,系统早就在替所有应用做了。所以这一问的难处不在"想不想得到",而在有没有用对接口——该交给系统安排的活,还留在自己手里掐表,那就等于白交。
而这本账的另一面是:交出去的时刻,不一定按你要的时刻回来。
一、不着急的活,交给系统挑时刻
第一问筛完,剩下的活是真的要干的。但它们不一定非得现在干,也不一定非得各干各的。
这一问有两招:把时间往后挪一挪,把批次凑到一起——两招经常一起用。先看第一招。
晚做的前提就三个字:不着急。判断方式也是一句话——这件事晚两个小时做,有人会察觉吗?
对很多任务来说,答案是:不会。
应用商店里几十 MB 的应用更新; 几个 GB 的系统升级包; 相册备份、云端同步; 日志上报、埋点上传; 模型、配置、词库的定期更新; 缓存清理; 系统升级后的相册索引重建、大批应用重新编译。
它们的共同点是:没有实时性要求,只有"最终要做完"的要求。
对这类任务,正确的做法不是"设个定时器每 X 分钟跑一次",而是给系统一张条件清单:
正在充电、连着 WiFi、设备空闲——三条都满足时才执行。
把任务连同它的约束一起交给系统的调度器,由系统去挑时刻。它还会顺手做一件事:把交给了它的任务对齐到同一批时间窗口。
这是从"各自掐表"到"系统统一调度"的转变,也是近几代系统改善待机最有效的手段之一。
交出去的是执行的时刻,不是执行本身。任务还在,只是什么时候跑不由你定。系统会把它和其他应用的活排到一起,挑一个整体代价最小的时刻下手——对应用来说,这意味着它得接受"这件事不会按你要的时刻发生"。
这三条为什么能省,各有各的道理:充电时那点耗电由补进来的电流顺带补上,电量不会因此少掉一块;连着 WiFi 时流量不计,用户对这时的传输没那么敏感,而且同样一份数据,走 WiFi 的功耗开销通常比蜂窝低;设备空闲意味着处理器本来就要醒,这些活搭在同一趟里,几乎不会额外叫醒谁,并且不会有太大的热风险。
二、三处容易写偏的地方,和一张写对的清单
一是条件要真能表达业务约束,不是凑够几条。"充电中 + 连着 WiFi + 空闲"是最常见的一组,但具体到某件事,约束可能更宽也可能更紧:一个只在夜里做的大活,也许"充电中"一条就够了;反过来,一件必须在某个时间点之前完成的事,条件里就得把时限写上。
二是要想清楚等不到条件的时候怎么办。条件清单的代价是:这件事什么时候做得了,从此不由应用说了算。单条条件很少卡住人——"充电中"这一条,在一台正常使用的手机上是等得到的;容易卡住的是叠加之后剩下的窗口:几条一起要,实际意思就是"等我不用手机、插着电、又连着 WiFi 的时候再说",而这个时刻多久来一次,没有谁能向应用保证。所以要么定一个兜底时限(最迟多久必须做一次),要么接受它长期挂着。
三是别在条件清单外面自己再补一个闹钟。这是最常见的走形:约束交给系统了,应用却还留着一个"每小时检查一下条件满足了没有"的定时器——这个定时器本身,正是它想要省掉的那次唤醒。
三处走形指向同一件事:条件清单不是"多写几条约束",而是把"这件事什么时候做才合适"完整地说出来。说不清楚,系统只能按最保守的方式安排——它不会报错,只是这件事要么被排得比你预想的晚,要么压根没等到条件。
一张躲开这三处的清单,长这样。拿一件最常见的活走一遍:把攒在本地的使用记录上报上去。
先看系统允许你写哪些约束。能挑的项本来就不多:网络是不是计量流量、电量不低于阈值、需要充电、需要设备空闲、存储不低于阈值。清单要做的是从这几样里挑出这件事真正需要的那几条——条件不是自己发明的,是从现成的几样里选的。
再看这件事需要哪几条。上报只要两样:网络不计流量、电量不低于阈值。充电不需要,设备空闲也不需要——这两条加上去,它只会迟迟等不到条件。
这里有一组容易混的:"需要充电"和"电量不低于阈值"不是一件事。前者挑的是"这件事放在什么时候做",后者管的是"现在这副电量经不经得起它"——两条管的是两件事,不要当成一条用。一件几秒钟的上报,只要求电量别见底。
还有一条行为要知道:约束不只在开始前检查一次。活干到一半条件没了——比如从 WiFi 切到了计费网络——这一次运行会被停下来,等条件重新满足再重跑一遍。所以这段活得经得起中途停、重新跑,别把"跑到过"当成"做完了"。
然后是自己定一条兜底:最迟三天。这三天里任何一次条件满足,它都会跑掉;三天到了条件还没齐,就按普通网络传一次——记录晚到就没用了,这个损失比省下的那点电重要。
写全、交出去,应用这边不再留任何东西:系统会在这段时间里挑一个整体代价最小的时刻,把它和别的应用的活排进同一批窗口。
最后一件最容易漏:"接受它长期挂着"这个选项,得是有意选的,不能是漏掉的。系统文档里写得直白:条件没在周期内满足,这一次运行会被推迟,甚至跳过。推迟和跳过本身没问题,问题是这件事不会自己找上门来——没有时限的清单,等于把"它还欠着"交给了运气。
三、等充电:换一个掉电看不出来的时段
条件清单里有一项要单独说:正在充电。
插着充电器的时候做这些事,那点耗电会被充电顺带补上,用户毫无感觉——反正手机正插着,任务照常完成。
它不是"把这件事变得更省",而是换了一个掉电看不出来的时段。
代价也在这一边:那一趟充电会慢一点——进来的电流要先分一部分给这些活;机器也会热一点,快充那一段尤其明显。所以它们该排在夜里那段没人用、也没人看的时段里,而不是插上电就得马上做完。
判断标准很直接:这件事耗电越多,就越应该等充电时做。
所以越大的活越该排队:几个 GB 的系统升级包(下载、校验、安装)、首次动辄几十 GB 的相册云备份、系统升级之后的那一摊——一大批应用要重新编译、相册里成千上万张照片要重建索引。这几件都是小时级的活。夜里插上充电器,早上起来一切就绪——这类安排已经很常见了,只是很少有人意识到它省下的是什么。
不过等充电也有它不适用的时候。用户点了下载、正等着看的文件,当天必须在某个时刻之前送达的上报,以及那些一拖就会卡住后续动作的操作(升级包没下完,安装就没法开始)——这些都不该等。
所以条件清单只写给"系统替你挑时刻"的活。用户主动点下去、正在等结果的那件事,不进这张清单——第二问能交出去的是时刻,不是用户正在等的那件事。
四、合做:唤醒的代价比干活大
第二招要算的是另一本账:唤醒本身的代价,往往大于要干的活。
十个应用各设一个定时器,就是在十分钟里让处理器醒十次、射频醒十次。每次醒来,系统都要供回电域、恢复时钟、调度任务、重新初始化外设——这一整套进出的开销,可能比真正干活的那几毫秒还大。
把十个定时器对齐到同一个时间点,十次并成一次:系统只醒一趟,所有的活一起干完,然后集体睡回去。

图1 唤醒时序对照
合做的典型有四个:
- 定时器对齐。
交给系统调度的非紧急周期任务会被对齐到同一批窗口——前提是你用对了接口。 - 统一推送。
与其每个应用各自维持一条长连接、各自发心跳保活,不如走系统的统一推送。消息类应用要收到消息,靠的是与服务器之间保持一条连接——连接不能一直闲着,得定期发一个很小的包确认它还活着,这就是心跳。一个应用一条连接、一份心跳,装的应用越多,份数就越多;而每一份心跳都要把射频叫醒一次,射频醒一次的开销,远远大于这个小包本身。共享一条连接、共享一份心跳之后,装十个消息应用也只叫醒射频一次——省下来的是那几十份重复的唤醒,而每一次唤醒,射频都要重新建立一次连接、再退回待机。 - 传感器批量读取。
好几个应用都要加速度计的数据,那就合并成一次采样,结果分发给需要它的应用。每个用到它的地方各开一路采样,传感器和处理器就会被反复叫醒;合并成一路、按固定节奏采样再分发,唤醒次数能降一档,代价是数据晚一点点到手——而计步这类用途,本来就不需要毫秒级的实时。 - 小文件与日志攒批。
零散的写盘,每一次都要把存储从休眠里叫醒;攒成一批处理,唤醒次数就降下来了。
把 N 次唤醒并成 1 次,省的不是干活的时间,是醒来的那套固定开销。
五、系统替你做了大半
三个问题里,第二问是门槛最低的一个——原因不在于它简单,而在于系统层面早就在做这件事了。
深度待机、应用待机、后台限制、任务攒批、网络与定位的批处理——即使应用什么都不做,这些机制也在替它延后、攒批、限流。
所以第二问的难度,不在"想不想得到",而在"有没有用对接口":把周期任务交给系统的调度器,而不是自己起一个定时器;把消息同步交给统一推送,而不是自己保活;把上传攒到充电、以及不计流量的网络下,而不是随时就发。
交出去也有下限:系统排的周期任务,间隔最短只能到 15 分钟——比这更密的活交不出去,只能自己拿着。
代价也要说清楚:交给系统,就意味着任务会被推迟。所以业务逻辑得按"可延迟、可合并、可重跑"来设计——跟系统的调度策略对抗,赢不了,而且会输两次:电没省下来,功能还少了。
这套代劳大致是这么走的:屏幕熄掉、设备静置一段时间之后,系统先把网络访问收起来,只留必要的放行;接着把定时任务与同步往后推,推到它认为合适的时刻;另一条并行的线是给每个应用记一笔活跃账,长期不被使用、又爱在后台活动的那几类,会被限制得更紧。而它留出来的那些"合适的时刻",是固定的放行窗口——攒下的活集中在一小段里做完,其余时间继续睡。
六、它的边界:只管什么时候做
系统替你做的这些事,边界也要看清楚。它管的是"什么时候做":延后、攒批、限流、把零散的活收进固定的放行窗口。这些手段有一个共同点——它们都承认"这件事要做",只是调整做它的方式和时刻。
它管不了"这件事要不要做"。一段没有业务方的常驻任务,在系统的调度器眼里和一段正常的周期任务没有区别:它一样会被攒批、一样会被排进窗口,然后一样地跑下去。系统的这些手段都在优化"怎么跑",没有一种能回答"还要不要跑"。
这条边界决定了两件事的位置:第二问可以把大头交给系统,第一问只能自己动手。也正因为如此,顺序才不能反——如果连"该不该做"都没筛过,交给系统的那些活里就混着本该消失的,而系统会好好地替你把它们养着。
七、把"能等多久"写成一条要求
"能等"和"不能等"之间不是一条线,是一排刻度。刻度定错了,要么用户觉得慢,要么白等一场——问的其实是一句话:这件事的用户,能容忍它晚多久?
不能等。用户此刻正等着结果的操作——点击、下拉刷新、搜索、支付确认、验证码;以及实时通信(消息、来电、语音视频)、导航与实时监控(位置更新、健康告警)。这几类延迟几秒,用户就能察觉。
分钟级。用户离开之后仍在后台推进的活:预取下一屏要用的内容、同步一批新消息、上报一次状态。前提是用户回头打开的时候,东西已经在了。
小时级。内容更新、日志上传、统计上报、缓存刷新——晚一两个小时没人会发现。这类活只需要"最终做完",不需要"尽快做完"。
隔夜或者等充电。升级包、相册备份、索引重建、大批应用重新编译,都是小时级的重活,最该落在充电的那一段时间里。
刻度定下来之后,它就变成一条能写进代码的要求:给这件事标一个最迟完成时间,连同它的条件一起交出去。没有时限的活,条件一直不满足也不会有人知道;有了时限,系统才有依据在最后关头把它单独排进来——但这不是按时完成的保证:过了这个点,它照样可能因为深度待机这类原因再往后拖。
难的不是定刻度,是承认有些活本来就不急——多数"必须马上做"的判断,来自"以前就是这么写的",而不是来自用户真的在等。
判据只有一条:用户能感知到延迟吗?能感知的别延,感知不到的都可以谈条件。
八、假实时:那层被默认成实时的东西
还有一类藏得比较深的:"假实时"。即时消息讲究立刻送到,靠的是系统统一的推送通道,应用不用自己掐表;另一种做法是让应用自己掐着表去问——每隔三十秒问一次服务端有没有新内容。这个间隔放宽到三分钟,多数人察觉不到区别,唤醒次数却只剩原来的六分之一。更麻烦的是,这类设计往往还带着一个看起来很有道理的理由——"这样用户能最快拿到新内容";可实际上,从内容产生、到用户真正点开,中间本来就隔着好几十秒。这个"更快",用户一次都没有收到过。
回过头看那些"默认实时"的设计,里面往往有一片本来可以延后的活。
识别它只要两个问题:这个"即时"是谁提出来的——用户,还是实现?以及把间隔放宽十倍,谁会第一个发现?如果答案分别是"实现"和"没人",那这就是一桩可以立刻动手的账。
九、动手验证
谁把时刻交给了系统、谁还自己数着秒:读系统统一的后台任务队列(adb shell dumpsys jobscheduler),里面排着谁、各自等什么条件(充电、空闲、有网),再对照闹钟队列里那些自己起的定时器。两份名单一比:哪些活已经交给系统安排时刻,哪些还在自己掐着表——还在自己掐表的那些,就是这一问的改造对象。
动手三条,前两条只用一台手机。
- 挑一条周期任务,把"每 X 分钟跑一次"换成条件清单。
充电、连 WiFi、设备空闲三条一起写,再补一条兜底时限——记下它原来一天醒几次,改完再看一次。 - 数一遍条件之外还剩几个定时器。
约束交出去之后,应用里留着的"每小时检查一下条件满足了没有"的检查器,是这类改造最常见的返工点——它本身就是被省掉的那次唤醒。 - 对齐前后各录一段波形
(有功耗板条件)。同一段静置时间、同一批任务,数尖峰的个数有没有变——对齐之后,十趟并成一趟,底电流那条线不动(单次尖峰的高度和深睡底电流由器件决定,不随对齐改变)。
最后是一条排查顺序:先看有没有自己掐表 → 再看条件写对没有 → 最后看条件之外有没有又补一个闹钟。第二问的问题,基本出在这三步里。
三条结论
一、这一问的大头不用自己动手。延后、攒批、限流,系统早就在替所有应用做了。所以难处不在"想不想得到",而在有没有用对接口——该交给系统安排的活还留在自己手里掐表,那就等于白交。
二、交出去的时刻,不一定按你要的时刻回来。这正是它比第一问容易、却比第一问多一道约束的地方:业务逻辑得按"可延迟、可合并、可重跑"来设计。跟系统的调度策略对抗,赢不了,还会输两次:电没省下来,功能还少了。
三、这一问省下的不是活,是醒来的那套固定开销。把它省掉的前提,是先分清哪些活真的不能等——能感知到延迟的,一点都别等;感知不到的,等多久都只是一句话的事。
收束
第二问管的是一件事:这件事什么时候做、几件一起做。
同样的活,各醒各的,和凑成一批,是两笔账——一笔记在活上,一笔记在醒来上。第二问动的是后一笔。
这两个问题筛过之后,剩下的活已经不多了:非做不可,也非此刻不可。它们身上还能不能省,是最后一个问题。
下一篇问第三个问题:能更高效地去做吗。同样一件必须做的事,两种写法能差出一大截;但省得最多的那一招,往往被人找错地方。
《软件省电(下):最后问怎么做得省》。
(评论区聊聊:你手机上有没有哪个应用,明明可以等,却偏偏要立刻跑?)