AMS重要的數(shù)據(jù)結(jié)構(gòu)解析(二):TaskRecord

Android依托Java型虛擬機(jī),OOM是經(jīng)常遇到的問(wèn)題础倍,那么在快達(dá)到OOM的時(shí)候烛占,系統(tǒng)難道不能回收部分界面來(lái)達(dá)到縮減開支的目的碼?在系統(tǒng)內(nèi)存不足的情況下沟启,可以通過(guò)AMS及LowMemoryKiller殺優(yōu)先級(jí)低的進(jìn)程忆家,來(lái)回收進(jìn)程資源。但是這點(diǎn)對(duì)于前臺(tái)OOM問(wèn)題并沒有多大幫助德迹,因?yàn)槊總€(gè)Android應(yīng)用有一個(gè)Java內(nèi)存上限芽卿,比如256或者512M,而系統(tǒng)內(nèi)存可能有6G或者8G胳搞,也就是說(shuō)卸例,一個(gè)APP的進(jìn)程達(dá)到OOM的時(shí)候,可能系統(tǒng)內(nèi)存還是很充足的肌毅,這個(gè)時(shí)候筷转,系統(tǒng)如何避免OOM的呢?ios是會(huì)將不可見界面都回收芽腾,之后再恢復(fù)旦装,Android做的并沒有那么徹底,簡(jiǎn)單說(shuō):對(duì)于單棧(TaskRecord)應(yīng)用摊滔,在前臺(tái)的時(shí)候阴绢,所有界面都不會(huì)被回收,只有多棧情況下艰躺,系統(tǒng)才會(huì)回收不可見棧的Activity呻袭。注意回收的目標(biāo)是不可見棧(TaskRecord)的Activity。

img

前臺(tái)APP回收?qǐng)鼍?/p>

如上圖腺兴,在前臺(tái)時(shí)左电,左邊單棧APP跟進(jìn)程生命周期綁定,多棧的页响,不可見棧TaskRecord1是有被干掉風(fēng)險(xiǎn)篓足,TaskRecord2不會(huì)。下面簡(jiǎn)單分析下闰蚕。

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

Google應(yīng)該也是想到了這種情況栈拖,源碼自身就給APP自身回收內(nèi)存留有入口,在每個(gè)進(jìn)程啟動(dòng)的時(shí)候没陡,回同步啟動(dòng)個(gè)微小的內(nèi)存監(jiān)測(cè)工具涩哟,入口是ActivityThread的attach函數(shù)索赏,Android應(yīng)用進(jìn)程啟動(dòng)后,都會(huì)調(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)鍵點(diǎn)1贴彼,添加監(jiān)測(cè)工具-->
            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)鍵點(diǎn)2 :如果已經(jīng)可用的內(nèi)存不足1/4著手處理殺死Activity潜腻,并且這個(gè)時(shí)候,沒有緩存進(jìn)程-->
                    if (dalvikUsed > ((3*dalvikMax)/4)) {
                        mSomeActivitiesChanged = false;
                        try {
                            mgr.releaseSomeActivities(mAppThread);
                        } catch (RemoteException e) {
                    ...
                }

先關(guān)鍵點(diǎn)1器仗,對(duì)于非系統(tǒng)進(jìn)程融涣,通過(guò)BinderInternal.addGcWatcher添加了一個(gè)內(nèi)存監(jiān)測(cè)工具,后面會(huì)發(fā)現(xiàn)精钮,這個(gè)工具的檢測(cè)時(shí)機(jī)是每個(gè)GC節(jié)點(diǎn)暴心。而對(duì)于我們上文說(shuō)的回收不可見Task的時(shí)機(jī)是在關(guān)鍵點(diǎn)2:Java使用內(nèi)存超過(guò)3/4的時(shí)候,調(diào)用AMS的releaseSomeActivities杂拨,嘗試釋放不可見Activity,當(dāng)然悯衬,并非所有不可見的Activity會(huì)被回收弹沽,當(dāng)APP內(nèi)存超過(guò)3/4的時(shí)候,調(diào)用棧如下:

img

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

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

之前說(shuō)過(guò)筋粗,通過(guò)BinderInternal.addGcWatcher就添加了一個(gè)內(nèi)存監(jiān)測(cè)工具策橘,原理是什么?其實(shí)很簡(jiǎn)單娜亿,就是利用了Java的finalize那一套:JVM垃圾回收器準(zhǔn)備釋放內(nèi)存前丽已,會(huì)先調(diào)用該對(duì)象finalize(如果有的話)。

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

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

這里有幾個(gè)關(guān)鍵點(diǎn)买决,關(guān)鍵點(diǎn)1是弱引用沛婴,GC的sGcWatcher引用的對(duì)象是要被回收的,這樣回收前就會(huì)走關(guān)鍵點(diǎn)2督赤,遍歷執(zhí)行之前通過(guò)BinderInternal.addGcWatcher添加的回調(diào)嘁灯,執(zhí)行完畢后,重新為sGcWatcher賦值新的弱引用躲舌,這樣就會(huì)走下一個(gè)輪回丑婿,這就是為什么GC的時(shí)候,有機(jī)會(huì)觸發(fā)releaseSomeActivities没卸,其實(shí)羹奉,這里是個(gè)不錯(cuò)的內(nèi)存監(jiān)測(cè)點(diǎn),用來(lái)擴(kuò)展自身的需求约计。

AMS的TaskRecord棧釋放機(jī)制

如果GC的時(shí)候诀拭,APP的Java內(nèi)存使用超過(guò)了3/4,就會(huì)觸發(fā)AMS的releaseSomeActivities病蛉,嘗試回收界面炫加,增加可用內(nèi)存瑰煎,但是并非所有場(chǎng)景都會(huì)真的銷毀Activity,比如單棧的APP就不會(huì)銷毀俗孝,多棧的也要分場(chǎng)景酒甸,可能選擇性銷毀不可見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)有一個(gè)進(jìn)行赋铝,則不再繼續(xù)-->
        if (r.finishing || r.state == DESTROYING || r.state == DESTROYED) {
            return;
        }
        <!--過(guò)濾-->
        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)鍵點(diǎn)1 只要要多余一個(gè)TaskRecord才有機(jī)會(huì)走這一步插勤,-->
            } 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;
            }
        }
    }
}

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

 tasks = new ArraySet<>();

