Android程式保活-自“裁”或者耍流氓

看書的小蝸牛發表於2017-03-20

本篇文章是後臺殺死系列的最後一篇,主要探討一下程式的保活,Android本身設計的時候是非常善良的,它希望程式在不可見或者其他一些場景下APP要懂得主動釋放,可是Android低估了”貪婪“,尤其是很多國產APP,只希望索取來提高自己的效能,不管其他APP或者系統的死活,導致了很嚴重的資源浪費,這也是Android被iOS詬病的最大原因。本文的保活手段也分兩種:遵紀守法的程式保活與流氓手段換來的程式保活。

宣告:堅決反對流氓手段實現程式保活 堅決反對流氓程式保活 堅決反對流氓程式保活 “請告訴產品:無法進入白名單”

  • 正常守法的程式保活:記憶體裁剪(好學生APP要使用)
  • 流氓的程式保活,提高優先順序(好學生APP別用)
  • 流氓的程式保活,雙Service程式相互喚醒(binder訃告原理)(好學生APP別用)

針對LowmemoryKiller所做的程式保活

LowmemoryKiller會在記憶體不足的時候掃描所有的使用者程式,找到不是太重要的程式殺死,至於LowmemoryKiller殺程式夠不夠狠,要看當前的記憶體使用情況,記憶體越少,下手越狠。在核心中,LowmemoryKiller.c定義了幾種記憶體回收等級如下:(也許不同的版本會有些不同)

static short lowmem_adj[6] = {
    0,
    1,
    6,
    12,
};
static int lowmem_adj_size = 4;

static int lowmem_minfree[6] = {
    3 * 512,    /* 6MB */
    2 * 1024,    /* 8MB */
    4 * 1024,    /* 16MB */
    16 * 1024,    /* 64MB */
};
static int lowmem_minfree_size = 4;複製程式碼

lowmem_adj中各項數值代表閾值的警戒級數,lowmem_minfree代表對應級數的剩餘記憶體,兩者一一對應,比如當系統的可用記憶體小於6MB時,警戒級數為0;當系統可用記憶體小於8M而大於6M時,警戒級數為1;當可用記憶體小於64M大於16MB時,警戒級數為12。LowmemoryKiller就是根據當前系統的可用記憶體多少來獲取當前的警戒級數,如果程式的oom_adj大於警戒級數並且佔記憶體最大,將會被優先殺死, 具有相同omm_adj的程式,則殺死佔用記憶體較多的。omm_adj越小,代表程式越重要。一些前臺的程式,oom_adj會比較小,而後臺的服務,omm_adj會比較大,所以當記憶體不足的時候,Lowmemorykiller先殺掉的是後臺服務而不是前臺的程式。對於LowmemoryKiller的殺死,這裡有一句話很重要,就是: 具有相同omm_adj的程式,則殺死佔用記憶體較多的,因此,如果我們的APP進入後臺,就儘量釋放不必要的資源,以降低自己被殺的風險。那麼如何釋放呢?onTrimeMemory是個不錯的時機,而onLowmemory可能是最後的稻草,下面複習一下,LowmemoryKiller如何殺程式的,簡單看一下實現原始碼(4.3):(其他版本原理大同小異)

static int lowmem_shrink(int nr_to_scan, gfp_t gfp_mask)
{
    ...        
    <!--關鍵點1 獲取free記憶體狀況-->
    int other_free = global_page_state(NR_FREE_PAGES);
    int other_file = global_page_state(NR_FILE_PAGES);
    <!--關鍵點2 找到min_adj -->
    for(i = 0; i < array_size; i++) {
        if (other_free < lowmem_minfree[i] &&
            other_file < lowmem_minfree[i]) {
            min_adj = lowmem_adj[i];
            break;
        }
    }
  <!--關鍵點3 找到p->oomkilladj>min_adj並且oomkilladj最大,記憶體最大的程式-->
    for_each_process(p) {
        // 找到第一個大於等於min_adj的,也就是優先順序比閾值低的
        if (p->oomkilladj < min_adj || !p->mm)
            continue;
        // 找到tasksize這個是什麼呢
        tasksize = get_mm_rss(p->mm);
        if (tasksize <= 0)
            continue;
        if (selected) {
        // 找到優先順序最低,並且記憶體佔用大的
            if (p->oomkilladj < selected->oomkilladj)
                continue;
            if (p->oomkilladj == selected->oomkilladj &&
                tasksize <= selected_tasksize)
                continue;
        }
        selected = p;
        selected_tasksize = tasksize;
        lowmem_print(2, "select %d (%s), adj %d, size %d, to kill\n",
                     p->pid, p->comm, p->oomkilladj, tasksize);
    }
    if(selected != NULL) {...
        force_sig(SIGKILL, selected);
    }
    return rem;
}複製程式碼

