應用在適配Android 8.0以上系統時,會發現后台啟動不了服務,會報出如下異常,並強退:
Fatal Exception: java.lang.IllegalStateException
Not allowed to start service Intent { act=com.xxx.xxx.xxx pkg=com.xxx.xxx (has extras) }: app is in background uid UidRecord{1cbd9ed u0a1967 CEM idle procs:1 seq(0,0,0)}
問題原因分析
Android 8.0 行為變更
https://developer.android.com/about/versions/oreo/android-8.0-changes.html#back-all
Android 8.0 除了提供諸多新特性和功能外,還對系統和 API 行為做出了各種變更。本文重點介紹您應該了解並在開發應用時加以考慮的一些主要變更。
其中大部分變更會影響所有應用,而不論應用針對的是何種版本的 Android。不過,有幾項變更僅影響針對 Android 8.0 的應用。為清楚起見,本頁面分為兩個部分:針對所有 API 級別的應用和針對 Android 8.0 的應用。
針對所有 API 級別的應用
這些行為變更適用於 在 Android 8.0 平台上運行的 所有應用,無論這些應用是針對哪個 API 級別構建。所有開發者都應查看這些變更,並修改其應用以正確支持這些變更(如果適用)。
后台執行限制Android 8.0 為提高電池續航時間而引入的變更之一是,當您的應用進入已緩存狀態時,如果沒有活動的組件,系統將解除應用具有的所有喚醒鎖。
此外,為提高設備性能,系統會限制未在前台運行的應用的某些行為。具體而言:
- 現在,在后台運行的應用對后台服務的訪問受到限制。
- 應用無法使用其清單注冊大部分隱式廣播(即,並非專門針對此應用的廣播)。
默認情況下,這些限制僅適用於針對 O 的應用。不過,用戶可以從 Settings 屏幕為任意應用啟用這些限制,即使應用並不是以 O 為目標平台。
Android 8.0 還對特定函數做出了以下變更:
- 如果針對 Android 8.0 的應用嘗試在不允許其創建后台服務的情況下使用
startService()
函數,則該函數將引發一個IllegalStateException
。 - 新的
Context.startForegroundService()
函數將啟動一個前台服務。現在,即使應用在后台運行,系統也允許其調用Context.startForegroundService()
。不過,應用必須在創建服務后的五秒內調用該服務的startForeground()
函數。
如需了解詳細信息,請參閱后台執行限制。
出錯代碼定位
在ContextImpl的startServiceCommon函數中爆出異常,
http://androidxref.com/8.1.0_r33/xref/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) { if (cn.getPackageName().equals("!")) { throw new SecurityException( "Not allowed to start service " + service + " without permission " + cn.getClassName()); } else if (cn.getPackageName().equals("!!")) { throw new SecurityException( "Unable to start service " + service + ": " + cn.getClassName()); //此處就是曝出異常的地方,非法狀態,不允許啟動服務 } 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,用於啟動服務
AMS的startService
接下去看AMS的startService,稍微注意一下傳遞的參數,里面有一個前台后台相關的requireForeground,可能跟問題有關系。
AMS代碼位置
@Override public ComponentName startService(IApplicationThread caller, Intent service, String resolvedType, boolean requireForeground, String callingPackage, int userId) throws TransactionTooLargeException { enforceNotIsolatedCaller("startService"); // Refuse possible leaked file descriptors if (service != null && service.hasFileDescriptors() == true) { throw new IllegalArgumentException("File descriptors passed in Intent"); } if (callingPackage == null) { throw new IllegalArgumentException("callingPackage cannot be null"); } if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "*** startService: " + service + " type=" + resolvedType + " fg=" + requireForeground); synchronized(this) { final int callingPid = Binder.getCallingPid(); final int callingUid = Binder.getCallingUid(); final long origId = Binder.clearCallingIdentity(); ComponentName res; try { //調用ActiveServices的startServiceLocked res = mServices.startServiceLocked(caller, service, resolvedType, callingPid, callingUid, requireForeground, callingPackage, userId); } finally { Binder.restoreCallingIdentity(origId); } return res; } }
會調用ActiveServices的startServiceLocked
ActiveServices的startServiceLocked
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, 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" // 找到了原因就在這里 return new ComponentName("?", "app is in background uid " + uidRec); } } //... }
這里面由於出現錯誤,那么startRequested==false而且fgRequired==false,說明這個服務是第一次啟動,而且是后台請求啟動服務。
至於為什么不允許啟動服務,我們還需要查看AMS的getAppStartModeLocked函數。
AMS判斷並返回服務啟動模式
int getAppStartModeLocked(int uid, String packageName, int packageTargetSdk,
int callingPid, boolean alwaysRestrict, boolean disabledOnly) {
UidRecord uidRec = mActiveUids.get(uid);
if (DEBUG_BACKGROUND_CHECK) Slog.d(TAG, "checkAllowBackground: uid=" + uid + " pkg="
+ packageName + " rec=" + uidRec + " always=" + alwaysRestrict + " idle="
+ (uidRec != null ? uidRec.idle : false));
if (uidRec == null || alwaysRestrict || uidRec.idle) {
boolean ephemeral;
if (uidRec == null) {
ephemeral = getPackageManagerInternalLocked().isPackageEphemeral(
UserHandle.getUserId(uid), packageName);
} else {
ephemeral = uidRec.ephemeral;
}
if (ephemeral) {
// We are hard-core about ephemeral apps not running in the background.
return ActivityManager.APP_START_MODE_DISABLED;
} else {
if (disabledOnly) {
// The caller is only interested in whether app starts are completely
// disabled for the given package (that is, it is an instant app). So
// we don't need to go further, which is all just seeing if we should
// apply a "delayed" mode for a regular app.
return ActivityManager.APP_START_MODE_NORMAL;
}
// 此處alwaysRestrict==false,於是調用的是appServicesRestrictedInBackgroundLocked
final int startMode = (alwaysRestrict)
? appRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk)
: appServicesRestrictedInBackgroundLocked(uid, packageName,
packageTargetSdk);
if (DEBUG_BACKGROUND_CHECK) Slog.d(TAG, "checkAllowBackground: uid=" + uid
+ " pkg=" + packageName + " startMode=" + startMode
+ " onwhitelist=" + isOnDeviceIdleWhitelistLocked(uid));
if (startMode == ActivityManager.APP_START_MODE_DELAYED) {
// This is an old app that has been forced into a "compatible as possible"
// mode of background check. To increase compatibility, we will allow other
// foreground apps to cause its services to start.
if (callingPid >= 0) {
ProcessRecord proc;
synchronized (mPidsSelfLocked) {
proc = mPidsSelfLocked.get(callingPid);
}
if (proc != null &&
!ActivityManager.isProcStateBackground(proc.curProcState)) {
// Whoever is instigating this is in the foreground, so we will allow it
// to go through.
return ActivityManager.APP_START_MODE_NORMAL;
}
}
}
return startMode;
}
}
return ActivityManager.APP_START_MODE_NORMAL;
}
根據alwaysRestrict的值會調用appRestrictedInBackgroundLocked或者appServicesRestrictedInBackgroundLocked;
其中appRestrictedInBackgroundLocked是直接根據應用sdk進行判斷,
appServicesRestrictedInBackgroundLocked會進行條件過濾,直接運行部分應用啟動服務,其它的進行應用sdk的判斷。
接下來看appServicesRestrictedInBackgroundLocked進行條件過濾,允許部分啟動服務
int appServicesRestrictedInBackgroundLocked(int uid, String packageName, int packageTargetSdk) { // Persistent app? // 如果是常駐內存的,可以直接啟動服務 if (mPackageManagerInt.isPackagePersistent(packageName)) { if (DEBUG_BACKGROUND_CHECK) { Slog.i(TAG, "App " + uid + "/" + packageName + " is persistent; not restricted in background"); } return ActivityManager.APP_START_MODE_NORMAL; } // Non-persistent but background whitelisted? // 如果是非常駐內存的話,但是在白名單列表里面的uid也是允許的 // 目前這個白名單里面就只有一個:藍牙BLUETOOTH_UID = 1002 if (uidOnBackgroundWhitelist(uid)) { if (DEBUG_BACKGROUND_CHECK) { Slog.i(TAG, "App " + uid + "/" + packageName + " on background whitelist; not restricted in background"); } return ActivityManager.APP_START_MODE_NORMAL; } // Is this app on the battery whitelist? // 如果是在電源相關的白名單里面,也是允許啟動服務的 if (isOnDeviceIdleWhitelistLocked(uid)) { if (DEBUG_BACKGROUND_CHECK) { Slog.i(TAG, "App " + uid + "/" + packageName + " on idle whitelist; not restricted in background"); } return ActivityManager.APP_START_MODE_NORMAL; } // None of the service-policy criteria apply, so we apply the common criteria return appRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk); }
分別對於:
1. 是否常駐內存應用,常駐內存允許啟動服務
2.如果是藍牙也是允許啟動服務
3.是在電源相關的DeviceIdle白名單里面,允許啟動服務的
4.如果都不是則執行默認策略appRestrictedInBackgroundLocked
再看appServicesRestrictedInBackgroundLocked進行條件過濾,允許部分啟動服務
int appRestrictedInBackgroundLocked(int uid, String packageName, int packageTargetSdk) { // Apps that target O+ are always subject to background check // 如果apk的sdk版本大於AndroidO的話,那么默認是不允許啟動服務的 if (packageTargetSdk >= Build.VERSION_CODES.O) { if (DEBUG_BACKGROUND_CHECK) { Slog.i(TAG, "App " + uid + "/" + packageName + " targets O+, restricted"); } return ActivityManager.APP_START_MODE_DELAYED_RIGID; } // ...and legacy apps get an AppOp check // 如果是之前版本的apk,會查看AppOps是否允許后台運行權限, // 由於我們sdk版本肯定會升級的,這個就暫時不考慮了 int appop = mAppOpsService.noteOperation(AppOpsManager.OP_RUN_IN_BACKGROUND, uid, packageName); if (DEBUG_BACKGROUND_CHECK) { Slog.i(TAG, "Legacy app " + uid + "/" + packageName + " bg appop " + appop); } 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以后的版本,此處是繞不過去的坎,建議盡早處理。
網上所傳的notification隱藏是否可以?
有的需求是啟動服務,但是又不想有通知,這和Google定的規則有沖突呀,有沒有什么辦法呢?網上2年前的方案是啟動兩個Service,一個Service干活,另外一個Service把通知隱藏掉。
Android O Google應該考慮到這個漏洞了:
private void cancelForegroundNotificationLocked(ServiceRecord r) { if (r.foregroundId != 0) { // First check to see if this app has any other active foreground services // with the same notification ID. If so, we shouldn't actually cancel it, // because that would wipe away the notification that still needs to be shown // due the other service. ServiceMap sm = getServiceMapLocked(r.userId); if (sm != null) { for (int i = sm.mServicesByName.size()-1; i >= 0; i--) { ServiceRecord other = sm.mServicesByName.valueAt(i); if (other != r && other.foregroundId == r.foregroundId && other.packageName.equals(r.packageName)) { // Found one! Abort the cancel. return; } } } r.cancelNotification(); } }
如果前台服務的通知還有被占用,那就別想用其他服務把它干掉了
框架規避方案
修改方案(僅供參考):ActiveServices.java如下加一個packageName的crash的規避,anr同理,發出消息的地方可以修改為不發出timeout消息,也可以在startForeground的時候就移除。(如果Service耗時小於5s,Service在stop流程的時候會將anr消息移除,可不修改)
// Check to see if the service had been started as foreground, but being // brought down before actually showing a notification. That is not allowed. if (r.fgRequired) { Slog.w(TAG_SERVICE, "Bringing down service while still waiting for start foreground: " + r); r.fgRequired = false; r.fgWaiting = false; mAm.mHandler.removeMessages( ActivityManagerService.SERVICE_FOREGROUND_TIMEOUT_MSG, r); if (r.app != null && !"packageName".equals(r.packageName)) { Message msg = mAm.mHandler.obtainMessage( ActivityManagerService.SERVICE_FOREGROUND_CRASH_MSG); msg.obj = r.app; mAm.mHandler.sendMessage(msg); } }
修改原理:
編譯一個service.jar,打印報錯堆棧
01-01 07:02:17.669 918 1334 W ActivityManager: Bringing down service while still waiting for start foreground: ServiceRecord{2d44a2d u0 packageName/.servicename} 01-01 07:02:17.669 918 1334 W ActivityManager: java.lang.Throwable 01-01 07:02:17.669 918 1334 W ActivityManager: at com.android.server.am.ActiveServices.bringDownServiceLocked(ActiveServices.java:2612) 01-01 07:02:17.669 918 1334 W ActivityManager: at com.android.server.am.ActiveServices.bringDownServiceIfNeededLocked(ActiveServices.java:2559) 01-01 07:02:17.669 918 1334 W ActivityManager: at com.android.server.am.ActiveServices.stopServiceTokenLocked(ActiveServices.java:792) 01-01 07:02:17.669 918 1334 W ActivityManager: at com.android.server.am.ActivityManagerService.stopServiceToken(ActivityManagerService.java:18789) 01-01 07:02:17.669 918 1334 W ActivityManager: at android.app.IActivityManager$Stub.onTransact(IActivityManager.java:759) 01-01 07:02:17.669 918 1334 W ActivityManager: at com.android.server.am.ActivityManagerService.onTransact(ActivityManagerService.java:3080) 01-01 07:02:17.669 918 1334 W ActivityManager: at android.os.Binder.execTransact(Binder.java:697)
找到對應拋出Context.startForegroundService() did not then call Service.startForeground()的邏輯代碼:
ActiveServices.java bringDownServiceLocked
// Check to see if the service had been started as foreground, but being // brought down before actually showing a notification. That is not allowed. if (r.fgRequired) { Slog.w(TAG_SERVICE, "Bringing down service while still waiting for start foreground: " + r); r.fgRequired = false; r.fgWaiting = false; mAm.mHandler.removeMessages( ActivityManagerService.SERVICE_FOREGROUND_TIMEOUT_MSG, r); if (r.app != null) { Message msg = mAm.mHandler.obtainMessage( ActivityManagerService.SERVICE_FOREGROUND_CRASH_MSG); msg.obj = r.app; mAm.mHandler.sendMessage(msg); } }
走到這里面繼而會由ams發出一個service_foreground_crash_msg的消息,導致crash。
至於為嘛會走到這里呢,都是id = 0 的過,既沒有走前台服務的流程也沒有將r.fgRequired設為false,anr的msg也沒有移除掉。
private void setServiceForegroundInnerLocked(ServiceRecord r, int id, Notification notification, int flags) { if (id != 0) { if (notification == null) { throw new IllegalArgumentException("null notification"); } // Instant apps need permission to create foreground services. ... if (r.fgRequired) { if (DEBUG_SERVICE || DEBUG_BACKGROUND_CHECK) { Slog.i(TAG, "Service called startForeground() as required: " + r); } r.fgRequired = false; r.fgWaiting = false; mAm.mHandler.removeMessages( ActivityManagerService.SERVICE_FOREGROUND_TIMEOUT_MSG, r); } if (r.foregroundId != id) { cancelForegroundNotificationLocked(r); r.foregroundId = id; } notification.flags |= Notification.FLAG_FOREGROUND_SERVICE; r.foregroundNoti = notification; if (!r.isForeground) { final ServiceMap smap = getServiceMapLocked(r.userId); if (smap != null) { ActiveForegroundApp active = smap.mActiveForegroundApps.get(r.packageName); if (active == null) { active = new ActiveForegroundApp(); active.mPackageName = r.packageName; active.mUid = r.appInfo.uid; active.mShownWhileScreenOn = mScreenOn; if (r.app != null) { active.mAppOnTop = active.mShownWhileTop = r.app.uidRecord.curProcState <= ActivityManager.PROCESS_STATE_TOP; } active.mStartTime = active.mStartVisibleTime = SystemClock.elapsedRealtime(); smap.mActiveForegroundApps.put(r.packageName, active); requestUpdateActiveForegroundAppsLocked(smap, 0); } active.mNumActive++; } r.isForeground = true; } r.postNotification(); if (r.app != null) { updateServiceForegroundLocked(r.app, true); } getServiceMapLocked(r.userId).ensureNotStartingBackgroundLocked(r); mAm.notifyPackageUse(r.serviceInfo.packageName, PackageManager.NOTIFY_PACKAGE_USE_FOREGROUND_SERVICE); } else { if (r.isForeground) { final ServiceMap smap = getServiceMapLocked(r.userId); if (smap != null) { decActiveForegroundAppLocked(smap, r); } r.isForeground = false; if (r.app != null) { mAm.updateLruProcessLocked(r.app, false, null); updateServiceForegroundLocked(r.app, true); } } if ((flags & Service.STOP_FOREGROUND_REMOVE) != 0) { cancelForegroundNotificationLocked(r); r.foregroundId = 0; r.foregroundNoti = null; } else if (r.appInfo.targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP) { r.stripForegroundServiceFlagFromNotification(); if ((flags & Service.STOP_FOREGROUND_DETACH) != 0) { r.foregroundId = 0; r.foregroundNoti = null; } } } }
anr的時限為嘛是5s呢?
void scheduleServiceForegroundTransitionTimeoutLocked(ServiceRecord r) { if (r.app.executingServices.size() == 0 || r.app.thread == null) { return; } Message msg = mAm.mHandler.obtainMessage( ActivityManagerService.SERVICE_FOREGROUND_TIMEOUT_MSG); msg.obj = r; r.fgWaiting = true; mAm.mHandler.sendMessageDelayed(msg, SERVICE_START_FOREGROUND_TIMEOUT); } // How long the startForegroundService() grace period is to get around to // calling startForeground() before we ANR + stop it. static final int SERVICE_START_FOREGROUND_TIMEOUT = 5*1000;
這種timeout流程就很熟悉了。
修改方案
有上面可知,問題原因主要是后台啟動了服務,在這部分Android O/GO做了限制,
根據章節2.4的過濾條件可以提供如下修改方案:
1) 提升應用優先級到常駐內存級別 (不建議應用采納這種方式,會導致手機出現很多性能問題)
=> 在AndroidManifest.xml添加android:persistent=”true”
並且簽上系統簽名
2) 類似與藍牙BLUETOOTH_UID一樣放在白名單里面(需要擁有源碼修改權限,而且修改了源碼,不利於apk的版本兼容,不建議采納)
3) 添加在電源相關的DeviceIdle白名單(不建議添加,可能導致功耗增加)
按照上面的都說是不建議采取,是否沒有辦法了呢?
我們繼續往源頭找找看看是否有辦法:
在ContextImpl的startServiceCommon、ActiveServices的startServiceLocked有一個參數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); }
ps:注意上面的方法是啟動前台服務,你的服務需要是前台的,這個怎么做呢,下面提供2種方法:
1) 在service中調用startForeground (最常見方法)
2) 設置service為前台,可以使用AMS的setProcessImportant設置優先級別 (優點是:不會在通知欄中出現通知圖標。缺點是:需要相應的權限)