一、概述
低电耗模式和应用待机模式是从Android M引入的新特性,之前一直没有分析,低电耗模式就是Doze,应用待机模式就是 App Standby。
Doze模式我们之前分析过了,Doze模式在Android N又有修改,Android 6.0(API 级别 23)引入了低电耗模式,当用户设备未插接电源、处于静止状态且屏幕关闭时,该模式会推迟 CPU 和网络活动,从而延长电池寿命。而 Android 7.0 则通过在设备未插接电源且屏幕关闭状态下、但不一定要处于静止状态(例如用户外出时把手持式设备装在口袋里)时应用部分 CPU 和网络限制,进一步增强了低电耗模式。这个我们之后再分析。
这里主要分析App standy模式。
二、实现
设置应用的待机模式接口主要是从UsageStatsManager的setAppInactive接口,实现在UsageStatsService的setAppInactive接口,这个接口最后调用了setAppIdleAsync函数
void setAppIdleAsync(String packageName, boolean idle, int userId) {
if (packageName == null) return;
mHandler.obtainMessage(MSG_FORCE_IDLE_STATE, userId, idle ? 1 : 0, packageName)
.sendToTarget();
}
这个消息的处理函数是forceIdleState函数主要是调用了AppIdleHistory的setIdle函数。
void forceIdleState(String packageName, int userId, boolean idle) {
final int appId = getAppId(packageName);
if (appId < 0) return;
final long elapsedRealtime = SystemClock.elapsedRealtime();//系统开机后的时间值
final boolean previouslyIdle = isAppIdleFiltered(packageName, appId,
userId, elapsedRealtime);
synchronized (mAppIdleLock) {
mAppIdleHistory.setIdle(packageName, userId, idle, elapsedRealtime);
}
final boolean stillIdle = isAppIdleFiltered(packageName, appId,
userId, elapsedRealtime);
// Inform listeners if necessary
if (previouslyIdle != stillIdle) {//改变了就通知listener
mHandler.sendMessage(mHandler.obtainMessage(MSG_INFORM_LISTENERS, userId,
/* idle = */ stillIdle ? 1 : 0, packageName));
if (!stillIdle) {
notifyBatteryStats(packageName, userId, idle);
}
}
}
因此我们还是继续分析AppIdleHistory的setIdle函数。这里getElapsedTime函数就是当前从系统开始的所有时间,getScreenOnTime是当前screenOn的所有时间,而mElapsedTimeThreahold为12小时,MScreenOntimeThreshold为2天。
设置Idle为false,就是lastUsedScreenTime会不一样。
public void setIdle(String packageName, int userId, boolean idle, long elapsedRealtime) {
ArrayMap<String, PackageHistory> userHistory = getUserHistory(userId);
PackageHistory packageHistory = getPackageHistory(userHistory, packageName,
elapsedRealtime);
packageHistory.lastUsedElapsedTime = getElapsedTime(elapsedRealtime)
- mElapsedTimeThreshold;
packageHistory.lastUsedScreenTime = getScreenOnTime(elapsedRealtime)
- (idle ? mScreenOnTimeThreshold : 0) - 1000 /* just a second more */;
}
这里设置app的Idle就是通过了时间,下面我们来看如果判断一个app是否Idle,通过UsageStatsService的isAppIdle函数,这个函数调用了isAppIdleFiltered函数,这里的时间是当前的时间。这个函数就是去除各种app,如果是系统app、带android字眼的app等都不是idle,而且DeviceIdleController的白名单app也不是Idle
private boolean isAppIdleFiltered(String packageName, int appId, int userId,
long elapsedRealtime) {
if (packageName == null) return false;
// If not enabled at all, of course nobody is ever idle.
if (!mAppIdleEnabled) {
return false;
}
if (appId < Process.FIRST_APPLICATION_UID) {
// System uids never go idle.
return false;
}
if (packageName.equals("android")) {
// Nor does the framework (which should be redundant with the above, but for MR1 we will
// retain this for safety).
return false;
}
if (mSystemServicesReady) {
try {
// We allow all whitelisted apps, including those that don't want to be whitelisted
// for idle mode, because app idle (aka app standby) is really not as big an issue
// for controlling who participates vs. doze mode.
if (mDeviceIdleController.isPowerSaveWhitelistExceptIdleApp(packageName)) {
return false;
}
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
if (isActiveDeviceAdmin(packageName, userId)) {
return false;
}
if (isActiveNetworkScorer(packageName)) {
return false;
}
if (mAppWidgetManager != null
&& mAppWidgetManager.isBoundWidgetPackage(packageName, userId)) {
return false;
}
if (isDeviceProvisioningPackage(packageName)) {
return false;
}
}
if (!isAppIdleUnfiltered(packageName, userId, elapsedRealtime)) {
return false;
}
// Check this last, as it is the most expensive check
// TODO: Optimize this by fetching the carrier privileged apps ahead of time
if (isCarrierApp(packageName)) {
return false;
}
return true;
}
我们最后主要看下IsAppIdleUnfiltered函数,这个函数最后调用了AppIdleHistory.isIdle函数
private boolean isAppIdleUnfiltered(String packageName, int userId, long elapsedRealtime) {
synchronized (mAppIdleLock) {
return mAppIdleHistory.isIdle(packageName, userId, elapsedRealtime);
}
}
我们再来看AppIdleHistory.isIdle函数,最后调用了hasPassedThresholds函数
public boolean isIdle(String packageName, int userId, long elapsedRealtime) {
ArrayMap<String, PackageHistory> userHistory = getUserHistory(userId);
PackageHistory packageHistory =
getPackageHistory(userHistory, packageName, elapsedRealtime);
if (packageHistory == null) {
return false; // Default to not idle
} else {
return hasPassedThresholds(packageHistory, elapsedRealtime);
}
}
最后来看hasPassedThresholds函数,结合之前设置这两个参数来看,当我们之前setIdle设置为true,这里获取的肯定也是true。因为会把mScreenOnTimeThreshold和mElapsedTimeThreshold去除再来做比较。之前setIdle设置为false,那么packageHistory.lastUsedScreenTime + mScreenOnTimeThreshold会大于当前时间(前提是小于12小时),也就是说12小时内该app不是Idle,超过12小时还是Idle。
private boolean hasPassedThresholds(PackageHistory packageHistory, long elapsedRealtime) {
return (packageHistory.lastUsedScreenTime
<= getScreenOnTime(elapsedRealtime) - mScreenOnTimeThreshold)
&& (packageHistory.lastUsedElapsedTime
<= getElapsedTime(elapsedRealtime) - mElapsedTimeThreshold);
}
三、限制
最后我们来看应用是Idle会有哪些限制,被设置我Idle的应用会限制网络访问,具体通过NetworkPolicyManagerService,最后应该通过iptables来实现。
四、命令
我们可以使用如下命令来使应用进入应用待机模式。
1. 首先要运行应用并使其保持活动状态
2. 通过运行以下命令强制应用进入应用待机模式:
$ adb shell dumpsys battery unplug
$ adb shell am set-inactive <packageName> true
3. 使用以下命令模拟唤醒应用:
$ adb shell am set-inactive <packageName> false
$ adb shell am get-inactive <packageName>