這裡先看一下關鍵點1,這裡是核心獲取當前的free記憶體狀況,並且根據當前空閒記憶體計算出當前後臺殺死的等級(關鍵點2),之後LowmemoryKiller會遍歷所有的程式,找到優先順序低並且記憶體佔用較大的程式,如果這個程式的p->oomkilladj>min_adj,就表示這個程式可以殺死,LowmemoryKiller就會送過傳送SIGKILL訊號殺死就程式,注意,lmkd會先找優先順序低的程式,如果多個程式優先順序相同,就優先殺死記憶體佔用高的程式,這樣就為我們提供了兩種程式包活手段

  • 1、提高程式的優先順序,其實就是減小程式的p->oomkilladj(越小越重要)
  • 2、降低APP的記憶體佔用量,在oom_adj相同的時候,會優先幹掉記憶體消耗大的程式

不過大多數情況下,Android對於程式優先順序的管理都是比較合理,即使某些場景需要特殊手段提高優先順序,Android也是給了參考方案的,比如音訊播放,UI隱藏的時候,需要將Sevice程式設定成特定的優先順序防止被後臺殺死,比如一些備份的程式也需要一些特殊處理,但是這些都是在Android允許的範圍內的,所以絕大多數情況下,Android是不建議APP自己提高優先順序的,因為這會與Android自身的的程式管理相悖,換句話說就是耍流氓。這裡先討論第二種情況,通過合理的釋放記憶體降低被殺的風險,地主不想被殺,只能交公糧,自裁保身,不過這裡也要看自裁的時機,什麼時候瘦身比較划算,O(∩_∩)O哈哈~!這裡就牽扯到有一個onTrimeMemory函式,該函式是一個系統回撥函式,主要是Android系統經過綜合評估,給APP一個記憶體裁剪的等級,比如當記憶體還算充足,APP退回後臺時候,會收到TRIM_MEMORY_UI_HIDDEN等級的裁剪,就是告訴APP,釋放一些UI資源,比如大量圖片記憶體,一些引入圖片瀏覽快取的場景,可能就更加需要釋放UI資源,下面來看下onTrimeMemory的回撥時機及APP應該做出相應處理。

onTrimeMemory的回撥時機及記憶體裁剪等級

OnTrimMemory是在Android 4.0引入的一個回撥介面,其主要作用就是通知應用程式在不同的場景下進行自我瘦身,釋放記憶體,降低被後臺殺死的風險,提高使用者體驗,由於目前APP的適配基本是在14之上,所以不必考慮相容問題。在APP中可以在Application或者Activity中直接覆蓋OnTrimMemory函式以響應系統號召:

  public class LabApplication extends Application {
     @Override
       public void onTrimMemory(int level) {
         super.onTrimMemory(level);
         //根據level裁減記憶體
          }
    }複製程式碼

onTrimeMemory支援不同裁剪等級,比如,APP通過HOME建進入後臺時,其優先順序(oom_adj)就發生變化,從未觸發onTrimeMemory回撥,這個時候系統給出的裁剪等級一般是TRIM_MEMORY_UI_HIDDEN,意思是,UI已經隱藏,UI相關的、佔用記憶體大的資源就可以釋放了,比如大量的圖片快取等,當然,還會有其他很多場景對應不同的裁剪等級。因此,需要弄清楚兩個問題:

  • 1、不同的裁剪等級是如何生成的,其意義是什麼
  • 2、APP如何根據不同的裁剪等級釋放記憶體資源,(自裁的程度)

先看下ComponentCallbacks2中定義的不同裁剪等級的意義:這裡一共定義了4+3共7個裁剪等級,為什麼說是4+3呢?因為有4個是針對後臺程式的,還有3個是針對前臺(RUNNING)程式的,目標物件不同,具體看下分析

裁剪等級 數值 目標程式
TRIM_MEMORY_COMPLETE 80 後臺程式
TRIM_MEMORY_MODERATE 60 後臺程式
TRIM_MEMORY_BACKGROUND 40 後臺程式
TRIM_MEMORY_UI_HIDDEN 20 後臺程式
TRIM_MEMORY_RUNNING_CRITICAL 15 前臺RUNNING程式
TRIM_MEMORY_RUNNING_LOW 10 前臺RUNNING程式
TRIM_MEMORY_RUNNING_MODERATE 5 前臺RUNNING程式

其意義如下:

  • TRIM_MEMORY_UI_HIDDEN 當前應用程式的所有UI介面不可見,一般是使用者點選了Home鍵或者Back鍵,導致應用的UI介面不可見,這時應該釋放一些UI相關資源,TRIM_MEMORY_UI_HIDDEN是使用頻率最高的裁剪等級。官方文件:the process had been showing a user interface, and is no longer doing so. Large allocations with the UI should be released at this point to allow memory to be better managed

  • TRIM_MEMORY_BACKGROUND 當前手機目前記憶體吃緊(後臺程式數量少),系統開始根據LRU快取來清理程式,而該程式位於LRU快取列表的頭部位置,不太可能被清理掉的,但釋放掉一些比較容易恢復的資源能夠提高手機執行效率,同時也能保證恢復速度。官方文件:the process has gone on to the LRU list. This is a good opportunity to clean up resources that can efficiently and quickly be re-built if the user returns to the app

  • TRIM_MEMORY_MODERATE 當前手機目前記憶體吃緊(後臺程式數量少),系統開始根據LRU快取來清理程式,而該程式位於LRU快取列表的中間位置,應該多釋放一些記憶體提高執行效率。官方文件:the process is around the middle of the background LRU list; freeing memory can help the system keep other processes running later in the list for better overall performance.

  • TRIM_MEMORY_COMPLETE 當前手機目前記憶體吃緊 (後臺程式數量少),系統開始根據LRU快取來清理程式,而該程式位於LRU快取列表的最邊緣位置,系統會先殺掉該程式,應盡釋放一切可以釋放的記憶體。官方文件:the process is nearing the end of the background LRU list, and if more memory isn't found soon it will be killed.

