你的ViewPager八成用錯(cuò)了(2)內(nèi)存泄漏??jī)?nèi)存溢出罪郊?

前言

寫作記錄:5月27日晚上寫下初版蠕蚜,30日下午補(bǔ)充一些內(nèi)容...結(jié)束

前幾天發(fā)布了第一篇文章
,關(guān)于分析FragmentPagerAdapter的...沒想到引起個(gè)各路英雄豪杰的激烈討論悔橄。這其中有兩個(gè)很有意義的點(diǎn):

  • 1靶累、錯(cuò)誤的第一種用法引發(fā)內(nèi)存泄漏(不準(zhǔn)確)。
  • 2癣疟、FragmentStatePagerAdapter在FragmentPagerAdapter基礎(chǔ)上做了什么挣柬。

今天這篇文章,咱們就來(lái)聊一聊上面?zhèn)z個(gè)話題睛挚。

以下源碼基于:implementation "androidx.fragment:fragment:1.2.0"

正文

錯(cuò)誤的用法引起內(nèi)存泄漏邪蛔。

說(shuō)實(shí)話,我其實(shí)的確沒有留意過這個(gè)點(diǎn)扎狱。當(dāng)評(píng)論中的同學(xué)提到這一點(diǎn)的時(shí)候侧到,我想了想似乎可以“說(shuō)得通”:Activity相對(duì)較Fragment勃教,應(yīng)該生命周期會(huì)更長(zhǎng),如果在Activity直接強(qiáng)引用所有的Fragment的實(shí)例匠抗。按理說(shuō)的確會(huì)有泄漏問題故源。

不過這個(gè)結(jié)論的前提是基于:Activity比Fragment生命周期更長(zhǎng),如果不是這樣的話汞贸,也談不上存在內(nèi)存泄漏绳军。所以為了求證這個(gè)結(jié)論咱們還是從源碼中一探究竟。

一矢腻、內(nèi)存泄漏门驾?

首先能夠確定的是,無(wú)論正誤用法都不會(huì)存在內(nèi)存泄漏問題多柑。

但是會(huì)有可能存在內(nèi)存溢出奶是,并且錯(cuò)誤的寫法更容易出現(xiàn)。而其實(shí)我們線上場(chǎng)景也遇到過這類問題顷蟆,當(dāng)時(shí)我們是有30+個(gè)Fragment诫隅,然后在低端手機(jī)上爆出了很多這樣的crash:

java.lang.RuntimeException: android.os.TransactionTooLargeException: data parcel size 3117432 bytes
這個(gè)crash出現(xiàn)的原因,下文會(huì)展開帐偎。

1.1逐纬、FragmentManager怎么初始化的

接下來(lái)咱們聊一下為什么不會(huì)出現(xiàn)內(nèi)存泄漏。

首先咱們都知道Fragment是由FragmentManager管理的削樊,那咱們就基于這個(gè)共識(shí)一起來(lái)看一看源碼:

通常咱們這樣從一個(gè)Activity中拿到FragmentManager:activity.supportFragmentManager豁生。那咱們就順著這個(gè)調(diào)用,看一看FM是如何初始化的漫贞。

final FragmentController mFragments = FragmentController.createController(new HostCallbacks());

@NonNull
public FragmentManager getSupportFragmentManager() {
    return mFragments.getSupportFragmentManager();
}

可以看出對(duì)外提供FragmentManager的FragmentController類是在Activity里直接被new出來(lái)的甸箱,而FragmentController中提供FM是這樣的:

private final FragmentHostCallback<?> mHost;

@NonNull
public FragmentManager getSupportFragmentManager() {
    return mHost.mFragmentManager;
}

mHost就是咱們new FragmentController時(shí)候傳進(jìn)來(lái)的new HostCallbacks()。而HostCallbacks中的FM又是怎么來(lái)的呢迅脐?

final FragmentManager mFragmentManager = new FragmentManagerImpl();

可以看到直接是new出來(lái)的芍殖。因此這里我們就能明確了,其實(shí)Activity是強(qiáng)引用了FM谴蔑。只要Activity不被回收豌骏,那么FM就不會(huì)被回收,那么FM中的Fragment也就不會(huì)被回收隐锭。那么也就有了上面的結(jié)論:大家生命周期一樣長(zhǎng)窃躲,其實(shí)談不上什么內(nèi)存泄漏。

1.2钦睡、Google如何幫我們緩存Fragment

