Android可見APP的不可見任務棧(TaskRecord)銷燬分析

看書的小蝸牛發表於2019-01-08

Android依託Java型虛擬機器,OOM是經常遇到的問題,那麼在快達到OOM的時候,系統難道不能回收部分介面來達到縮減開支的目的碼?在系統記憶體不足的情況下,可以通過AMS及LowMemoryKiller殺優先順序低的程式,來回收程式資源。但是這點對於前臺OOM問題並沒有多大幫助,因為每個Android應用有一個Java記憶體上限,比如256或者512M,而系統記憶體可能有6G或者8G,也就是說,一個APP的程式達到OOM的時候,可能系統記憶體還是很充足的,這個時候,系統如何避免OOM的呢?ios是會將不可見介面都回收,之後再恢復,Android做的並沒有那麼徹底,簡單說:對於單棧(TaskRecord)應用,在前臺的時候,所有介面都不會被回收,只有多棧情況下,系統才會回收不可見棧的Activity。注意回收的目標是不可見**棧(TaskRecord)**的Activity。

前臺APP回收場景

如上圖,在前臺時,左邊單棧APP跟程式生命週期繫結,多棧的,不可見棧TaskRecord1是有被幹掉風險,TaskRecord2不會。下面簡單分析下。

Android原生提供記憶體回收入口

Google應該也是想到了這種情況,原始碼自身就給APP自身回收記憶體留有入口,在每個程式啟動的時候,回同步啟動個微小的記憶體監測工具,入口是ActivityThread的attach函式,Android應用程式啟動後,都會呼叫該函式:

ActivityThread

 private void attach(boolean system) {
        sCurrentActivityThread = this;
        mSystemThread = system;
        if (!system) {
           ...
            final IActivityManager mgr = ActivityManagerNative.getDefault();
			 ...
            // Watch for getting close to heap limit.
            <!--關鍵點1,新增監測工具-->
            BinderInternal.addGcWatcher(new Runnable() {
                @Override public void run() {
                    if (!mSomeActivitiesChanged) {
                        return;
                    }
                    Runtime runtime = Runtime.getRuntime();
                    long dalvikMax = runtime.maxMemory();
                    long dalvikUsed = runtime.totalMemory() - runtime.freeMemory();
                     <!--關鍵點2 :如果已經可用的記憶體不足1/4著手處理殺死Activity,並且這個時候,沒有快取程式-->
                    if (dalvikUsed > ((3*dalvikMax)/4)) {
                        mSomeActivitiesChanged = false;
                        try {
                            mgr.releaseSomeActivities(mAppThread);
                        } catch (RemoteException e) {
                    ...
				}
複製程式碼

先關鍵點1,對於非系統程式,通過BinderInternal.addGcWatcher新增了一個記憶體監測工具,後面會發現,這個工具的檢測時機是每個GC節點。而對於我們上文說的回收不可見Task的時機是在關鍵點2:Java使用記憶體超過3/4的時候,呼叫AMS的releaseSomeActivities,嘗試釋放不可見Activity,當然,並非所有不可見的Activity會被回收,當APP記憶體超過3/4的時候,呼叫棧如下:

APP記憶體超過3/4就會嘗試GC

APP在GC節點的記憶體監測機制

之前說過,通過BinderInternal.addGcWatcher就新增了一個記憶體監測工具,原理是什麼?其實很簡單,就是利用了Java的finalize那一套:JVM垃圾回收器準備釋放記憶體前,會先呼叫該物件finalize(如果有的話)。

  public class BinderInternal {
  <!--關鍵點1 弱引用-->
    static WeakReference<GcWatcher> sGcWatcher
            = new WeakReference<GcWatcher>(new GcWatcher());
    static ArrayList<Runnable> sGcWatchers = new ArrayList<>();
    static Runnable[] sTmpWatchers = new Runnable[1];
    static long sLastGcTime;

    static final class GcWatcher {
        @Override
        protected void finalize() throws Throwable {
            handleGc();
            sLastGcTime = SystemClock.uptimeMillis();
            synchronized (sGcWatchers) {
                sTmpWatchers = sGcWatchers.toArray(sTmpWatchers);
            }
            <!--關鍵點2 執行之前新增的回撥-->
            for (int i=0; i<sTmpWatchers.length; i++) {
                if (sTmpWatchers[i] != null) {
                    sTmpWatchers[i].run();
                }
            }
            <!--關鍵點3 下一次輪迴-->
            sGcWatcher = new WeakReference<GcWatcher>(new GcWatcher());
        }
    }

    public static void addGcWatcher(Runnable watcher) {
        synchronized (sGcWatchers) {
        
            sGcWatchers.add(watcher);
        }	
    }
 ...
}
複製程式碼

這裡有幾個關鍵點,關鍵點1是弱引用,GC的sGcWatcher引用的物件是要被回收的,這樣回收前就會走關鍵點2,遍歷執行之前通過BinderInternal.addGcWatcher新增的回撥,執行完畢後,重新為sGcWatcher賦值新的弱引用,這樣就會走下一個輪迴,這就是為什麼GC的時候,有機會觸發releaseSomeActivities,其實,這裡是個不錯的記憶體監測點,用來擴充套件自身的需求。

AMS的TaskRecord棧釋放機制

如果GC的時候,APP的Java記憶體使用超過了3/4,就會觸發AMS的releaseSomeActivities,嘗試回收介面,增加可用記憶體,但是並非所有場景都會真的銷燬Activity,比如單棧的APP就不會銷燬,多棧的也要分場景,可能選擇性銷燬不可見Activity。

ActivityManagerService

@Override
public void releaseSomeActivities(IApplicationThread appInt) {
    synchronized(this) {
        final long origId = Binder.clearCallingIdentity();
        try {
            ProcessRecord app = getRecordForAppLocked(appInt);
            mStackSupervisor.releaseSomeActivitiesLocked(app, "low-mem");
        } finally {
            Binder.restoreCallingIdentity(origId);
        }
    }
}


void releaseSomeActivitiesLocked(ProcessRecord app, String reason) {
    TaskRecord firstTask = null;
    ArraySet<TaskRecord> tasks = null;
    for (int i = 0; i < app.activities.size(); i++) {
        ActivityRecord r = app.activities.get(i);
        <!--如果已經有一個進行,則不再繼續-->
        if (r.finishing || r.state == DESTROYING || r.state == DESTROYED) {
            return;
        }
        <!--過濾-->
        if (r.visible || !r.stopped || !r.haveState || r.state == RESUMED || r.state == PAUSING
                || r.state == PAUSED || r.state == STOPPING) {
            continue;
        }
        if (r.task != null) {
            if (firstTask == null) {
                firstTask = r.task;
         <!--關鍵點1 只要要多餘一個TaskRecord才有機會走這一步,-->
            } else if (firstTask != r.task) {
                if (tasks == null) {
                    tasks = new ArraySet<>();
                    tasks.add(firstTask);
                }
                tasks.add(r.task);
            }
        }
    }
    <!--註釋很明顯,-->
    if (tasks == null) {
        if (DEBUG_RELEASE) Slog.d(TAG_RELEASE, "Didn't find two or more tasks to release");
        return;
    }

    // If we have activities in multiple tasks that are in a position to be destroyed,
    // let's iterate through the tasks and release the oldest one.
    final int numDisplays = mActivityDisplays.size();
    for (int displayNdx = 0; displayNdx < numDisplays; ++displayNdx) {
        final ArrayList<ActivityStack> stacks = mActivityDisplays.valueAt(displayNdx).mStacks;
        // Step through all stacks starting from behind, to hit the oldest things first.
        for (int stackNdx = 0; stackNdx < stacks.size(); stackNdx++) {
            final ActivityStack stack = stacks.get(stackNdx);
            // Try to release activities in this stack; if we manage to, we are done.
            if (stack.releaseSomeActivitiesLocked(app, tasks, reason) > 0) {
                return;
            }
        }
    }
}
複製程式碼

這裡先看第一個關鍵點1:如果想要tasks非空,則至少需要兩個TaskRecord才行,不然,只有一個firstTask,永遠無法滿足firstTask != r.task這個條件,也無法走

 tasks = new ArraySet<>();
複製程式碼

也就是說,APP當前程式中,至少兩個TaskRecord才有必要走Activity的銷燬邏輯,註釋說明很清楚:Didn't find two or more tasks to release,如果能找到超過兩個會怎麼樣呢?

 final int releaseSomeActivitiesLocked(ProcessRecord app, ArraySet<TaskRecord> tasks,
        String reason) {
    
    <!--maxTasks 保證最多清理- tasks.size() / 4有效個,最少清理一個 同時最少保留一個前臺TaskRecord->
    int maxTasks = tasks.size() / 4;
    if (maxTasks < 1) {
    <!--至少清理一個-->
        maxTasks = 1;
    }
    int numReleased = 0;
    for (int taskNdx = 0; taskNdx < mTaskHistory.size() && maxTasks > 0; taskNdx++) {
        final TaskRecord task = mTaskHistory.get(taskNdx);
        if (!tasks.contains(task)) {
            continue;
        }
        int curNum = 0;
        final ArrayList<ActivityRecord> activities = task.mActivities;
        for (int actNdx = 0; actNdx < activities.size(); actNdx++) {
            final ActivityRecord activity = activities.get(actNdx);
            if (activity.app == app && activity.isDestroyable()) {
                destroyActivityLocked(activity, true, reason);
                if (activities.get(actNdx) != activity) {
                    actNdx--;
                }
                curNum++;
            }
        }
        if (curNum > 0) {
            numReleased += curNum;
            maxTasks--;
            if (mTaskHistory.get(taskNdx) != task) {
                // The entire task got removed, back up so we don't miss the next one.
                taskNdx--;
            }
        }
    }
    return numReleased;
}
複製程式碼

ActivityStack利用maxTasks 保證,最多清理tasks.size() / 4,最少清理1個TaskRecord,同時,至少要保證保留一個前臺可見TaskRecord,比如如果有兩個TaskRecord,則清理先前的一個,保留前臺顯示的這個,如果三個,則還要看看最老的是否被有效清理,也就是是否有Activity被清理,如果有則只清理一個,保留兩個,如果沒有,則繼續清理次老的,保留一個前臺展示的,如果有四個,類似,如果有5個,則至少兩個清理,這裡的規則如果有興趣,可自己簡單看下。一般APP中,很少有超過兩個TaskRecord的。

demo驗證

模擬了兩個Task的模型,先啟動在一個棧裡面啟動多個Activity,然後在通過startActivity啟動一個新TaskRecord,並且在新棧中不斷分配java記憶體,當Java記憶體使用超過3/4的時候,就會看到前一個TaskRecord棧內Activity被銷燬的Log,同時如果通過studio的layoutinspect檢視,會發現APP只保留了新棧內的Activity,驗證了之前的分析。

image.png

總結

  • 單棧的程式,Activity跟程式宣告週期一致
  • 多棧的,只有不可見棧的Activity可能被銷燬(Java記憶體超過3/4,不可見)
  • 該回收機制利用了Java虛擬機器的gc機finalize
  • 至少兩個TaskRecord佔才有效,所以該機制並不激進,因為主流APP都是單棧。

作者:看書的小蝸牛

Android可見APP的不可見任務棧(TaskRecord)被銷燬分析

僅供參考,歡迎指正

相關文章