以下三個等級針對前臺執行應用

  • TRIM_MEMORY_RUNNING_MODERATE 表示該程式是前臺或可見程式,正常執行,一般不會被殺掉,但是目前手機有些吃緊(後臺及空程式存量不多),系統已經開始清理記憶體,有必要的話,可以釋放一些記憶體。官方文件:the process is not an expendable background process, but the device is running moderately low on memory. Your running process may want to release some unneeded resources for use elsewhere。

  • TRIM_MEMORY_RUNNING_LOW 表示該程式是前臺或可見程式,正常執行,一般不會被殺掉,但是目前手機比較吃緊(後臺及空程式被全乾掉了一大波),應該去釋放掉一些不必要的資源以提升系統效能。 官方文件:the process is not an expendable background process, but the device is running low on memory. Your running process should free up unneeded resources to allow that memory to be used elsewhere.

  • TRIM_MEMORY_RUNNING_CRITICAL 表示該程式是前臺或可見程式,但是目前手機比較記憶體十分吃緊(後臺及空程式基本被全乾掉了),這時應當儘可能地去釋放任何不必要的資源,否則,系統可能會殺掉所有快取中的程式,並且殺一些本來應當保持執行的程式。官方文件:the process is not an expendable background process, but the device is running extremely low on memory and is about to not be able to keep any background processes running. Your running process should free up as many non-critical resources as it can to allow that memory to be used elsewhere. The next thing that will happen after this is called to report that nothing at all can be kept in the background, a situation that can start to notably impact the user.

以上抽象的說明了一下Android既定引數的意義,下面看一下onTrimeMemory回撥的時機及原理,這裡採用6.0的程式碼分析,因為6.0比之前4.3的程式碼清晰很多:當使用者的操作導致APP優先順序發生變化,就會呼叫updateOomAdjLocked去更新程式的優先順序,在更新優先順序的時候,會掃描一遍LRU程式列表, 重新計算程式的oom_adj,並且參考當前系統狀況去通知程式裁剪記憶體(這裡只是針對Android Java層APP),這次操作一般發生在開啟新的Activity介面、退回後臺、應用跳轉切換等等,updateOomAdjLocked程式碼大概600多行,比較長,儘量精簡後如下,還是比較長,這裡拆分成一段段梳理:

