前言:
在官方文檔 Android 8.0 行為變更 中有這樣一段話:
Android 8.0 有一項復雜功能;系統不允許后台應用創建后台服務。 因此,Android 8.0 引入了一種全新的方法,即 Context.startForegroundService()
,以在前台啟動新服務。
在系統創建服務后,應用有五秒的時間來調用該服務的 startForeground()
方法以顯示新服務的用戶可見通知。
如果應用在此時間限制內未調用 startForeground()
,則系統將停止服務並聲明此應用為 ANR。
Android service 啟動篇之 startService 中對整個start 過程進行了梳理,其中startService 和startForegroundService 最終調用調用的接口時一樣的,只是其中要求foreground 啟動service。基於上一篇博文,這里對於前台服務進行詳細的解析。
1 startServiceLocked
流程同Android service 啟動篇之 startService ,最終調用接口為ActivieServices 中startServiceLocked:
r.lastActivity = SystemClock.uptimeMillis();
r.startRequested = true;
r.delayedStop = false;
r.fgRequired = fgRequired;
r.pendingStarts.add(new ServiceRecord.StartItem(r, false, r.makeNextStartId(),
service, neededGrants, callingUid));
初始化ServiceRecord,其中fgRequired 為true。
然后將需要start 的service 添加到pendingStarts 中,Android service 啟動篇之 startService 中知道最后會在bringUpServiceLocked的函數中進行最終啟動。
對於前台服務 sendServiceArgsLocked() 函數中會拉起一個timeout,時長為 5 秒,也就是說5s 后會拋出ANR的異常。
詳細看下面第 4 點。
從這里我們知道在Context.startForegroundService() 之后必須要調用Service.startForeground,也就是說在foreground 的啟動接口調用后的 5 秒內必須要在service 中調用startForeground() 接口來解除timeout。
2 startFroeground
來看下是否是這樣設計的,來看下startFroeground():
public final void startForeground(int id, Notification notification) {
try {
mActivityManager.setServiceForeground(
new ComponentName(this, mClassName), mToken, id,
notification, 0);
} catch (RemoteException ex) {
}
}
在函數的上面有段注釋:
* @param id The identifier for this notification as per
* {@link NotificationManager#notify(int, Notification)
* NotificationManager.notify(int, Notification)}; must not be 0.
* @param notification The Notification to be displayed.
*
* @see #stopForeground(boolean)
*/
一共 5 個參數,其中id 和notification 是需要通過service 傳入的。id 是用於notification notify 使用。
3 setServiceForegroundInnerLocked
3.1 取消timeout
接着來看AMS 中的接口,最終調用的是ActiveServices 中的setServiceForegroundInnerLocked():
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);
}
fgRequired 在這里會被置成false,意味了這個請求已經被安全處理。
這里看到會取消掉foreground 的timeout,但是,前提條件是:
if (id != 0) {
if (notification == null) {
throw new IllegalArgumentException("null notification");
}
要求startFroeground() 中的id 不能為0,而且notification不能為null。
注意:
上面提到sendServiceArgsLocked() 的時候會schedule 一個timeout,時長為5秒,5秒過了之后會出現ANR。那需要注意的是函數sendServiceArgsLocked() 是在onCreate() 之后,並且是在onStartCommand() 之前調用的,這就給了我們取消的空間。雖然說都是異步的操作,但是為了正常流程考慮,一般會將startFroeground() 加到onStartCommand() 中執行。
3.2 隱藏notification
if (r.foregroundId != id) {
cancelForegroundNotificationLocked(r);
r.foregroundId = id;
}
code 中在foreground 的id 發生變化的時候,會將原來的notification 隱藏掉。
那有種可能,有可能兩個service 公用一個notification,這個時候不需要將notification cancel。
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();
}
}
3.3 將service 設為前台服務
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;
}
4 異常處理
4.1 ANR
上面已經說過,如果在 5 秒內沒有調用startForeground(),timeout 就會觸發,會報出ANR:
void serviceForegroundTimeout(ServiceRecord r) {
ProcessRecord app;
synchronized (mAm) {
if (!r.fgRequired || r.destroying) {
return;
}
if (DEBUG_BACKGROUND_CHECK) {
Slog.i(TAG, "Service foreground-required timeout for " + r);
}
app = r.app;
r.fgWaiting = false;
stopServiceLocked(r);
}
if (app != null) {
mAm.mAppErrors.appNotResponding(app, null, null, false,
"Context.startForegroundService() did not then call Service.startForeground()");
}
}
log 如下:
11-06 02:01:59.616 3877 3893 E ActivityManager: ANR in com.shift.phonemanager.permission.accesslog
11-06 02:01:59.616 3877 3893 E ActivityManager: PID: 1369
11-06 02:01:59.616 3877 3893 E ActivityManager: Reason: Context.startForegroundService() did not then call Service.startForeground()
11-06 02:01:59.616 3877 3893 E ActivityManager: Load: 0.0 / 0.0 / 0.0
11-06 02:01:59.616 3877 3893 E ActivityManager: CPU usage from 7945ms to 0ms ago (2007-11-06 02:01:51.418 to 2007-11-06 02:01:59.363):
11-06 02:01:59.616 3877 3893 E ActivityManager: 60% 3877/system_server: 35% user + 25% kernel / faults: 3744 minor 6 major
11-06 02:01:59.616 3877 3893 E ActivityManager: 25% 1042/com.android.launcher3: 20% user + 4.9% kernel / faults: 11190 minor 9 major
11-06 02:01:59.616 3877 3893 E ActivityManager: 18% 1149/android.process.media: 13% user + 5.3% kernel / faults: 6130 minor
11-06 02:01:59.616 3877 3893 E ActivityManager: 15% 1420/adbd: 3.6% user + 11% kernel / faults: 5074 minor
11-06 02:01:59.616 3877 3893 E ActivityManager: 9.7% 255/logd: 2.7% user + 6.9% kernel / faults: 5 minor
11-06 02:01:59.616 3877 3893 E ActivityManager: 9.2% 3814/surfaceflinger: 4.4% user + 4.8% kernel / faults: 658 minor
4.2 crash
上面看到如果timeout 觸發,會報出ANR,但是code 中也有另外一個地方限制,要求service 一旦startForegroundService() 啟動,必須要在service 中startForeground(),如果在這之前stop 或stopSelf,那就會用crash 來代替ANR。
詳細看bringDownServiceLocked()。
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);
}
}
這里的r.fgRequired 必須要處理掉,不然stop 的時候會觸發bringDown,然后會將timeout 的remove,換成了crash。
log 如下:
--------- beginning of crash
11-06 02:06:05.307 3106 3106 E AndroidRuntime: FATAL EXCEPTION: main
11-06 02:06:05.307 3106 3106 E AndroidRuntime: Process: com.shift.phonemanager.permission.accesslog, PID: 3106
11-06 02:06:05.307 3106 3106 E AndroidRuntime: android.app.RemoteServiceException: Context.startForegroundService() did not then call Service.startForeground()
11-06 02:06:05.307 3106 3106 E AndroidRuntime: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1771)
11-06 02:06:05.307 3106 3106 E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:106)
11-06 02:06:05.307 3106 3106 E AndroidRuntime: at android.os.Looper.loop(Looper.java:164)
11-06 02:06:05.307 3106 3106 E AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:6518)
11-06 02:06:05.307 3106 3106 E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method)
11-06 02:06:05.307 3106 3106 E AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
11-06 02:06:05.307 3106 3106 E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
11-06 02:06:05.320 3118 3118 D ExtensionsFactory: No custom extensions.
5 總結
- 8.0 以后不希望后台應用運行后台服務,除非特殊條件
- 一旦通過startForegroundService() 啟動前台服務,必須在service 中有startForeground() 配套,不然會出現ANR 或者crash
- startForeground() 中的id 和notification 不能為0 和 null