Android O新增的一個特性,系統會在通知欄顯示當前在后台運行的應用,其實際是顯示啟動了前台服務的應用,並且當前應用的Activity不在前台。具體我們看下源碼是怎么實現的。
1 APP調用startService或startForegroundService啟動一個service.
startService和startForegroundService在Android O上主要有兩個區別:
一個是后台應用無法通過startService啟動一個服務,而無論前台應用還是后台應用,都可以通過startForegroundService啟動一個服務。
此外Android規定,在調用startForegroundService啟動一個服務后,需要在服務被啟動后5秒內調用startForeground方法,
否則會結束掉該service並且拋出一個ANR異常。關於前台應用和后台應用的規范見官網
2 Service被啟動后,需要調用startForeground方法,將service置為前台服務。其中有一個地方要注意,第一個參數id不能等於0。
public final void startForeground(int id, Notification notification) {
try {
mActivityManager.setServiceForeground(
new ComponentName(this, mClassName), mToken, id,
notification, 0);
} catch (RemoteException ex) {
}
}
接着調用了AMS的setServiceForeground方法,該方法會調用ActiveServices的setServiceForegroundLocked方法。
ActiveServices是用來輔助AMS管理應用service的一個類。
public void setServiceForegroundLocked(ComponentName className, IBinder token,
int id, Notification notification, int flags) {
final int userId = UserHandle.getCallingUserId();
final long origId = Binder.clearCallingIdentity();
try {
ServiceRecord r = findServiceLocked(className, token, userId);
if (r != null) {
setServiceForegroundInnerLocked(r, id, notification, flags);
}
} finally {
Binder.restoreCallingIdentity(origId);
}
}
3 調用了setServiceForegroundInnerLocked方法
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.
// ...A lot of code is omitted
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);
}
}
該方法主要做的事情,創建一個ActiveForegroundApp實例,並把實例加入到smap.mActiveForegroundApps,
調用requestUpdateActiveForegroundAppsLocked,設置ServiceRecord的isForeground = true.
由此可見,所有的前台服務都會在smap.mActiveForegroundApps列表中對應一個實例。
requestUpdateActiveForegroundAppsLocked方法又調用了updateForegroundApps方法,見下面代碼。
這里有個關鍵代碼是
active.mAppOnTop = active.mShownWhileTop =
r.app.uidRecord.curProcState <= ActivityManager.PROCESS_STATE_TOP;
下面會再次提到這段代碼。
4 updateForegroundApps方法。通知欄上面的“running in the background”就是在這個方法里面去更新的。
void updateForegroundApps(ServiceMap smap) {
// This is called from the handler without the lock held.
ArrayList<ActiveForegroundApp> active = null;
synchronized (mAm) {
final long now = SystemClock.elapsedRealtime();
long nextUpdateTime = Long.MAX_VALUE;
if (smap != null) {
for (int i = smap.mActiveForegroundApps.size()-1; i >= 0; i--) {
ActiveForegroundApp aa = smap.mActiveForegroundApps.valueAt(i);
// ...A lot of code is omitted
if (!aa.mAppOnTop) {
if (active == null) {
active = new ArrayList<>();
}
active.add(aa);
}
}
}
}
final NotificationManager nm = (NotificationManager) mAm.mContext.getSystemService(
Context.NOTIFICATION_SERVICE);
final Context context = mAm.mContext;
if (active != null) {
// ...A lot of code is omitted
//這里是更新通知的地方,具體代碼太長,省略掉了。
} else {
//如果active為空,取消掉通知。
nm.cancelAsUser(null, SystemMessageProto.SystemMessage.NOTE_FOREGROUND_SERVICES,
new UserHandle(smap.mUserId));
}
}
-
遍歷
smap.mActiveForegroundApps列表,判斷列表中的元素,如果其mAppOnTop成員屬性為false,則加入active列表中。 -
根據active列表,更新notification。
可見,只有在smap.mActiveForegroundApps列表中,並且mAppOnTop為false的前台服務才會顯示在通知欄中的“running in the background”中。
以上是一個應用啟動一個前台服務到被顯示在通知欄中的“running in the background”中的代碼上的流程。此外我們再了解些相關的邏輯。
mAppOnTop的狀態
從上面的分析看出,mAppOnTop的值決定了一個前台服務是否會被顯示在通知欄的“running in the background”中。mAppOnTop的狀態除了會在創建的時候賦值,還會在另一個方法中被更新。
每次更新后,如果值有變化,就會調用requestUpdateActiveForegroundAppsLocked,該方法上面分析過了。
void foregroundServiceProcStateChangedLocked(UidRecord uidRec) {
ServiceMap smap = mServiceMap.get(UserHandle.getUserId(uidRec.uid));
if (smap != null) {
boolean changed = false;
for (int j = smap.mActiveForegroundApps.size()-1; j >= 0; j--) {
ActiveForegroundApp active = smap.mActiveForegroundApps.valueAt(j);
if (active.mUid == uidRec.uid) {
if (uidRec.curProcState <= ActivityManager.PROCESS_STATE_TOP) {
if (!active.mAppOnTop) {
active.mAppOnTop = true;
changed = true;
}
active.mShownWhileTop = true;
} else if (active.mAppOnTop) {
active.mAppOnTop = false;
changed = true;
}
}
}
if (changed) {
requestUpdateActiveForegroundAppsLocked(smap, 0);
}
}
}
該方法傳入一個uidrecord參數,指定具體的uid及相關狀態。判斷邏輯跟前面setServiceForegroundInnerLocked方法的邏輯一致。
其中ActivityManager.PROCESS_STATE_TOP的官方解釋是
Process is hosting the current top activities. Note that this covers all activities that are visible to the user.
意思就是,當前進程的一個activity在棧頂,覆蓋了所有其它activity,用戶可以真正看到的。
換句話,如果用戶不能直接看到該應用的activity,並且該應用啟動了一個前台服務,那么就會被顯示在“running in the background”中。
foregroundServiceProcStateChangedLocked方法只有一處調用,AMS的updateOomAdjLocked,該方法的調用地方太多,無法一一分析。
"running in the background"通知的更新。
除了以上兩種情況(應用在service中調用startForeground和mAppOnTop的狀態變更)會觸發該通知的更新外,還有一些其它情況會觸發更新。
從上面代碼的分析中,我們知道,觸發更新的地方必須要調用requestUpdateActiveForegroundAppsLocked方法。
該方法會在ActiveSercices類中的如下幾個方法中調用。
setServiceForegroundInnerLocked (這個我們前面分析過)
decActiveForegroundAppLocked (減小前台應用)
updateScreenStateLocked (屏幕狀態變化)
foregroundServiceProcStateChangedLocked (進程狀態變化)
forceStopPackageLocked (強制停止應用)
我們看下其中的decActiveForegroundAppLocked方法
decActiveForegroundAppLocked
private void decActiveForegroundAppLocked(ServiceMap smap, ServiceRecord r) {
ActiveForegroundApp active = smap.mActiveForegroundApps.get(r.packageName);
if (active != null) {
active.mNumActive--;
if (active.mNumActive <= 0) {
active.mEndTime = SystemClock.elapsedRealtime();
if (foregroundAppShownEnoughLocked(active, active.mEndTime)) {
// Have been active for long enough that we will remove it immediately.
smap.mActiveForegroundApps.remove(r.packageName);
smap.mActiveForegroundAppsChanged = true;
requestUpdateActiveForegroundAppsLocked(smap, 0);
} else if (active.mHideTime < Long.MAX_VALUE){
requestUpdateActiveForegroundAppsLocked(smap, active.mHideTime);
}
}
}
}
該方法主要是移除前台service,根據foregroundAppShownEnoughLocked判斷,是否馬上移除還是過一段時間移除。
該方法主要在兩個地方調用。一個是在setServiceForegroundInnerLocked中調用,當應用調用startForeground,第一個參數設為0時,會走到這個路徑。
另一個bringDownServiceLocked,也就是當綁定到該service的數量減小時,會調用該方法。
前台服務是否一定會在通知欄顯示應用自己的通知
如果是一定的話,我想系統也沒必要再額外顯示一條“running in the background”的通知,列出所有后台運行的應用了。
所以答案是不一定,雖然在調用startForeground方法時,必須要傳一個notification作為參數,但依然會有兩種情況會導致不會在通知欄顯示應用發的通知。
- 用戶主動屏蔽應用通知,可以通過長按通知,點擊“ALL CATEGORIES”進入通知管理,關閉通知。關閉后前台服務依然生效。
- NotificationManager在顯示應用通知的時候,因為某些原因顯示失敗,失敗原因可能是應用創建了不規范的通知,比如Android O新增了NotificationChannel,應用在創建通知的時候,必須指定一個NotificationChannel,但是如果應用創建通知的時候,指定的NotificationChannel是無效的,或者直接傳null作為參數值,那么在NotificationManager就沒辦法顯示該通知。這種情況下,前台服務還是會生效,但是卻不會在通知欄顯示應用的通知,不過NotificationManager發現不規范的通知時,一般會彈出一個toast提醒用戶。