final void updateOomAdjLocked() {
    final ActivityRecord TOP_ACT = resumedAppLocked();
    <!--關鍵點1 找到TOP——APP,最頂層顯示的APP-->
    final ProcessRecord TOP_APP = TOP_ACT != null ? TOP_ACT.app : null;
    final long oldTime = SystemClock.uptimeMillis() - ProcessList.MAX_EMPTY_TIME;
    mAdjSeq++;
    mNewNumServiceProcs = 0;
    final int emptyProcessLimit;
    final int hiddenProcessLimit;
    <!--關鍵點2 找到TOP——APP,最頂層顯示的APP-->
    // 初始化一些程式數量的限制:
    if (mProcessLimit <= 0) {
        emptyProcessLimit = hiddenProcessLimit = 0;
    } else if (mProcessLimit == 1) {
        emptyProcessLimit = 1;
        hiddenProcessLimit = 0;
    } else {
        // 空程式跟後臺非空快取繼承的比例
        emptyProcessLimit = ProcessList.computeEmptyProcessLimit(mProcessLimit);
        cachedProcessLimit = mProcessLimit - emptyProcessLimit;
    }

     <!--關鍵點3 確定下程式槽 3個槽->
    int numSlots = (ProcessList.HIDDEN_APP_MAX_ADJ - ProcessList.HIDDEN_APP_MIN_ADJ + 1) / 2;
    // 後臺程式/前臺程式/空程式
    int numEmptyProcs = N - mNumNonCachedProcs - mNumCachedHiddenProcs;
     int emptyFactor = numEmptyProcs/numSlots;
    if (emptyFactor < 1) emptyFactor = 1;
    int hiddenFactor = (mNumHiddenProcs > 0 ? mNumHiddenProcs : 1)/numSlots;
    if (hiddenFactor < 1) hiddenFactor = 1;
    int stepHidden = 0;
    int stepEmpty = 0;
    int numHidden = 0;
    int numEmpty = 0;
    int numTrimming = 0;
    mNumNonHiddenProcs = 0;
    mNumHiddenProcs = 0;
    int i = mLruProcesses.size();
    // 優先順序
    int curHiddenAdj = ProcessList.HIDDEN_APP_MIN_ADJ;
    // 初始化的一些值
    int nextHiddenAdj = curHiddenAdj+1;
    // 優先順序
    int curEmptyAdj = ProcessList.HIDDEN_APP_MIN_ADJ;
    // 有意思
    int nextEmptyAdj = curEmptyAdj+2;複製程式碼

這前三個關鍵點主要是做了一些準備工作,關鍵點1 是單獨抽離出TOP_APP,因為它比較特殊,系統只有一個前天程式,關鍵點2主要是根據當前的配置獲取後臺快取程式與空程式的數目限制,而關鍵點3是將後臺程式分為三備份,無論是後臺程式還是空程式,會間插的均分6個優先順序,一個優先順序是可以有多個程式的,而且並不一定空程式的優先順序小於HIDDEN程式優先順序。

    for (int i=N-1; i>=0; i--) {
            ProcessRecord app = mLruProcesses.get(i);
            if (!app.killedByAm && app.thread != null) {
                app.procStateChanged = false;
                <!--關鍵點4 計算程式的優先順序或者快取程式的優先順序->   
                // computeOomAdjLocked計算程式優先順序,但是對於後臺程式和empty程式computeOomAdjLocked無效,這部分優先順序是AMS自己根據LRU原則分配的
                computeOomAdjLocked(app, ProcessList.UNKNOWN_ADJ, TOP_APP, true, now);
                //還未最終確認,有些程式的優先順序,比如只有後臺activity或者沒有activity的程式,
              <!--關鍵點5 計算程式的優先順序或者快取程式的優先順序->   
                if (app.curAdj >= ProcessList.UNKNOWN_ADJ) {
                    switch (app.curProcState) {
                        case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY:
                        case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY_CLIENT:
                            app.curRawAdj = curCachedAdj;
                                    <!--關鍵點6 根據LRU為後臺程式分配優先順序-->
                            if (curCachedAdj != nextCachedAdj) {
                                stepCached++;
                                if (stepCached >= cachedFactor) {
                                    stepCached = 0;
                                    curCachedAdj = nextCachedAdj;
                                    nextCachedAdj += 2;
                                    if (nextCachedAdj > ProcessList.CACHED_APP_MAX_ADJ) {
                                        nextCachedAdj = ProcessList.CACHED_APP_MAX_ADJ;
                                    }
                                }
                            }
                            break;
                        default:
                                                                 <!--關鍵點7 根據LRU為後臺程式分配優先順序-->
                            app.curRawAdj = curEmptyAdj;
                            app.curAdj = app.modifyRawOomAdj(curEmptyAdj);
                            if (curEmptyAdj != nextEmptyAdj) {
                                stepEmpty++;
                                if (stepEmpty >= emptyFactor) {
                                    stepEmpty = 0;
                                    curEmptyAdj = nextEmptyAdj;
                                    nextEmptyAdj += 2;
                                    if (nextEmptyAdj > ProcessList.CACHED_APP_MAX_ADJ) {
                                        nextEmptyAdj = ProcessList.CACHED_APP_MAX_ADJ;
                                    }
                                }
                            }
                            break;
                    }
                }
                <!--關鍵點8 設定優先順序-->
                applyOomAdjLocked(app, true, now, nowElapsed);複製程式碼

上面的這幾個關鍵點主要是為所有程式計算出其優先順序oom_adj之類的值,對於非後臺程式,比如HOME程式 服務程式,備份程式等都有自己的獨特的計算方式,而剩餘的後臺程式就根據LRU三等分配優先順序。

                 <!--關鍵點9 根據快取程式的數由AMS選擇性殺程式,後臺程式太多-->
                switch (app.curProcState) {
                    case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY:
                    case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY_CLIENT:
                        mNumCachedHiddenProcs++;
                        numCached++;
                        if (numCached > cachedProcessLimit) {
                            app.kill("cached #" + numCached, true);
                        }
                        break;
                    case ActivityManager.PROCESS_STATE_CACHED_EMPTY:
                        if (numEmpty > ProcessList.TRIM_EMPTY_APPS
                                && app.lastActivityTime < oldTime) {
                            app.kill("empty for "
                                    + ((oldTime + ProcessList.MAX_EMPTY_TIME - app.lastActivityTime)
                                    / 1000) + "s", true);
                        } else {
                            numEmpty++;
                            if (numEmpty > emptyProcessLimit) {
                                app.kill("empty #" + numEmpty, true);
                            }
                        }
                        break;
                    default:
                        mNumNonCachedProcs++;
                        break;
                }
                 <!--關鍵點10 計算需要裁剪程式的數目-->
                if (app.curProcState >= ActivityManager.PROCESS_STATE_HOME
                        && !app.killedByAm) {
                        // 比home高的都需要裁剪,不包括那些等級高的程式
                    numTrimming++;
                }
            }
        }複製程式碼

上面的兩個關鍵點是看當前後臺程式是否過多或者過老,如果存在過多或者過老的後臺程式,AMS是有權利殺死他們的。之後才是我們比較關心的存活程式的裁剪:

        final int numCachedAndEmpty = numCached + numEmpty;
        int memFactor;
         <!--關鍵點11 根據後臺程式數目確定當前系統的記憶體使用狀況 ,確立記憶體裁剪等級(記憶體因子)memFactor,android的理念是准許存在一定數量的後臺程式,並且只有記憶體不夠的時候,才會縮減後臺程式-->
        if (numCached <= ProcessList.TRIM_CACHED_APPS
                && numEmpty <= ProcessList.TRIM_EMPTY_APPS) {
              // 等級高低 ,殺的越厲害,越少,需要約緊急的時候才殺
            if (numCachedAndEmpty <= ProcessList.TRIM_CRITICAL_THRESHOLD) {//3
                memFactor = ProcessStats.ADJ_MEM_FACTOR_CRITICAL;
            } else if (numCachedAndEmpty <= ProcessList.TRIM_LOW_THRESHOLD) { //5
                memFactor = ProcessStats.ADJ_MEM_FACTOR_LOW;
            } else {
                memFactor = ProcessStats.ADJ_MEM_FACTOR_MODERATE;
            }
        } else {
            // 後臺程式數量足夠說明記憶體充足
            memFactor = ProcessStats.ADJ_MEM_FACTOR_NORMAL;
        }
       <!--關鍵點12 根據記憶體裁剪等級裁剪記憶體 Android認為後臺程式不足的時候,記憶體也不足-->
        if (memFactor != ProcessStats.ADJ_MEM_FACTOR_NORMAL) {
            if (mLowRamStartTime == 0) {
                mLowRamStartTime = now;
            }
            int step = 0;
            int fgTrimLevel;
         // 記憶體不足的時候,也要通知前臺或可見程式進行縮減
            switch (memFactor) {
                case ProcessStats.ADJ_MEM_FACTOR_CRITICAL:
                    fgTrimLevel = ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL;
                    break;
                case ProcessStats.ADJ_MEM_FACTOR_LOW:
                    fgTrimLevel = ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW;
                    break;
                default:
                    fgTrimLevel = ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE;
                    break;
            }
            int factor = numTrimming/3;
            int minFactor = 2;
            if (mHomeProcess != null) minFactor++;
            if (mPreviousProcess != null) minFactor++;
            if (factor < minFactor) factor = minFactor;
            int curLevel = ComponentCallbacks2.TRIM_MEMORY_COMPLETE;複製程式碼

關鍵點11這裡不太好理解:Android系統根據後臺程式的數目來確定當前系統記憶體的狀況,後臺程式越多,越說明記憶體並不緊張,越少,說明越緊張,回收等級也就越高,如果後臺程式的數目較多,記憶體裁剪就比較寬鬆是ProcessStats.ADJ_MEM_FACTOR_NORMAL,如果不足,則再根據快取數目劃分等級。以6.0原始碼來說:

  • 如果後臺程式數量(包含空程式)< 3 ,就說明記憶體非常緊張,記憶體裁剪因子就是ProcessStats.ADJ_MEM_FACTOR_CRITICAL
  • 如果後臺程式數量(包含空程式)< 5 ,就說明記憶體非常緊張,記憶體裁剪因子就是ProcessStats.ADJ_MEM_FACTOR_LOW
  • 如果比上面兩個多,但是仍然不足正常的後臺數目 ,記憶體裁剪因子就是ProcessStats.ADJ_MEM_FACTOR_MODERATE

與之對應的關鍵點12,是確立前臺RUNNING程式(也不一定是前臺顯示)的裁剪等級。

  • ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL;
  • ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW;
  • ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE;

之後就真正開始裁剪APP,這裡先看後臺程式不足的情況的裁剪,這部分相對複雜一些:

            <!--裁剪後臺程式-->
            for (int i=N-1; i>=0; i--) {
                ProcessRecord app = mLruProcesses.get(i);
                if (allChanged || app.procStateChanged) {
                    setProcessTrackerStateLocked(app, trackerMemFactor, now);
                    app.procStateChanged = false;
                }   
                   //  PROCESS_STATE_HOME = 12;  
                   //PROCESS_STATE_LAST_ACTIVITY = 13; 退到後臺的就會用
                // 優先順序比較低,回收等級比較高ComponentCallbacks2.TRIM_MEMORY_COMPLETE
                //  當curProcState > 12且沒有被am殺掉的情況;上面的update的時候,在kill的時候,是會設定app.killedByAm的
                //裁剪的話,如果 >= ActivityManager.PROCESS_STATE_HOME,老的裁剪等級較高,不重要,越新鮮的程式,裁剪等級越低

                if (app.curProcState >= ActivityManager.PROCESS_STATE_HOME
                        && !app.killedByAm) {
                        // 先清理最陳舊的 ,最陳舊的那個遭殃
                    if (app.trimMemoryLevel < curLevel && app.thread != null) {
                        try {
                            app.thread.scheduleTrimMemory(curLevel);
                        } catch (RemoteException e) {
                        }
                    }
                    app.trimMemoryLevel = curLevel;
                    step++; 
                    // 反正一共就三個槽,將來再次重新整理的 時候,要看看是不是從一個槽裡面移動到另一個槽,
                    // 沒有移動,就不需要再次裁剪,等級沒變
                    if (step >= factor) {
                        step = 0;
                        switch (curLevel) {
                            case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:
                                curLevel = ComponentCallbacks2.TRIM_MEMORY_MODERATE;
                                break;
                            case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
                                curLevel = ComponentCallbacks2.TRIM_MEMORY_BACKGROUND;
                                break;
                        }
                    }
                }複製程式碼

上面的這部分是負責 app.curProcState >= ActivityManager.PROCESS_STATE_HOME這部分程式裁剪,這部分主要是後臺快取程式,一般是oom_adj在9-11之間的程式,這部門主要根據LRU確定不同的裁減等級。

                else {
                    if ((app.curProcState >= ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND
                            || app.systemNoUi) && app.pendingUiClean) {
                        // 釋放UI
                        final int level = ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN;
                        if (app.trimMemoryLevel < level && app.thread != null) {
                            try {
                                app.thread.scheduleTrimMemory(level);
                            } catch (RemoteException e) {
                            }
                        }
                        app.pendingUiClean = false;
                    }
                    // 啟動的時候會回撥一遍,如果有必要,啟動APP的時候,app.trimMemoryLevel=0
                    if (app.trimMemoryLevel < fgTrimLevel && app.thread != null) {
                        try {
                            app.thread.scheduleTrimMemory(fgTrimLevel);
                        } catch (RemoteException e) {
                        }
                    }
                    app.trimMemoryLevel = fgTrimLevel;
                }
            }
        } 複製程式碼

而這裡的裁剪主要是一些優先順序較高的程式,其裁剪一般是 ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN ,由於這部分程式比較重要,裁剪等級較低,至於前臺程式的裁剪,一般是在啟動的時候,這個時候app.pendingUiClean==false,只會裁剪當前程式:

        else {
              <!--關鍵點13 記憶體充足的時候,程式的裁剪-->
             ... 
            for (int i=N-1; i>=0; i--) {
                ProcessRecord app = mLruProcesses.get(i);
                // 在resume的時候,都是設定成true,所以退回後臺的時候app.pendingUiClean==true是滿足的,
                // 因此縮減一次,但是不會再次走這裡的分支縮減即使優先順序變化,但是已經縮減過
                // 除非走上面的後臺流程,那個時候這個程式的等級已經很低了,
                if ((app.curProcState >= ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND
                        || app.systemNoUi) && app.pendingUiClean) {
                    if (app.trimMemoryLevel < ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN
                            && app.thread != null) {
                        try {
                            app.thread.scheduleTrimMemory(
                                    ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN);
                        } catch (RemoteException e) {
                        }
                    }
                    // clean一次就弄成false
                    app.pendingUiClean = false;
                }
                // 基本算沒怎麼裁剪
                app.trimMemoryLevel = 0;
            }
        }
 }複製程式碼

