我们在工作中发现一个问题,我们的一个应用,启动时只启动一个后台服务,而不会启动界面。并且在后台服务中网络请求超时。但是如果启动一个activity之后,就可以正常联网。
Android P(9.x)为了更加严格的限制后台应用的行为,新增了一个特性,叫做应用待机群组,可以参考官方文档:
https://developer.android.com/about/versions/pie/power
从这个文档中,我们可以得到如下关键信息:
系统会根据用户对应用的使用情况,将应用分到不同的组中,系统对不同的组内的应用限制程度是不同的。一共有五个组,文档上是这样定义的:
活跃
如果用户当前正在使用应用,应用将被归到“活跃”群组中,例如:
应用已启动一个 Activity
应用正在运行前台服务
应用的同步适配器与某个前台应用使用的 content provider 关联
用户在应用中点击了某个通知
如果应用处于“活跃”群组,系统不会对应用的作业、报警或 FCM 消息施加任何限制。
工作集
如果应用经常运行,但当前未处于活跃状态,它将被归到“工作集”群组中。 例如,用户在大部分时间都启动的某个社交媒体应用可能就属于“工作集”群组。 如果应用被间接使用,它们也会被升级到“工作集”群组中 。
如果应用处于“工作集”群组,系统会对它运行作业和触发报警的能力施加轻度限制。 如需了解详细信息,请参阅电源管理限制。
常用
如果应用会定期使用,但不是每天都必须使用,它将被归到“常用”群组中。 例如,用户在健身房运行的某个锻炼跟踪应用可能就属于“常用”群组。
如果应用处于“常用”群组,系统将对它运行作业和触发报警的能力施加较强的限制,也会对高优先级 FCM 消息的数量设定限制。 如需了解详细信息,请参阅电源管理限制。
极少使用
如果应用不经常使用,那么它属于“极少使用”群组。 例如,用户仅在入住酒店期间运行的酒店应用就可能属于“极少使用”群组。
如果应用处于“极少使用”群组,系统将对它运行作业、触发警报和接收高优先级 FCM 消息的能力施加严格限制。系统还会限制应用连接到网络的能力。 如需了解详细信息,请参阅电源管理限制。
从未使用
安装但是从未运行过的应用会被归到“从未使用”群组中。 系统会对这些应用施加极强的限制。
有两种方式可以查看一个应用的分组
1 通过调用UsageStatsManager.getAppStandbyBucket()
2 通过adb命令查看(下面是官方文档的介绍)
您可以使用 ADB 为您的应用手动指定应用待机群组。 要更改应用的群组,请使用以下命令:
$ adb shell am set-standby-bucket packagename active|working_set|frequent|rare
您还可以使用该命令一次设置多个软件包:
$ adb shell am set-standby-bucket package1 bucket1 package2 bucket2…
要检查应用处于哪一个群组,请运行以下命令:
$ adb shell am get-standby-bucket [packagename]
如果您不传递 packagename 参数,命令将列出所有应用的群组。 应用还可以调用新函数 UsageStatsManager.getAppStandbyBucket(),在运行时查找所属的群组。
从文档上我们还可以知道,有个省电管理白名单,省电管理白名单中的应用不受应用待机群组特性的限制。
因为我们的应用刚装上时,处于从未使用这个分组,官方文档中也说了,会对从未使用这个分组中的应用施加极强的限制。
知道了上述信息后,需要解决这个问题,我们需要把自己加到白名单中。关于如何设置白名单,参考如下文档:
https://developer.android.com/training/monitoring-device-state/doze-standby#support_for_other_use_cases
关键信息如下:
用户可以在 Settings > Battery > Battery Optimization 中手动配置该白名单。或者,系统会为应用提供请求用户将应用加入白名单的方式。
应用可以触发 ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS Intent,让用户直接进入 Battery Optimization,他们可以在其中添加应用。
具有 REQUEST_IGNORE_BATTERY_OPTIMIZATIONS 权限的应用可以触发系统对话框,让用户无需转到“设置”即可直接将应用添加到白名单。应用将通过触发 ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS Intent 来触发该对话框。
用户可以根据需要手动从白名单中移除应用。
在请求用户将应用添加到白名单之前,请确保应用符合加入白名单的可接受用例。
可以在设置中加入白名单,也可以在应用内通过Intent拉起对话框,让用户将应用加入白名单。但是这两种方式都依赖用户的行为。因为我们是系统应用,并且具有platform签名,所以我们要在系统中找到设置白名单的接口,并且测试调用该接口是否生效。
下面我们就根据源码来分析如何设置白名单。官方文档中提到UsageStatsManager
这个类,我们就从这个类开始。
源码位于frameworks/base/core/java/android/app/usage/UsageStatsManager.java
/**
* The app is whitelisted for some reason and the bucket cannot be changed.
* {@hide}
*/
@SystemApi
public static final int STANDBY_BUCKET_EXEMPTED = 5;
/**
* The app was used very recently, currently in use or likely to be used very soon. Standby
* bucket values that are ≤ {@link #STANDBY_BUCKET_ACTIVE} will not be throttled by the
* system while they are in this bucket. Buckets > {@link #STANDBY_BUCKET_ACTIVE} will most
* likely be restricted in some way. For instance, jobs and alarms may be deferred.
* @see #getAppStandbyBucket()
*/
public static final int STANDBY_BUCKET_ACTIVE = 10;
/**
* The app was used recently and/or likely to be used in the next few hours. Restrictions will
* apply to these apps, such as deferral of jobs and alarms.
* @see #getAppStandbyBucket()
*/
public static final int STANDBY_BUCKET_WORKING_SET = 20;
/**
* The app was used in the last few days and/or likely to be used in the next few days.
* Restrictions will apply to these apps, such as deferral of jobs and alarms. The delays may be
* greater than for apps in higher buckets (lower bucket value). Bucket values >
* {@link #STANDBY_BUCKET_FREQUENT} may additionally have network access limited.
* @see #getAppStandbyBucket()
*/
public static final int STANDBY_BUCKET_FREQUENT = 30;
/**
* The app has not be used for several days and/or is unlikely to be used for several days.
* Apps in this bucket will have the most restrictions, including network restrictions, except
* during certain short periods (at a minimum, once a day) when they are allowed to execute
* jobs, access the network, etc.
* @see #getAppStandbyBucket()
*/
public static final int STANDBY_BUCKET_RARE = 40;
/**
* The app has never been used.
* {@hide}
*/
@SystemApi
public static final int STANDBY_BUCKET_NEVER = 50;
这5个常量,就是五个分组的定义。可以看到STANDBY_BUCKET_EXEMPTED
就是白名单。
下面我们在frameworks中搜索STANDBY_BUCKET_EXEMPTED
,看一下如何使用该常量。搜索结果如下:
base/core/java/android/app/usage/UsageStatsManager.java
108: public static final int STANDBY_BUCKET_EXEMPTED = 5;
199: STANDBY_BUCKET_EXEMPTED,
base/config/hiddenapi-public-dex.txt
4712:Landroid/app/usage/UsageStatsManager;->STANDBY_BUCKET_EXEMPTED:I
base/api/system-current.txt
765: field public static final int STANDBY_BUCKET_EXEMPTED = 5; // 0x5
base/services/core/java/com/android/server/AppStateTracker.java
704: if (bucket == UsageStatsManager.STANDBY_BUCKET_EXEMPTED) {
base/services/usage/java/com/android/server/usage/AppStandbyController.java
40:import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_EXEMPTED;
630: STANDBY_BUCKET_EXEMPTED, REASON_MAIN_DEFAULT);
633: STANDBY_BUCKET_EXEMPTED, REASON_MAIN_DEFAULT, false);
base/services/tests/servicestests/src/com/android/server/AppStateTrackerTest.java
632: UsageStatsManager.STANDBY_BUCKET_EXEMPTED, REASON_MAIN_DEFAULT);
644: UsageStatsManager.STANDBY_BUCKET_EXEMPTED, REASON_MAIN_DEFAULT);
664: UsageStatsManager.STANDBY_BUCKET_EXEMPTED, REASON_MAIN_DEFAULT);
666: UsageStatsManager.STANDBY_BUCKET_EXEMPTED, REASON_MAIN_DEFAULT);
base/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java
30:import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_EXEMPTED;
280: assertEquals(STANDBY_BUCKET_EXEMPTED,
283: assertNotEquals(STANDBY_BUCKET_EXEMPTED,
可以看到AppStandbyController.java
这个类引用了STANDBY_BUCKET_EXEMPTED
。我们打开base/services/usage/java/com/android/server/usage/AppStandbyController.java
的630行:
/** Check if we need to update the standby state of a specific app. */
private void checkAndUpdateStandbyState(String packageName, @UserIdInt int userId,
int uid, long elapsedRealtime) {
if (uid <= 0) {
try {
uid = mPackageManager.getPackageUidAsUser(packageName, userId);
} catch (PackageManager.NameNotFoundException e) {
// Not a valid package for this user, nothing to do
// TODO: Remove any history of removed packages
return;
}
}
final boolean isSpecial = isAppSpecial(packageName,
UserHandle.getAppId(uid),
userId);
if (DEBUG) {
Slog.d(TAG, " Checking idle state for " + packageName + " special=" +
isSpecial);
}
if (isSpecial) {
synchronized (mAppIdleLock) {
mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime,
STANDBY_BUCKET_EXEMPTED, REASON_MAIN_DEFAULT);
}
maybeInformListeners(packageName, userId, elapsedRealtime,
STANDBY_BUCKET_EXEMPTED, REASON_MAIN_DEFAULT, false);
} else {
所在的方法是checkAndUpdateStandbyState
,该方法在系统需要改变某个app的分组时调用。可以看到,如果符合isSpecial
的条件,就会把应用设为STANDBY_BUCKET_EXEMPTED
分组,也就是加入白名单。
我们接着看什么样的应用是special
的:
/** Returns true if this app should be whitelisted for some reason, to never go into standby */
boolean isAppSpecial(String packageName, int appId, int userId) {
if (packageName == null) return false;
// If not enabled at all, of course nobody is ever idle.
if (!mAppIdleEnabled) {
return true;
}
if (appId < Process.FIRST_APPLICATION_UID) {
// System uids never go idle.
return true;
}
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 true;
}
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 (mInjector.isPowerSaveWhitelistExceptIdleApp(packageName)) {
return true;
}
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
if (isActiveDeviceAdmin(packageName, userId)) {
return true;
}
if (isActiveNetworkScorer(packageName)) {
return true;
}
if (mAppWidgetManager != null
&& mInjector.isBoundWidgetPackage(mAppWidgetManager, packageName, userId)) {
return true;
}
if (isDeviceProvisioningPackage(packageName)) {
return true;
}
}
// Check this last, as it can be the most expensive check
if (isCarrierApp(packageName)) {
return true;
}
return false;
}
一些特殊的包是special的,比如包名为android的包,uid小于Process.FIRST_APPLICATION_UID
的包,设备管理器,开机向导等。这些对于解决我们的问题没有帮助。除此之外mInjector.isPowerSaveWhitelistExceptIdleApp(packageName)
的包也是special的,这个就是省电管理的白名单。
下面我们看一下如何加到这个名单中。
AppStandbyController.java的内部类Injector的实现如下:
static class Injector {
private final Context mContext;
private final Looper mLooper;
private IDeviceIdleController mDeviceIdleController;
private IBatteryStats mBatteryStats;
private PackageManagerInternal mPackageManagerInternal;
private DisplayManager mDisplayManager;
private PowerManager mPowerManager;
int mBootPhase;
Injector(Context context, Looper looper) {
mContext = context;
mLooper = looper;
}
Context getContext() {
return mContext;
}
Looper getLooper() {
return mLooper;
}
void onBootPhase(int phase) {
if (phase == PHASE_SYSTEM_SERVICES_READY) {
mDeviceIdleController = IDeviceIdleController.Stub.asInterface(
ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER));
mBatteryStats = IBatteryStats.Stub.asInterface(
ServiceManager.getService(BatteryStats.SERVICE_NAME));
mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
mDisplayManager = (DisplayManager) mContext.getSystemService(
Context.DISPLAY_SERVICE);
mPowerManager = mContext.getSystemService(PowerManager.class);
}
mBootPhase = phase;
}
int getBootPhase() {
return mBootPhase;
}
/**
* Returns the elapsed realtime since the device started. Override this
* to control the clock.
* @return elapsed realtime
*/
long elapsedRealtime() {
return SystemClock.elapsedRealtime();
}
long currentTimeMillis() {
return System.currentTimeMillis();
}
boolean isAppIdleEnabled() {
final boolean buildFlag = mContext.getResources().getBoolean(
com.android.internal.R.bool.config_enableAutoPowerModes);
final boolean runtimeFlag = Global.getInt(mContext.getContentResolver(),
Global.APP_STANDBY_ENABLED, 1) == 1
&& Global.getInt(mContext.getContentResolver(),
Global.ADAPTIVE_BATTERY_MANAGEMENT_ENABLED, 1) == 1;
return buildFlag && runtimeFlag;
}
boolean isCharging() {
return mContext.getSystemService(BatteryManager.class).isCharging();
}
boolean isPowerSaveWhitelistExceptIdleApp(String packageName) throws RemoteException {
return mDeviceIdleController.isPowerSaveWhitelistExceptIdleApp(packageName);
}
获取省电管理的白名单调用的是mDeviceIdleController的方法:
mDeviceIdleController = IDeviceIdleController.Stub.asInterface(
ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER));
boolean isPowerSaveWhitelistExceptIdleApp(String packageName) throws RemoteException {
return mDeviceIdleController.isPowerSaveWhitelistExceptIdleApp(packageName);
}
所以我们可以猜测,添加白名单的操作也会实现在mDeviceIdleController中。下面我们找DeviceIdleController的实现。从IDeviceIdleController.Stub.asInterface
可以看出来DeviceIdleController应该是一个系统服务。
zhangjg@zjg:~/deve/android/frameworks$ find . -name "DeviceIdleController*"
./base/services/core/java/com/android/server/DeviceIdleController.java
通过find搜索,我们找到一个DeviceIdleController.java,该文件在frameworks/base/services/
下,所以我们可以确定这个就是DeviceIdleController
服务端的实现。在该文件中,我们确实找到了DeviceIdleController服务的实现:
private final class BinderService extends IDeviceIdleController.Stub {
@Override public void addPowerSaveWhitelistApp(String name) {
if (DEBUG) {
Slog.i(TAG, "addPowerSaveWhitelistApp(name = " + name + ")");
}
getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER,
null);
long ident = Binder.clearCallingIdentity();
try {
addPowerSaveWhitelistAppInternal(name);
} finally {
Binder.restoreCallingIdentity(ident);
}
}
可以确定添加白名单的方法为addPowerSaveWhitelistApp
,调用该方法需要android.Manifest.permission.DEVICE_POWER
权限。我们看一下是否能够申请到这个权限。
该权限定义在frameworks/base/core/res/AndroidManifest.xml
中:
<permission android:name="android.permission.DEVICE_POWER"
android:protectionLevel="signature" />
该权限的protectionLevel为signature,而我们的应用是系统platform签名,所以可以申请到该权限。
下面我们搜一下如何调用addPowerSaveWhitelistApp
这个方法,搜索结果如下:
zhangjg@zjg:~/deve/android/frameworks$ ag "addPowerSaveWhitelistApp"
base/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/PowerWhitelistBackendTest.java
71: doNothing().when(mDeviceIdleService).addPowerSaveWhitelistApp(anyString());
92: verify(mDeviceIdleService, atLeastOnce()).addPowerSaveWhitelistApp(PACKAGE_TWO);
base/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerWhitelistBackend.java
135: mDeviceIdleService.addPowerSaveWhitelistApp(pkg);
base/core/java/android/os/IDeviceIdleController.aidl
24: void addPowerSaveWhitelistApp(String name);
base/services/core/java/com/android/server/DeviceIdleController.java
1160: @Override public void addPowerSaveWhitelistApp(String name) {
1162: Slog.i(TAG, "addPowerSaveWhitelistApp(name = " + name + ")");
1168: addPowerSaveWhitelistAppInternal(name);
1551: public boolean addPowerSaveWhitelistAppInternal(String name) {
3065: if (addPowerSaveWhitelistAppInternal(pkg)) {
base/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java
174: mDeviceIdleController.addPowerSaveWhitelistApp(TEST_APP_PACKAGE);
base/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
454: mIdleController.addPowerSaveWhitelistApp(packageInfo.packageName);
base/config/boot-image-profile.txt
27580:HSPLandroid/os/IDeviceIdleController;->addPowerSaveWhitelistApp(Ljava/lang/String;)V
base/config/hiddenapi-private-dex.txt
62300:Landroid/os/IDeviceIdleController$Stub$Proxy;->addPowerSaveWhitelistApp(Ljava/lang/String;)V
62328:Landroid/os/IDeviceIdleController$Stub;->TRANSACTION_addPowerSaveWhitelistApp:I
62351:Landroid/os/IDeviceIdleController;->addPowerSaveWhitelistApp(Ljava/lang/String;)V
在base/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerWhitelistBackend.java
中有这个调用方式。下面我们打开这个文件的135行:
public void addApp(String pkg) {
try {
mDeviceIdleService.addPowerSaveWhitelistApp(pkg);
mWhitelistedApps.add(pkg);
} catch (RemoteException e) {
Log.w(TAG, "Unable to reach IDeviceIdleController", e);
}
}
mDeviceIdleService
的初始化如下:
public PowerWhitelistBackend(Context context) {
this(context, IDeviceIdleController.Stub.asInterface(
ServiceManager.getService(DEVICE_IDLE_SERVICE)));
}
@VisibleForTesting
PowerWhitelistBackend(Context context, IDeviceIdleController deviceIdleService) {
mAppContext = context.getApplicationContext();
mDeviceIdleService = deviceIdleService;
refreshList();
}
所以通过在我们的app中调用如下代码,可以加到白名单中:
IDeviceIdleController deviceIdleService = IDeviceIdleController.Stub.asInterface(
ServiceManager.getService(DEVICE_IDLE_SERVICE));
deviceIdleService.addPowerSaveWhitelistApp(pkg);
到此为止问题可以解决了。
其实在分析frameworks/base/core/java/android/app/usage/UsageStatsManager.java
文件的时候,我们只看到了5个分组的定义。但是在该文件中,还定义了两个方法用来获取分组和设置分组:
/**
* {@hide}
* Returns the current standby bucket of the specified app. The caller must hold the permission
* android.permission.PACKAGE_USAGE_STATS.
* @param packageName the package for which to fetch the current standby bucket.
*/
@SystemApi
@RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS)
public @StandbyBuckets int getAppStandbyBucket(String packageName) {
try {
return mService.getAppStandbyBucket(packageName, mContext.getOpPackageName(),
mContext.getUserId());
} catch (RemoteException e) {
}
return STANDBY_BUCKET_ACTIVE;
}
/**
* {@hide}
* Changes an app's standby bucket to the provided value. The caller can only set the standby
* bucket for a different app than itself.
* @param packageName the package name of the app to set the bucket for. A SecurityException
* will be thrown if the package name is that of the caller.
* @param bucket the standby bucket to set it to, which should be one of STANDBY_BUCKET_*.
* Setting a standby bucket outside of the range of STANDBY_BUCKET_ACTIVE to
* STANDBY_BUCKET_NEVER will result in a SecurityException.
*/
@SystemApi
@RequiresPermission(android.Manifest.permission.CHANGE_APP_IDLE_STATE)
public void setAppStandbyBucket(String packageName, @StandbyBuckets int bucket) {
try {
mService.setAppStandbyBucket(packageName, bucket, mContext.getUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
可以看到直接调用setAppStandbyBucket,是可以改变应用分组的。只要我们把分组设为STANDBY_BUCKET_EXEMPTED
,我们的应用就不会受后台限制。第一种方式是通过添加到省电管理白名单中,间接将应用的分组设为STANDBY_BUCKET_EXEMPTED, 相对于上面的第一种方式,这种方式更直接。
调用setAppStandbyBucket方法,需要CHANGE_APP_IDLE_STATE
权限。该权限同样定义在frameworks/base/core/res/AndroidManifest.xml
中:
<permission android:name="android.permission.CHANGE_APP_IDLE_STATE"
android:protectionLevel="signature|privileged" />
该权限的protectionLevel为signature|privileged
,因为我们的应用是系统platform签名,所以可以申请到该权限,是可以调用该方法的。
下一篇文章分析应用待机分组是如何限制应用的联网行为的。