但是蒂窒,咱們上邊提到過,雖然沒有內(nèi)存泄漏,但是存在內(nèi)存溢出洒琢!那么這又是誰(shuí)的鍋呢秧秉?這次咱們可以放心,這個(gè)鍋還真不是咱們開發(fā)者的問題 纬凤!沒錯(cuò)福贞,這口鍋必須得穩(wěn)穩(wěn)的扣在Google頭上撩嚼!來(lái)咱們看源碼:

咱們?nèi)粘+@取Fragment實(shí)例都是基于FragmentManager的find()系列方法停士,咱們就從這個(gè)方法來(lái)看一看FM如果保存咱們的Fragment實(shí)例:

@Nullable
private final FragmentStore mFragmentStore = new FragmentStore();

public Fragment findFragmentById(@IdRes int id) {
    return mFragmentStore.findFragmentById(id);
}

真正的實(shí)現(xiàn)是代理到FragmentStore中,沒直白的名字完丽。FragmentStore這樣去find:

@Nullable
Fragment findFragmentById(@IdRes int id) {
    // First look through added fragments.
    for (int i = mAdded.size() - 1; i >= 0; i--) {
        Fragment f = mAdded.get(i);
        if (f != null && f.mFragmentId == id) {
            return f;
        }
    }
    // Now for any known fragment.
    for (FragmentStateManager fragmentStateManager : mActive.values()) {
        if (fragmentStateManager != null) {
            Fragment f = fragmentStateManager.getFragment();
            if (f.mFragmentId == id) {
                return f;
            }
        }
    }
    return null;
}

可以看出這里是通過倆個(gè)集合去find恋技,分別是mAdded、mActive逻族。

private final ArrayList<Fragment> mAdded = new ArrayList<>();
private final HashMap<String, FragmentStateManager> mActive = new HashMap<>();

1.3蜻底、什么樣的Fragment進(jìn)到mAdded集合

mAdded這個(gè)List會(huì)存儲(chǔ)attach上的Fragment,因此它不會(huì)有很多(如果我們的mOffscreenPageLimit=1)聘鳞,那么這個(gè)集合的size最大是3薄辅,為啥?咱們看源碼抠璃。

void addFragment(@NonNull Fragment fragment) {
    if (mAdded.contains(fragment)) {
        throw new IllegalStateException("Fragment already added: " + fragment);
    }
    synchronized (mAdded) {
        mAdded.add(fragment);
    }
    fragment.mAdded = true;
}

void removeFragment(@NonNull Fragment fragment) {
    synchronized (mAdded) {
        mAdded.remove(fragment);
    }
    fragment.mAdded = false;
}

mAdded的add和remove又在FM中有四種可能調(diào)用站楚,對(duì)于addFragment()來(lái)說(shuō),F(xiàn)M會(huì)在OP_ADD搏嗡、OP_ATTACH時(shí)調(diào)用窿春,源碼分別如下:

case OP_ADD:
    f.setNextAnim(op.mEnterAnim);
    mManager.setExitAnimationOrder(f, false);
    mManager.addFragment(f);
    break;
case OP_ATTACH:
    f.setNextAnim(op.mEnterAnim);
    mManager.setExitAnimationOrder(f, false);
    mManager.attachFragment(f);
    break;

有了第一篇文章的基礎(chǔ),咱們明白對(duì)于FragmentPageradapter來(lái)說(shuō)find不到Fragment采盒,就會(huì)調(diào)用getItem()去new Fragment然后add旧乞,也就是走到OP_ADD。否則直接attach走OP_ATTACH磅氨,這里種狀態(tài)都會(huì)走到mAdded的add尺栖。既然咱們看到了add,那么同樣對(duì)這兩種狀態(tài)相對(duì)的就是remove:

case OP_DETACH:
    f.setNextAnim(op.mExitAnim);
    mManager.detachFragment(f);
    break;
case OP_REMOVE:
    f.setNextAnim(op.mExitAnim);
    mManager.removeFragment(f);
    break;

很明顯的成對(duì)出現(xiàn)烦租,因此這個(gè)集合問題不大延赌,只要用法無(wú)誤這個(gè)集合就是恒等的。

1.4左权、什么樣的Fragment進(jìn)到mActive集合

接下來(lái)皮胡,咱們把目光移到mActive上。掃遍整個(gè)FragmentStore會(huì)發(fā)現(xiàn)赏迟,mActive只有一個(gè)場(chǎng)景會(huì)將集合特定位置置為null:

void makeInactive(@NonNull FragmentStateManager newlyInactive) {
    // 省略部分代碼
    mActive.put(f.mWho, null);
    // 省略部分代碼
}