最後這部分是後臺程式數量充足的時候,系統只會針對app.curProcState >= ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND的程式進行裁剪,而裁剪等級也較低:ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN,因此根據裁剪等級APP可以大概知道系統當前的記憶體狀況,同時也能知道系統希望自己如何裁剪,之後APP做出相應的瘦身即可。不過,上面的程式裁剪的優先順序是完全根據後臺程式數量來判斷的,但是,不同的ROM可能進行了改造,所以裁剪等級不一定完全準確,比如在開發者模式開啟限制後臺程式數量的選項,限制後臺程式數目不超過2個,那麼這個時候的裁剪等級就是不太合理的,因為記憶體可能很充足,但是由於限制了後臺程式的數量,導致裁剪等級過高。因此在使用的時候,最好結合裁剪等級與當前記憶體數量來綜合考量。

通過“流氓”手段提高oom_adj,降低被殺風險,化身流氓程式

關於程式優先順序的計算,Android是有自己的一條準則的,就算某些特殊場景的需要額外處理程式的oom_adj Android也是給了參考方案的。但是,那對於流氓來說,並沒有任何約束效力。 "流氓"仍然能夠參照oom_adj(優先順序)的計算規則,利用其漏洞,提高程式的oom_adj,以降低被殺的風險。如果單單降低被殺風險還好,就怕那種即不想死,又想佔用資源的APP,累積下去就會導致系統記憶體不足,導致整個系統卡頓。

