2015年5月29日GoogleI/O大会发布新一代Android系统 - Android M preview 版本(API-"MNC")。该版本在电量续航能力方面针对整个系统和单个应用分别增加了特性doze和App standby。除范围不同外两者差别不大,这里仅讨论doze(休眠模式),代码分析主要基于Release 1版本,同时注明Release 2版本改动之处。相关文档可见https://developer.android.com/preview/behavior-changes.html#behavior-power。
在Android4.4的Wear系统(API 20)第一次引入doze概念,当时用在android.view.Display的state成员取值中,并在android5.0推广到大部分Android设备,当其时旨在描述在屏幕开启状态只临时显示静态(无交互)内容的低功耗状态。在Android M中,doze模式的含义略有修改,其含义为只允许少量后台进程活动的“IDEL”状态,这可以看做是android为了解决其饱受诟病的续航能力问题而进一步“伪后台”化,即在某种状态中限制大部分app的运行。
类似的权限管理通常都会有白名单,doze也不例外,名单中的应用不受上述doze限制,例如系统自带的下载服务,Google Play及GMS服务都默认加入白名单。用户可以通过系统设置->应用->高级->忽略优化界面添加或移除白名单,如下图所示。
使用adb命令可以手动将手机切入doze模式,即IDLE状态进行调试。
带USB调试的时候要先将充电模式禁止掉,使用battery服务的unplug命令。
顾名思义,dumpsys是用来查看系统service,而doze相关的系统服务是"deviceidle",如下命令即可查看。
新增加的“deviceidle”服务是通过IDeviceIdleController接口实现的,后面将会对其进行源码分析。 进入doze需要满足三方面的条件,这些条件控制着DeviceIdleController内部的状态机实现,分为5个状态:
[注]Release 2在IDEL_PENDING和IDLE状态间增加了SENSING表示在进入IDLE侦测运动情况的状态。
我们可使用下面命令dump出手机当前的IDLE状态信息,包括白名单列表。
在禁用充电模式关闭屏幕后,手机会进入INACTIVE状态,此时通过step命令来手工控制状态切换。
也可以通过whitelist命令增加或删除白名单应用。
下面基于Android M Preview Release 1 版本对doze相关代码进行分析。系统在com/android/server路径下新增了一个继承自SystemService的DeviceIdleController服务类负责doze的控制逻辑。其内部字符串常量SERVICE_NAME明确定义了其服务的名称为“deviceidle”。
[注]Release 2该定义由Context.java新增常量DEVICE_IDLE_CONTROLLER取代。
系统定义了5个int常量表示5个状态。
在事件响应和状态切换方面,依靠其内部BroadcastReceiver类成员和两个listener实现驱动。
DeviceIdleController通过响应内外部事件完成状态驱动。实现逻辑如下:
内部定义的状态切换事件ACTION_STEP_IDLE_STATE由AlarmManager类成员根据预设时间触发,mReceiver接收到事件后调用stepIdleStateLocked()完成状态切换。
非ACTIVE状态之间的转换都是通过预设时间不同的alarm触发ACTION_STEP_IDLE_STATE事件完成跳转:
(a)如果没有外界干扰,进入IDLE态之后,即doze模式,系统就会在IDLE和IDLE_MAINTENAEC之前切换,后者会允许应用程序做一些事情(时间在5~10min)
(b)接收到某些外部事件则有可能转换为ACTIVE状态,包括充电/亮屏/运动等,以mReceiver处理的USB充电事件为例,相应的处理方法updateChargingLocked()实现如下:
USB插入充电会将手机马上唤醒,切换到ACTIVE状态并且停止运动检测;如果是拔出则视屏幕关闭等条件决定是否将其切换到INACTIVE状态,若发生切换则同时设定一个alarm(默认30min)看是否需要进一步发送ACTION_STEP_IDLE_STATE事件触发后续的状态切换。
先插一句,遗憾的是当前DeviceIdleController没有提供任何公开API给上层应用使用。先来看看系统服务是如何与其交互的。
DeviceIdleController没有提供接口访问私有成员mState,而是通过其内部的Handler成员把IDLE开关两个状态传给系统服务PowerManager,NetworkPolicyManager,BatteryStats等。
系统提供了接口IDeviceIdleController,DeviceIdleController内部类BinderSevice实现该接口,在启动时以“deviceidle”的名字将后者实例注册到系统中。
系统其他服务程序则通过AIDL方式调用访问,如NetworkPolicyManagerService新增加如下接口成员。
IDeviceIdleController一共定义了7个方法,用于增加/删除/判断白名单,以及调试用到的dump命令。
开发者很容易想到使用上面系统服务一样的方式利用白名单,很遗憾,最关键的add/remove接口需要DEVICE_POWER系统权限,如何获得该权限这里不详述,总之就是也要把自己变成系统级应用,和系统共享数据,这个代价比较大。不过在判断自身是否在白名单这一问题上通过hack接口isPowerSaveWhitelistApp()的方式还是还是可行。
[注]Release 2中已经将判断应用是否在白名单这一功能接口在PowerManager.java中公开,接口实现如下:
既然说到白名单,顺便看一下其保存位置,系统路径“/data/system/deviceidle.xml”,所以没有root权限就不用多想直接对其修改。
至此难道真的不能再取到更多的信息了吗?回头看其利用内部Handler成员将idle mode状态传给了LocalPowerManager,进而查看PowerManager代码,发现其新增如下一个事件定义和一个状态接口,两者均公开,故可以通过注册BroadcastReceiver的方式监听该事件。同步管理SyncManager正是采用这种方式获知系统进入和退出doze的时机。
在明确如何手工进入doze和监听事件后,可以验证下doze模式下网络连接情况。在子线程中测试下面简单的连接请求,发现子线程在openConnection后一直被挂起。
换下面的网络连接检查代码:
在doze状态下isAvailable接口返回true,而isConnected返回是false,网络连接失败,查看系统日志发现这样一行输出:
也就是APP的网络连接被BLOCKED掉,翻看其对应的系统服务ConnectivityService源码找到如下方法:
[注]Release 2把上面的系统debug log关闭。
继续看isNetworkWithLinkPropertiesBlocked()。
也就是系统通过应用uid维护了一份网络连接策略规则列表,该列表通过AIDL从NetworkPolicyManagerService同步而来。
绕了一圈再来看NetworkPolicyManagerService。
上面的代码片段明确指出如果在doze模式下限制所有后台非白名单的网络访问,返回RULE_REJECT_METERED。
对于网络应用,特别是如微信等IM应用,doze模式下限制网络,消息收发功能必然受到影响,Android给出了解决方案-GCM:
微信本身已经具备注册接收GCM推送功能,在接收到GCM推送消息后,会取拉取消息内容,前一个步骤由系统GCM服务完成,GCM服务默认已在白名单中,而后面拉取的动作需要微信联网完成。
经过测试在doze模式下,即使接收到了GCM推送后,应用再发起网络连接的结果和上面的网络测试一样,仍旧是被禁止的!Google决心强推所有的消息接收都只能依靠GCM推送!?只能说持续跟进+拭目以待。
[注]使用Release 2测试结果和1一致,Google方面确认此处存在bug导致应用无法加到临时白名单中,此问题已在修复中。
最后探讨下应用如何“悄悄”地使系统退出doze模式。根据doze的条件,在没有充电的情况下,只能通过亮屏或震动等外部事件触发系统退出IDLE状态。
1.亮屏
APP拥有“android.permission.WAKE_LOCK”权限,执行下面代码即可点亮屏幕,实测可以让手机马上退出doze模式。
2.震动
DeviceIdleController使用的运动传感器是Significant motion,单触发低功耗的传感器,调用deviceidle dump命令即可查看实际传感器信息。
APP拥有“android.permission.VIBRATE”权限,执行下面代码即可能触发手机震动退出doze模式。
综上,doze的限制很多,但是不可否认地是其对手机的续航能力,特别是夜间待机的提升是相当显著的。普通应用如何顺应其趋势要求又能保持原有功能及体验效果,这是一个值得深入考虑的问题。