這是唯一一個(gè)可以回收mActive的機(jī)會(huì)屡贺。不過這個(gè)方法只會(huì)在當(dāng)前Fragment處于removing才會(huì)調(diào)用:

boolean beingRemoved = f.mRemoving && !f.isInBackStack();
if (beingRemoved || mNonConfig.shouldDestroy(f)) {
    makeInactive(fragmentStateManager);
}

而我們的FragmentPagerAdapter中destory的邏輯并沒有remove:

public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
    // 省略部分代碼
    mCurTransaction.detach(fragment);
    // 省略部分代碼
}

這就意味了除了最終的清理(clear)以外,在使用的過程中mActive是始終增加的!

實(shí)際也是如此甩栈,當(dāng)我們滑光所有的Fragmet泻仙,會(huì)發(fā)現(xiàn)mActive的數(shù)量之和就是所有Fragment的數(shù)量。比如這樣:

image.png

當(dāng)然量没,這樣可怕么玉转?只能說(shuō)不一定,因?yàn)檫@里僅僅是持有了Fragment的實(shí)例殴蹄,并不會(huì)包含View究抓。(只要不屬于add狀態(tài)的Fragment的View是為null的):

image.png

因此常規(guī)情況下Fragment實(shí)例并不怎么占內(nèi)存,畢竟此View上的內(nèi)存是會(huì)被回收掉的袭灯。因此如果我們不在Fragment中強(qiáng)引用一些其他大內(nèi)存對(duì)象刺下,問題也不大...但是事實(shí)卻與之相反,我們很容易在Fragment中留下大量成員變量稽荧,比如:

  • 1橘茉、為了減少布局inflate的時(shí)間,我們?nèi)ゾ彺鎂iew姨丈。
  • 2畅卓、為了緩存一些數(shù)據(jù),我們?cè)贔ragment中保留大量成員變量蟋恬。

但是翁潘,我們?cè)捳f(shuō)回來(lái),這樣的操作有毛病么筋现?個(gè)人覺得沒毛病唐础。但是在Google的這種設(shè)計(jì)下,那就很容易出問題矾飞。下面咱們模擬一個(gè)這種case下出現(xiàn)OOM的場(chǎng)景:在Fragment上開辟一些大內(nèi)存對(duì)象:

val array = IntArray(1024 * 1024 * 10)

當(dāng)我滑動(dòng)到第6個(gè)的Fragment時(shí)一膨,崩了...
java.lang.OutOfMemoryError: Failed to allocate a 41943052 byte allocation with 6959760 free bytes and 6MB until OOM

我們dump一下內(nèi)存:

image.png

并且無(wú)論我們?nèi)绾螐?qiáng)制GC,都無(wú)法回收這個(gè)內(nèi)存洒沦。因此這也就是驗(yàn)證了我們上邊的問題豹绪,出問題的本身在于Google的機(jī)制,而壓死這個(gè)機(jī)制的最后一根稻草在于Fragment中的“濫用”申眼。

其實(shí)我們也不用太擔(dān)心瞒津,這畢竟是極端情況。不過當(dāng)我們的場(chǎng)景需要大量的Fragment時(shí)括尸,是需要認(rèn)真考慮這部分聊的問題巷蚪。

那么問題來(lái)了,這個(gè)坑點(diǎn)Google知道嗎濒翻?答案是知道屁柏,所以才有了FragmentStatePagerAdapter啦膜,以及后來(lái)的ViewPager2。

1.5淌喻、額外聊聊android.os.TransactionTooLargeException

android.os.TransactionTooLargeException僧家,這個(gè)異常我在開篇提到過,官網(wǎng)也有單獨(dú)的介紹裸删。有經(jīng)驗(yàn)的老司機(jī)應(yīng)該都遇到過八拱,這個(gè)異常本身似乎和咱們今天聊的話題沒有直接關(guān)系。

但是咱們上述聊的內(nèi)容涯塔,很容易造成這個(gè)Exception肌稻。大家有興趣可以做一下這個(gè)操作:

  • 1、把ViewPager的個(gè)數(shù)弄的很大伤塌,然后滑到最后灯萍。
  • 2轧铁、開發(fā)者選項(xiàng)里打開 不保留活動(dòng)
  • 3每聪、按home鍵

八成會(huì)出現(xiàn)這個(gè)異常...如果遇不到,繼續(xù)加大ViewPager的個(gè)數(shù)齿风!