優先順序的計算邏輯比較複雜,這裡只簡述非快取程式,因為一旦淪為快取程式,其優先順序就只能依靠LRU來計算,不可控。而流氓是不會讓自己淪為快取程式的,非快取程式是以下程式中的一種,並且,優先順序越高(數值越小),越不易被殺死:

ADJ優先順序 優先順序 程式型別
SERVICE_ADJ 5 服務程式(Service process)
HEAVY_WEIGHT_APP_ADJ 4 後臺的重量級程式,system/rootdir/init.rc檔案中設定
BACKUP_APP_ADJ 3 備份程式(這個不太瞭解)
PERCEPTIBLE_APP_ADJ 2 可感知程式,比如後臺音樂播放 ,通過startForeground設定的程式
VISIBLE_APP_ADJ 1 可見程式(可見,但是沒能獲取焦點,比如新程式僅有一個懸浮Activity,其後面的程式就是Visible process)
FOREGROUND_APP_ADJ 0 前臺程式(正在展示是APP,存在互動介面,Foreground process)
  • 第一種提高到FOREGROUND_APP_ADJ

我們從低到高看:如何讓程式程式設計FOREGROUND_APP_ADJ程式,也就是前臺程式,這個沒有別的辦法,只有TOP activity程式才能是算前臺程式。正常的互動邏輯下,這個是無法實現的,鎖屏的時候倒是可以啟動一個Activity,但是需要螢幕點亮的時候再隱藏,容易被使用者感知,得不償失,所以基本是無解,所以之前傳說的QQ通過一個畫素來保活的應該不是這種方案,而通過WindowManager往主螢幕新增View的方式也並未阻止程式被殺,到底是否通過一畫素實現程式包活,個人還未得到解答,希望能有人解惑。

  • 第二種,提高到VISIBLE_APP_ADJ或者PERCEPTIBLE_APP_ADJ(不同版本等級可能不同 “4.3 = PERCEPTIBLE_APP_ADJ” 而 “> 5.0 = VISIBLE_APP_ADJ”),就表現形式上看,微博,微等信都可能用到了,而且這種手段的APP一般很難殺死,就算從最近的任務列表刪除,其實程式還是沒有被殺死,只是殺死了Activity等元件。