也就是說(shuō),APP當(dāng)前進(jìn)程中筑凫,至少兩個(gè)TaskRecord才有必要走Activity的銷毀邏輯滑沧,注釋說(shuō)明很清楚:Didn't find two or more tasks to release,如果能找到超過(guò)兩個(gè)會(huì)怎么樣呢巍实?

 final int releaseSomeActivitiesLocked(ProcessRecord app, ArraySet<TaskRecord> tasks,
        String reason) {
    
    <!--maxTasks 保證最多清理- tasks.size() / 4有效個(gè)滓技,最少清理一個(gè) 同時(shí)最少保留一個(gè)前臺(tái)TaskRecord->
    int maxTasks = tasks.size() / 4;
    if (maxTasks < 1) {
    <!--至少清理一個(gè)-->
        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個(gè)TaskRecord令漂,同時(shí),至少要保證保留一個(gè)前臺(tái)可見TaskRecord丸边,比如如果有兩個(gè)TaskRecord叠必,則清理先前的一個(gè),保留前臺(tái)顯示的這個(gè)妹窖,如果三個(gè)挠唆,則還要看看最老的是否被有效清理,也就是是否有Activity被清理嘱吗,如果有則只清理一個(gè)玄组,保留兩個(gè),如果沒有谒麦,則繼續(xù)清理次老的俄讹,保留一個(gè)前臺(tái)展示的,如果有四個(gè)绕德,類似患膛,如果有5個(gè),則至少兩個(gè)清理耻蛇,這里的規(guī)則如果有興趣踪蹬,可自己簡(jiǎn)單看下胞此。一般APP中,很少有超過(guò)兩個(gè)TaskRecord的跃捣。

demo驗(yàn)證

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

img

總結(jié)

  • 單棧的進(jìn)程铆农,Activity跟進(jìn)程聲明周期一致
  • 多棧的,只有不可見棧的Activity可能被銷毀(Java內(nèi)存超過(guò)3/4,不可見)
  • 該回收機(jī)制利用了Java虛擬機(jī)的gc機(jī)finalize
  • 至少兩個(gè)TaskRecord占才有效狡耻,所以該機(jī)制并不激進(jìn)顿涣,因?yàn)橹髁鰽PP都是單棧。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末酝豪,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子精堕,更是在濱河造成了極大的恐慌孵淘,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,948評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件歹篓,死亡現(xiàn)場(chǎng)離奇詭異瘫证,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)庄撮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門背捌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人洞斯,你說(shuō)我怎么就攤上這事毡庆。” “怎么了烙如?”我有些...
    開封第一講書人閱讀 157,490評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵么抗,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我亚铁,道長(zhǎng)蝇刀,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,521評(píng)論 1 284
  • 正文 為了忘掉前任徘溢,我火速辦了婚禮吞琐,結(jié)果婚禮上捆探,老公的妹妹穿的比我還像新娘。我一直安慰自己站粟,他們只是感情好黍图,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,627評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著卒蘸,像睡著了一般雌隅。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上缸沃,一...
    開封第一講書人閱讀 49,842評(píng)論 1 290
  • 那天恰起,我揣著相機(jī)與錄音,去河邊找鬼趾牧。 笑死检盼,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的翘单。 我是一名探鬼主播吨枉,決...
    沈念sama閱讀 38,997評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼哄芜!你這毒婦竟也來(lái)了貌亭?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,741評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤认臊,失蹤者是張志新(化名)和其女友劉穎圃庭,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體失晴,經(jīng)...
    沈念sama閱讀 44,203評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡剧腻,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,534評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了涂屁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片书在。...
    茶點(diǎn)故事閱讀 38,673評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖拆又,靈堂內(nèi)的尸體忽然破棺而出儒旬,到底是詐尸還是另有隱情,我是刑警寧澤帖族,帶...
    沈念sama閱讀 34,339評(píng)論 4 330
  • 正文 年R本政府宣布义矛,位于F島的核電站,受9級(jí)特大地震影響盟萨,放射性物質(zhì)發(fā)生泄漏凉翻。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,955評(píng)論 3 313
  • 文/蒙蒙 一捻激、第九天 我趴在偏房一處隱蔽的房頂上張望制轰。 院中可真熱鬧前计,春花似錦、人聲如沸垃杖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)调俘。三九已至伶棒,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間彩库,已是汗流浹背肤无。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留骇钦,地道東北人宛渐。 一個(gè)月前我還...
    沈念sama閱讀 46,394評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像眯搭,于是被迫代替她去往敵國(guó)和親窥翩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,562評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容