咱們這種場(chǎng)景出現(xiàn)這個(gè)問題:本質(zhì)的原因在于onSaveInstanceState()

@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
    super.onSaveInstanceState(outState);
    markFragmentsCreated();
    mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
    Parcelable p = mFragments.saveAllState();
    if (p != null) {
        outState.putParcelable(FRAGMENTS_TAG, p);
    }
    // 省略部分代碼
}

FM的在saveState()的時(shí)候是會(huì)保存mAdded集合和mActive集合的...咱們剛才也已經(jīng)分析過去药薯,mActive集合是一個(gè)全量數(shù)據(jù)集。所以Fragment足夠多救斑,這里的Parcel在傳遞的過程中就爆炸了童本。

這里咱們引申一下,Binder在通信的過程中最大的數(shù)據(jù)量是多少呢脸候?官網(wǎng)給出的答案是:1M

image.png

二穷娱、小總結(jié)

咱們第二部分聊的內(nèi)容,其實(shí)只有在極端情況下出現(xiàn)运沦。日常開發(fā)時(shí)泵额,我們八成遇不到這種場(chǎng)景。但是當(dāng)我們了解了這些內(nèi)容携添,就可以在遇到這類問題時(shí)準(zhǔn)確的預(yù)防或者根治嫁盲。

當(dāng)然正是因?yàn)檫@種種的原因,也就有了后續(xù)的FragmentStatePagerAdapter甚至ViewPager2烈掠。

尾聲

本是這篇文章開始是想把FragmentStatePagerAdapter一并聊了...但是寫完這一部分的時(shí)候發(fā)現(xiàn)篇幅已經(jīng)足夠長(zhǎng)了羞秤,為了避免大家“消化不良”。后續(xù)的內(nèi)容咱們下一篇文章再聊左敌。

整起來(lái)瘾蛋,我還能學(xué)!

還是那個(gè)原則:我會(huì)力求把文章寫到我認(rèn)為正確為止矫限,因此由于個(gè)人水平有限哺哼,難免出現(xiàn)紕漏京革,歡迎大家一起討論,一起共建標(biāo)準(zhǔn)答案幸斥!

我是一個(gè)應(yīng)屆生匹摇,最近和朋友們維護(hù)了一個(gè)公眾號(hào),內(nèi)容是我們?cè)趶膽?yīng)屆生過渡到開發(fā)這一路所踩過的坑甲葬,以及我們一步步學(xué)習(xí)的記錄廊勃,如果感興趣的朋友可以關(guān)注一下,一同加油~

個(gè)人公眾號(hào):咸魚正翻身
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末经窖,一起剝皮案震驚了整個(gè)濱河市坡垫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌画侣,老刑警劉巖冰悠,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異配乱,居然都是意外死亡溉卓,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門搬泥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)桑寨,“玉大人,你說(shuō)我怎么就攤上這事忿檩∥疚玻” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵燥透,是天一觀的道長(zhǎng)沙咏。 經(jīng)常有香客問我,道長(zhǎng)班套,這世上最難降的妖魔是什么肢藐? 我笑而不...
    開封第一講書人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮孽尽,結(jié)果婚禮上窖壕,老公的妹妹穿的比我還像新娘。我一直安慰自己杉女,他們只是感情好瞻讽,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著熏挎,像睡著了一般速勇。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上坎拐,一...
    開封第一講書人閱讀 51,679評(píng)論 1 305
  • 那天烦磁,我揣著相機(jī)與錄音养匈,去河邊找鬼。 笑死都伪,一個(gè)胖子當(dāng)著我的面吹牛呕乎,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播陨晶,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼猬仁,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了先誉?” 一聲冷哼從身側(cè)響起湿刽,我...
    開封第一講書人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎褐耳,沒想到半個(gè)月后诈闺,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡铃芦,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年雅镊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片杨帽。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡漓穿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出注盈,到底是詐尸還是另有隱情,我是刑警寧澤叙赚,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布老客,位于F島的核電站,受9級(jí)特大地震影響震叮,放射性物質(zhì)發(fā)生泄漏胧砰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一苇瓣、第九天 我趴在偏房一處隱蔽的房頂上張望尉间。 院中可真熱鬧,春花似錦击罪、人聲如沸哲嘲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)眠副。三九已至,卻和暖如春竣稽,著一層夾襖步出監(jiān)牢的瞬間囱怕,已是汗流浹背霍弹。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留娃弓,地道東北人典格。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像台丛,于是被迫代替她去往敵國(guó)和親钝计。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355