先看一下原始碼中對兩種優先順序的定義,VISIBLE_APP_ADJ是含有可見但是非互動Activity的程式,PERCEPTIBLE_APP_ADJ是使用者可感知的程式,如後臺音樂播放等

    // This is a process only hosting components that are perceptible to the
    // user, and we really want to avoid killing them, but they are not
    // immediately visible. An example is background music playback.
    static final int PERCEPTIBLE_APP_ADJ = 2;

    // This is a process only hosting activities that are visible to the
    // user, so we'd prefer they don't disappear.
    static final int VISIBLE_APP_ADJ = 1;複製程式碼

這種做法是相對溫和點的,Android官方曾給過類似的方案,比如音樂播放時後,通過設定前臺服務的方式來保活,這裡就為流氓程式提供了入口,不過顯示一個常住服務會在通知欄上有個執行狀態的圖示,會被使用者感知到。但是Android恰恰還有個漏洞可以把該圖示移除,真不知道是不是Google故意的。這裡可以參考微信的保活方案:雙Service強制前臺程式保活

startForeground(ID, new Notification()),可以將Service變成前臺服務,所在程式就算退到後臺,優先順序只會降到PERCEPTIBLE_APP_ADJ或者VISIBLE_APP_ADJ,一般不會被殺掉,Android的有個漏洞,如果兩個Service通過同樣的ID設定為前臺程式,而其一通過stopForeground取消了前臺顯示,結果是保留一個前臺服務,但不在狀態列顯示通知,這樣就不會被使用者感知到耍流氓,這種手段是比較常用的流氓手段。優先順序提高後,AMS的killBackgroundProcesses已經不能把程式殺死了,它只會殺死oom_adj大於ProcessList.SERVICE_ADJ的程式,而最近的任務列表也只會清空Activity,無法殺掉程式。 因為後臺APP的優先順序已經提高到了PERCEPTIBLE_APP_ADJ或ProcessList.VISIBLE_APP_ADJ,可謂流氓至極,如果再佔據著記憶體不釋放,那就是潑皮無賴了,這裡有個遺留疑問:startForeground看原始碼只會提升到PERCEPTIBLE_APP_ADJ,但是在5.0之後的版本提升到了VISIBLE_APP_ADJ,這裡看原始碼,沒找到原因,希望有人能解惑。具體做法如下:

public class RogueBackGroundService extends Service {

    private static int ROGUE_ID = 1;
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return START_STICKY;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Intent intent = new Intent(this, RogueIntentService.class);
        startService(intent);
        startForeground(ROGUE_ID, new Notification());
    }
    public static class RogueIntentService extends IntentService {

        //流氓相互喚醒Service
        public RogueIntentService(String name) {
            super(name);
        }

        public RogueIntentService() {
            super("RogueIntentService");
        }

        @Override
        protected void onHandleIntent(Intent intent) {

        }
        @Override
        public void onCreate() {
            super.onCreate();
            startForeground(ROGUE_ID, new Notification());
        }    
       @Override
        public void onDestroy() {
            stopForeground(true);//這裡不寫也沒問題,好像會自動停止
            super.onDestroy();
        }
    }
}複製程式碼

