Android可見APP的不可見任務(wù)棧(TaskRecord)銷毀分析

Android依托Java型虛擬機,OOM是經(jīng)常遇到的問題,那么在快達到OOM的時候,系統(tǒng)難道不能回收部分界面來達到縮減開支的目的碼?在系統(tǒng)內(nèi)存不足的情況下,可以通過AMS及LowMemoryKiller殺優(yōu)先級低的進程,來回收進程資源。但是這點對于前臺OOM問題并沒有多大幫助,因為每個Android應(yīng)用有一個Java內(nèi)存上限,比如256或者512M,而系統(tǒng)內(nèi)存可能有6G或者8G,也就是說,一個APP的進程達到OOM的時候,可能系統(tǒng)內(nèi)存還是很充足的,這個時候,系統(tǒng)如何避免OOM的呢?ios是會將不可見界面都回收,之后再恢復(fù),Android做的并沒有那么徹底,簡單說:對于單棧(TaskRecord)應(yīng)用,在前臺的時候,所有界面都不會被回收,只有多棧情況下,系統(tǒng)才會回收不可見棧的Activity。注意回收的目標(biāo)是不可見棧(TaskRecord)的Activity。

前臺APP回收場景

如上圖,在前臺時,左邊單棧APP跟進程生命周期綁定,多棧的,不可見棧TaskRecord1是有被干掉風(fēng)險,TaskRecord2不會。下面簡單分析下。

Android原生提供內(nèi)存回收入口

Google應(yīng)該也是想到了這種情況,源碼自身就給APP自身回收內(nèi)存留有入口,在每個進程啟動的時候,回同步啟動個微小的內(nèi)存監(jiān)測工具,入口是ActivityThread的attach函數(shù),Android應(yīng)用進程啟動后,都會調(diào)用該函數(shù):

ActivityThread

 private void attach(boolean system) {
        sCurrentActivityThread = this;
        mSystemThread = system;
        if (!system) {
           ...
            final IActivityManager mgr = ActivityManagerNative.getDefault();
             ...
            // Watch for getting close to heap limit.
            <!--關(guān)鍵點1,添加監(jiān)測工具-->
            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();
                     <!--關(guān)鍵點2 :如果已經(jīng)可用的內(nèi)存不足1/4著手處理殺死Activity,并且這個時候,沒有緩存進程-->
                    if (dalvikUsed > ((3*dalvikMax)/4)) {
                        mSomeActivitiesChanged = false;
                        try {
                            mgr.releaseSomeActivities(mAppThread);
                        } catch (RemoteException e) {
                    ...
                }

先關(guān)鍵點1,對于非系統(tǒng)進程,通過BinderInternal.addGcWatcher添加了一個內(nèi)存監(jiān)測工具,后面會發(fā)現(xiàn),這個工具的檢測時機是每個GC節(jié)點。而對于我們上文說的回收不可見Task的時機是在關(guān)鍵點2:Java使用內(nèi)存超過3/4的時候,調(diào)用AMS的releaseSomeActivities,嘗試釋放不可見Activity,當(dāng)然,并非所有不可見的Activity會被回收,當(dāng)APP內(nèi)存超過3/4的時候,調(diào)用棧如下:

APP內(nèi)存超過3/4就會嘗試GC

APP在GC節(jié)點的內(nèi)存監(jiān)測機制

之前說過,通過BinderInternal.addGcWatcher就添加了一個內(nèi)存監(jiān)測工具,原理是什么?其實很簡單,就是利用了Java的finalize那一套:JVM垃圾回收器準(zhǔn)備釋放內(nèi)存前,會先調(diào)用該對象finalize(如果有的話)。

  public class BinderInternal {
  <!--關(guān)鍵點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);
            }
            <!--關(guān)鍵點2 執(zhí)行之前添加的回調(diào)-->
            for (int i=0; i<sTmpWatchers.length; i++) {
                if (sTmpWatchers[i] != null) {
                    sTmpWatchers[i].run();
                }
            }
            <!--關(guān)鍵點3 下一次輪回-->
            sGcWatcher = new WeakReference<GcWatcher>(new GcWatcher());
        }
    }

    public static void addGcWatcher(Runnable watcher) {
        synchronized (sGcWatchers) {
        
            sGcWatchers.add(watcher);
        }   
    }
 ...
}

這里有幾個關(guān)鍵點,關(guān)鍵點1是弱引用,GC的sGcWatcher引用的對象是要被回收的,這樣回收前就會走關(guān)鍵點2,遍歷執(zhí)行之前通過BinderInternal.addGcWatcher添加的回調(diào),執(zhí)行完畢后,重新為sGcWatcher賦值新的弱引用,這樣就會走下一個輪回,這就是為什么GC的時候,有機會觸發(fā)releaseSomeActivities,其實,這里是個不錯的內(nèi)存監(jiān)測點,用來擴展自身的需求。

AMS的TaskRecord棧釋放機制

如果GC的時候,APP的Java內(nèi)存使用超過了3/4,就會觸發(fā)AMS的releaseSomeActivities,嘗試回收界面,增加可用內(nèi)存,但是并非所有場景都會真的銷毀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);
        <!--如果已經(jīng)有一個進行,則不再繼續(xù)-->
        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;
         <!--關(guān)鍵點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;
            }
        }
    }
}

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

 tasks = new ArraySet<>();

也就是說,APP當(dāng)前進程中,至少兩個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被清理,如果有則只清理一個,保留兩個,如果沒有,則繼續(xù)清理次老的,保留一個前臺展示的,如果有四個,類似,如果有5個,則至少兩個清理,這里的規(guī)則如果有興趣,可自己簡單看下。一般APP中,很少有超過兩個TaskRecord的。

demo驗證

模擬了兩個Task的模型,先啟動在一個棧里面啟動多個Activity,然后在通過startActivity啟動一個新TaskRecord,并且在新棧中不斷分配java內(nèi)存,當(dāng)Java內(nèi)存使用超過3/4的時候,就會看到前一個TaskRecord棧內(nèi)Activity被銷毀的Log,同時如果通過studio的layoutinspect查看,會發(fā)現(xiàn)APP只保留了新棧內(nèi)的Activity,驗證了之前的分析。

image.png

總結(jié)

  • 單棧的進程,Activity跟進程聲明周期一致
  • 多棧的,只有不可見棧的Activity可能被銷毀(Java內(nèi)存超過3/4,不可見)
  • 該回收機制利用了Java虛擬機的gc機finalize
  • 至少兩個TaskRecord占才有效,所以該機制并不激進,因為主流APP都是單棧。

作者:看書的小蝸牛

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

僅供參考,歡迎指正

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容