Android O 通知欄的"running in the background"

Android O新增的一個特性,系統會在通知欄顯示當前在後臺運行的應用,其實際是顯示啓動了前臺服務的應用,而且當前應用的Activity不在前臺。具體咱們看下源碼是怎麼實現的。html

1 APP調用startServicestartForegroundService啓動一個service.

startServicestartForegroundService在Android O上主要有兩個區別:
一個是後臺應用沒法經過startService啓動一個服務,而不管前臺應用仍是後臺應用,均可以經過startForegroundService啓動一個服務。
此外Android規定,在調用startForegroundService啓動一個服務後,須要在服務被啓動後5秒內調用startForeground方法,
不然會結束掉該service而且拋出一個ANR異常。關於前臺應用和後臺應用的規範見官網android

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方法,該方法會調用ActiveServicessetServiceForegroundLocked方法。
ActiveServices是用來輔助AMS管理應用service的一個類。app

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方法,見下面代碼。
這裏有個關鍵代碼是ide

active.mAppOnTop = active.mShownWhileTop =
                                    r.app.uidRecord.curProcState <= ActivityManager.PROCESS_STATE_TOP;

下面會再次提到這段代碼。post

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列表中。ui

  • 根據active列表,更新notification。this

可見,只有在smap.mActiveForegroundApps列表中,而且mAppOnTop爲false的前臺服務纔會顯示在通知欄中的「running in the background」中。rest

以上是一個應用啓動一個前臺服務到被顯示在通知欄中的「running in the background」中的代碼上的流程。此外咱們再瞭解些相關的邏輯。code

mAppOnTop的狀態

從上面的分析看出,mAppOnTop的值決定了一個前臺服務是否會被顯示在通知欄的「running in the background」中。mAppOnTop的狀態除了會在建立的時候賦值,還會在另外一個方法中被更新。
每次更新後,若是值有變化,就會調用requestUpdateActiveForegroundAppsLocked,該方法上面分析過了。htm

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中調用startForegroundmAppOnTop的狀態變動)會觸發該通知的更新外,還有一些其它狀況會觸發更新。
從上面代碼的分析中,咱們知道,觸發更新的地方必需要調用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提醒用戶。
相關文章
相關標籤/搜索