不過這個漏洞在Android7.1之後失效了,因為Google加了一個校驗:如果還有Service通過setForeground繫結相同id的Notification,就不能cancelNotification,也就是說還是會顯示通知(在通知列表)。

 private void cancelForegroudNotificationLocked(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 = getServiceMap(r.userId);
            if (sm != null) {
            <!--檢視是不是與該ID 通知繫結的Service取消了了前臺顯示-->
                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.
                        <!--如果找到還有顯示的Service,直接返回-->
                        return;
                    }
                }
            }
            r.cancelNotification();
        }
    }複製程式碼

在7.1上,Google PlayStore渠道的微信似乎也放棄了這種保活手段,因為7.1的微信從最近的任務列表刪除是可以殺死程式的,如果採用上述手段是殺不了的。

雙Service守護程式保活(這個也很流氓,不過如果不提高優先順序(允許被殺),也算稍微良心)

前文我們分析過Android Binder的訃告機制:如果Service Binder實體的程式掛掉,系統會向Client傳送訃告,而這個訃告系統就給程式保活一個可鑽的空子。可以通過兩個程式中啟動兩個binder服務,並且互為C/S,一旦一個程式掛掉,另一個程式就會收到訃告,在收到訃告的時候,喚起被殺程式。邏輯如下下:

Android程式保活-自“裁”或者耍流氓
雙服務保活.jpg

首先編寫兩個binder實體服務PairServiceA ,PairServiceB,並且在onCreate的時候相互繫結,並在onServiceDisconnected收到訃告的時候再次繫結。

public class PairServiceA extends Service {

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new AliveBinder();
    }

    @Override
    public void onCreate() {
        super.onCreate();
        bindService(new Intent(PairServiceA.this, PairServiceB.class), mServiceConnection, BIND_AUTO_CREATE);
    }

    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {

        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            bindService(new Intent(PairServiceA.this, PairServiceB.class), mServiceConnection, BIND_AUTO_CREATE);
            ToastUtil.show("bind A");
        }
    };複製程式碼

與之配對的B

public class PairServiceB extends Service {

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new AliveBinder();
    }

    @Override
    public void onCreate() {
        super.onCreate();
        bindService(new Intent(PairServiceB.this, PairServiceA.class), mServiceConnection, BIND_AUTO_CREATE);
    }

    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {

        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            bindService(new Intent(PairServiceB.this, PairServiceA.class), mServiceConnection, BIND_AUTO_CREATE);
        }
    };
}複製程式碼

之後再Manifest中註冊,注意要程式分離

    <service android:name=".service.alive.PairServiceA"/>
    <service
        android:name=".service.alive.PairServiceB"
        android:process=":alive"/>複製程式碼

之後再Application或者Activity中啟動一個Service即可。

startService(new Intent(MainActivity.this, PairServiceA.class));複製程式碼

這個方案一般都沒問題,因為Binder訃告是系統中Binder框架自帶的,除非一次性全部殺死所有父子程式,這個沒測試過。

廣播或者Service原地復活的程式保活

還有一些比較常見的程式保活手段是通過註冊BroadcastReceiver來實現的比如:

  • 開機
  • 網路狀態切換
  • 相機
  • 一些國內推送SDK(內含一些)

另外也能依靠Service的自啟動特性,通過onStartCommand的START_STICKY來實現,相比上面的不死,這些算比較柔和的啟動了,畢竟這兩種都是允許後臺殺死的前提下啟動的:

public class BackGroundService extends Service {
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return START_STICKY;
    }
}複製程式碼

總結

所有流氓手段的程式保活,都是下策,建議不要使用,本文只是分析實驗用。當APP退回後臺,優先順序變低,就應該適時釋放記憶體,以提高系統流暢度,依賴流氓手段提高優先順序,還不釋放記憶體,保持不死的,都是作死。

Android 後臺殺死系列之一:FragmentActivity 及 PhoneWindow 後臺殺死處理機制
Android後臺殺死系列之二:ActivityManagerService與App現場恢復機制 Android後臺殺死系列之三:後臺殺死原理LowMemoryKiller(4.3-6.0)
Android後臺殺死系列之四:Binder訃告原理

僅供參考,歡迎指正

參考文件

谷歌文件Application
Android四大元件與程式啟動的關係
Android 7.0 ActivityManagerService(8) 程式管理相關流程分析(2) updateOomAdjLocked
Android 7.0 ActivityManagerService(9) 程式管理相關流程分析(3) computeOomAdjLocked 精
Android程式碼記憶體優化建議-OnTrimMemory優化 精
微信Android客戶端後臺保活經驗分享
按"Home"鍵回到桌面的過程
Android low memory killer 機制
應用記憶體優化之OnLowMemory&OnTrimMemory

相關文章