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

来源:http://www.cnblogs.com/xiaji5572/archive/2017/10/25/7729987.html
-Advertisement-
Play Games

Android O新增的一個特性,系統會在通知欄顯示當前在後臺運行的應用,其實際是顯示啟動了前臺服務的應用,並且當前應用的Activity不在前臺。具體我們看下源碼是怎麼實現的。 1 APP調用 或`startForegroundService`啟動一個service. 和`startForegro ...


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

1 APP調用startServicestartForegroundService啟動一個service.

startServicestartForegroundService在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方法,該方法會調用ActiveServicessetServiceForegroundLocked方法。
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中調用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的數量減小時,會調用該方法。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ...
  • 通過<input />標簽,給它指定type類型為file,可提供文件上傳; accept:可選擇上傳類型,如:只要傳圖片,且不限製圖片格式,為image/*; multiple:規定是否可以選擇多個文件; 規定只可上傳圖片,且可以選擇多個文件 當然,直接一個input type=file 只能選擇 ...
  • 關於在vue cli搭建的項目中怎麼配置sass,網上搜到的基本是這種答案: 但是我認為,直接將樣式寫在每個單文件的<style 里,是十分不明智的做法。且不說node sass安裝過程的各種坑,內嵌的<style 也讓組件顯得十分混亂。想象一下你在修改某個methods時必須拖動滾輪穿 ...
  • 在這之前是給路由加一個meta屬性: 註意:但是事實是登錄的時候大多數時候並不進行跳轉,所以這裡需要在login跳轉的路徑中再加一段: ...
  • 有時候特別需要,個別網頁要去掉橫向滾動條和豎向滾動條,那該怎麼去掉呢,很簡單,看代碼: 讓豎條沒有: <body style=`overflow:-Scroll;overflow-y:hidden` > </body> 讓橫條沒有: <body style=`overflow:-Scroll;ove ...
  • 前言 vue這個框架現在挺流行的,作為一個專註前端100年的代碼愛好者,學習下目前流行的框架是必須的!在網上搜索vue的項目是比較少的,在官網進行了入門學習後,沒有一個項目練習鞏固下,學了就等於沒學,所以我就決定自己寫一個項目咯。在這裡我也順便分享下我學習vue的資源。我在GitHub上發現了一個v ...
  • 一、簡述 最近跟小伙伴一起討論了一下,決定一起仿一個BiliBili的app(包括android端和iOS端),我們並沒有打算把這個項目完全做完,畢竟我們的重點是掌握一些新框架的使用,併在實戰過程中發現並彌補自身的不足。 本系列將記錄我(android端)在開發過程中的一些我覺得有必要記錄的功能實現 ...
  • 前言 在面試過程中你也許會被問到消息轉發機制。這篇文章就是對消息的轉發機制進行一個梳理。主要包括什麼是消息、靜態綁定/動態綁定、消息的傳遞和消息的轉發。接下來開發進入正題。 消息的解釋 在其他語言裡面,我們可以用一個類去調用某個方法,在OC裡面,這個方法就是消息。某個類調用一個方法就是向這個類發送一 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...