Android O_GO後臺啓動服務改動
1. 問題現象
應用在適配Android O/GO的系統時,會發現後臺啓動不了服務,會報出如下異常,並強退:
java.lang.RuntimeException:
Caused by: java.lang.IllegalStateException: Not allowed to start service Intent
{ cmp=com.android.test/com.android.test.TestService (has extras) }:
app is in background uid UidRecord{255693 u0a26 RCVR idle procs:2 seq(0,0,0)}
at android.app.ContextImpl.startServiceCommon(ContextImpl.java:1506)
at android.app.ContextImpl.startService(ContextImpl.java:1462)
at android.content.ContextWrapper.startService(ContextWrapper.java:648)
at android.content.ContextWrapper.startService(ContextWrapper.java:648)
異常狀態是:”IllegalStateException”非法狀態,
內容是:”Not allowed to start service”不允許啓動服務,
原因是:”app is in background”應用在後臺運行
爲什麼google會搞出這東西呢:
=> google對於app的權限釋放得太多了,所以android手機卡頓、耗電快的問題一直都困擾着用戶,
特別是國內很多流氓的apk,不僅自己佔資源,還拉別的應用一起來。
google無奈,只能收回權限,目前收回了很多後臺運行進程的權限(啓動服務、接收廣播等),如此處限制了後臺啓動服務。
這樣做對於android系統性能、功耗確實會有提升,但是道高一尺魔高一丈,我們總是有辦法的。
2. 問題原因分析
需要先找出問題的原因再討論修復方案。
2.1 出錯代碼定位
在ContextImpl的startServiceCommon函數中爆出異常,
//frameworks/base/core/java/android/app/ContextImpl.java
private ComponentName startServiceCommon(Intent service, boolean requireForeground,
UserHandle user) {
try {
validateServiceIntent(service);
service.prepareToLeaveProcess(this);
ComponentName cn = ActivityManager.getService().startService(
mMainThread.getApplicationThread(), service, service.resolveTypeIfNeeded(
getContentResolver()), requireForeground,
getOpPackageName(), user.getIdentifier());
if (cn != null) {
//...
//此處就是曝出異常的地方,非法狀態,不允許啓動服務
} else if (cn.getPackageName().equals("?")) {
throw new IllegalStateException(
"Not allowed to start service " + service + ": " + cn.getClassName());
}
}
return cn;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
startServiceCommon這個函數做的操作是AMS的startService,用於啓動服務.
2.2 AMS的startService
接下去看AMS的startService,稍微注意一下傳遞的參數,裏面有一個前臺後臺相關的requireForeground,可能跟問題有關係。
AMS代碼位置
//frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
public ComponentName startService(IApplicationThread caller, Intent service,
String resolvedType, boolean requireForeground, String callingPackage, int userId)
throws TransactionTooLargeException {
//...
//調用ActiveServices的startServiceLocked
res = mServices.startServiceLocked(caller, service,
resolvedType, callingPid, callingUid,
requireForeground, callingPackage, userId);
//...
}
其最終會調用ActiveServices的startServiceLocked
2.3 ActiveServices的startServiceLocked
ActiveServices這裏的代碼很多都是很關鍵的,如果對Android組件Service感興趣的最好把這個文件研究一下。
ActiveServices代碼位置
//frameworks/base/services/core/java/com/android/server/am/ActiveServices.java
ComponentName startServiceLocked(IApplicationThread caller, Intent service, String resolvedType,
int callingPid, int callingUid, boolean fgRequired, String callingPackage, final int userId)
throws TransactionTooLargeException {
//...
// 啓動服務之前有2個判斷一個是startRequested,一個是fgRequired。
// startRequested代表的是:是否已經啓動過服務,一般出現問題都是啓動一個沒有運行的服務,
// 那麼這個就是false。
// fgRequired這個就是啓動服務傳遞的requireForeground,
// 可以查看2.1章節的startServiceCommon函數。
if (!r.startRequested && !fgRequired) {
// 這裏面有個關鍵函數getAppStartModeLocked,判斷是否運行啓動服務
// 注意此處傳遞的最後2個參數:alwaysRestrict和disabledOnly都是false
final int allowed = mAm.getAppStartModeLocked(r.appInfo.uid, r.packageName,
r.appInfo.targetSdkVersion, callingPid, false, false);
// 如果不允許啓動服務則會運行到裏面
if (allowed != ActivityManager.APP_START_MODE_NORMAL) {
//...
UidRecord uidRec = mAm.mActiveUids.get(r.appInfo.uid);
// 此處就是不允許運行服務返回的原因"app is in background"
// 和章節1中的問題現象的原因是一致的
return new ComponentName("?", "app is in background uid " + uidRec);
}
}
//...
}
這裏面由於出現錯誤(問題現象具體可以查看:章節1),那麼startRequested==false而且fgRequired==false,說明這個服務是第一次啓動,而且是後臺請求啓動服務。
至於爲什麼不允許啓動服務,我們還需要查看AMS的getAppStartModeLocked函數。
2.4 AMS判斷並返回服務啓動模式
AMS代碼位置
//frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
1、getAppStartModeLocked返回值就是啓動模式,其中此處傳遞的alwaysRestrict==false
int getAppStartModeLocked(int uid, String packageName, int packageTargetSdk,
int callingPid, boolean alwaysRestrict, boolean disabledOnly) {
UidRecord uidRec = mActiveUids.get(uid);
//...
// 此處alwaysRestrict==false,於是調用的是appServicesRestrictedInBackgroundLocked
final int startMode = (alwaysRestrict)
? appRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk)
: appServicesRestrictedInBackgroundLocked(uid, packageName,
packageTargetSdk);
//...
return startMode;
//...
}
根據alwaysRestrict的值會調用appRestrictedInBackgroundLocked或者appServicesRestrictedInBackgroundLocked;
其中appRestrictedInBackgroundLocked是直接根據應用sdk進行判斷,
appServicesRestrictedInBackgroundLocked會進行條件過濾,直接運行部分應用啓動服務,其它的進行應用sdk的判斷。
2、appServicesRestrictedInBackgroundLocked進行條件過濾,允許部分啓動服務
int appServicesRestrictedInBackgroundLocked(int uid, String packageName, int packageTargetSdk) {
// 如果是常駐內存的,可以直接啓動服務
if (mPackageManagerInt.isPackagePersistent(packageName)) {
//...
return ActivityManager.APP_START_MODE_NORMAL;
}
// 如果是非常駐內存的話,但是在白名單列表裏面的uid也是允許的
// 目前這個白名單裏面就只有一個:藍牙BLUETOOTH_UID = 1002
if (uidOnBackgroundWhitelist(uid)) {
//...
return ActivityManager.APP_START_MODE_NORMAL;
}
// 如果是在電源相關的白名單裏面,也是允許啓動服務的
if (isOnDeviceIdleWhitelistLocked(uid)) {
//...
return ActivityManager.APP_START_MODE_NORMAL;
}
// 默認的策略是appRestrictedInBackgroundLocked
return appRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk);
}
分別對於:
1) 是否常駐內存應用,常駐內存允許啓動服務
2) 如果是藍牙也是允許啓動服務
3) 是在電源相關的DeviceIdle白名單裏面,允許啓動服務的
4) 如果都不是則執行默認策略appRestrictedInBackgroundLocked
3、appRestrictedInBackgroundLocked默認限制策略
int appRestrictedInBackgroundLocked(int uid, String packageName, int packageTargetSdk) {
// 如果apk的sdk版本大於AndroidO的話,那麼默認是不允許啓動服務的
if (packageTargetSdk >= Build.VERSION_CODES.O) {
//...
return ActivityManager.APP_START_MODE_DELAYED_RIGID;
}
// 如果是之前版本的apk,會查看AppOps是否允許後臺運行權限,
// 由於我們sdk版本肯定會升級的,這個就暫時不考慮了
int appop = mAppOpsService.noteOperation(AppOpsManager.OP_RUN_IN_BACKGROUND,
uid, packageName);
//...
switch (appop) {
case AppOpsManager.MODE_ALLOWED:
return ActivityManager.APP_START_MODE_NORMAL;
case AppOpsManager.MODE_IGNORED:
return ActivityManager.APP_START_MODE_DELAYED;
default:
return ActivityManager.APP_START_MODE_DELAYED_RIGID;
}
}
如果apk的sdk版本大於AndroidO的話,那麼默認是不允許啓動服務的,那麼要適配Android O/GO以後的版本,此處是繞不過去的坎,建議儘早處理。
3. 修改方案
有上面可知,問題原因主要是後臺啓動了服務,在這部分Android O/GO做了限制,
根據章節2.4的過濾條件可以提供如下修改方案:
1) 提升應用優先級到常駐內存級別 (不建議應用採納這種方式,會導致手機出現很多性能問題)
=> 在AndroidManifest.xml添加android:persistent=”true”
並且簽上系統簽名
2) 類似與藍牙BLUETOOTH_UID一樣放在白名單裏面(需要擁有源碼修改權限,而且修改了源碼,不利於apk的版本兼容,不建議採納)
3) 添加在電源相關的DeviceIdle白名單(不建議添加,可能導致功耗增加)
按照上面的都說是不建議採取,是否沒有辦法了呢?
我們繼續往源頭找找看看是否有辦法:
在章節2.1、章節2.3有一個參數requireForeground/fgRequired,是否前臺請求,如果requireForeground/fgRequired爲false纔會進行後臺請求判斷,如果是true的話,是可以直接繞過去的
回到ActiveServices的startServiceLocked=>
ComponentName startServiceLocked(IApplicationThread caller, Intent service, String resolvedType,
int callingPid, int callingUid, boolean fgRequired, String callingPackage, final int userId)
throws TransactionTooLargeException {
//...
// fgRequired是true可以直接繞過
if (!r.startRequested && !fgRequired) {
//..
}
//...
}
那麼方案4,我們可以採取如下方式
4) 通過Context(activity、service的this都是包含context的,故不用擔心調用方式),將之前的startService,修改成ContextImpl的startForegroundService或者startForegroundServiceAsUser方法,啓動一個前臺服務。
//frameworks/base/core/java/android/app/ContextImpl.java
@Override
public ComponentName startForegroundService(Intent service) {
warnIfCallingFromSystemProcess();
return startServiceCommon(service, true, mUser);
}
@Override
public ComponentName startForegroundServiceAsUser(Intent service, UserHandle user) {
return startServiceCommon(service, true, user);
}
好了,那麼上述的第4種方法可以很好解決該問題。
ps:注意上面的方法是啓動前臺服務,你的服務需要是前臺的,這個怎麼做呢,下面提供2種方法:
1) 在service中調用startForeground (最常見方法)
2) 設置service爲前臺,可以使用AMS的setProcessImportant設置優先級別 (優點是:不會在通知欄中出現通知圖標。缺點是:需要相應的權限)