錯誤的ViewPager用法(續),會產生內存泄漏?內存溢出?

前言

寫做記錄:5月27日晚上寫下第一版,30日下午補充一些內容...結束java

前幾天發佈了一篇文章:你的ViewPager八成用錯了。關於分析FragmentPagerAdapter的...沒想到引發個各路英雄豪傑的激烈討論。這其中有兩個頗有意義的點:android

  • 一、錯誤的第一種用法引起內存泄漏(不許確)。
  • 二、FragmentStatePagerAdapter在FragmentPagerAdapter基礎上作了什麼。

今天這篇文章,我們就來聊一聊上面倆個話題。緩存

如下源碼基於:implementation "androidx.fragment:fragment:1.2.0"ide

正文

錯誤的用法引發內存泄漏。佈局

說實話,我其實的確沒有留意過這個點。當評論中的同窗提到這一點的時候,我想了想彷佛能夠「說得通」:Activity相對較Fragment,應該生命週期會更長,若是在Activity直接強引用全部的Fragment的實例。按理說的確會有泄漏問題。post

不過這個結論的前提是基於:Activity比Fragment生命週期更長,若是不是這樣的話,也談不上存在內存泄漏。因此爲了求證這個結論我們仍是從源碼中一探究竟。學習

1、內存泄漏?

首先可以肯定的是,不管正誤用法都不會存在內存泄漏問題。spa

可是會有可能存在內存溢出,而且錯誤的寫法更容易出現。而其實咱們線上場景也遇到過這類問題,當時咱們是有30+個Fragment,而後在低端手機上爆出了不少這樣的crash:設計

java.lang.RuntimeException: android.os.TransactionTooLargeException: data parcel size 3117432 bytes 這個crash出現的緣由,下文會展開。代理

1.一、FragmentManager怎麼初始化的

接下來我們聊一下爲何不會出現內存泄漏。

首先我們都知道Fragment是由FragmentManager管理的,那我們就基於這個共識一塊兒來看一看源碼:

一般我們這樣從一個Activity中拿到FragmentManager:activity.supportFragmentManager。那我們就順着這個調用,看一看FM是如何初始化的。

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

@NonNull
public FragmentManager getSupportFragmentManager() {
    return mFragments.getSupportFragmentManager();
}
複製代碼

能夠看出對外提供FragmentManager的FragmentController類是在Activity裏直接被new出來的,而FragmentController中提供FM是這樣的:

private final FragmentHostCallback<?> mHost;

@NonNull
public FragmentManager getSupportFragmentManager() {
    return mHost.mFragmentManager;
}
複製代碼

mHost就是我們new FragmentController時候傳進來的new HostCallbacks()。而HostCallbacks中的FM又是怎麼來的呢?

final FragmentManager mFragmentManager = new FragmentManagerImpl();
複製代碼

能夠看到直接是new出來的。所以這裏咱們就能明確了,其實Activity是強引用了FM。只要Activity不被回收,那麼FM就不會被回收,那麼FM中的Fragment也就不會被回收。那麼也就有了上面的結論:你們生命週期同樣長,其實談不上什麼內存泄漏。

1.二、Google如何幫咱們緩存Fragment

可是,我們上邊提到過,雖然沒有內存泄漏,可是存在內存溢出!那麼這又是誰的鍋呢?此次我們能夠放心,這個鍋還真不是我們開發者的問題 !沒錯,這口鍋必須得穩穩的扣在Google頭上!來我們看源碼:

我們平常獲取Fragment實例都是基於FragmentManager的find()系列方法,我們就從這個方法來看一看FM若是保存我們的Fragment實例:

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

public Fragment findFragmentById(@IdRes int id) {
    return mFragmentStore.findFragmentById(id);
}
複製代碼

真正的實現是代理到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;
}
複製代碼

能夠看出這裏是經過倆個集合去find,分別是mAdded、mActive。

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

1.三、什麼樣的Fragment進到mAdded集合

mAdded這個List會存儲attach上的Fragment,所以它不會有不少(若是咱們的mOffscreenPageLimit=1),那麼這個集合的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中有四種可能調用,對於addFragment()來講,FM會在OP_ADD、OP_ATTACH時調用,源碼分別以下:

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;
複製代碼

有了第一篇文章的基礎,我們明白對於FragmentPageradapter來講find不到Fragment,就會調用getItem()去new Fragment而後add,也就是走到OP_ADD。不然直接attach走OP_ATTACH,這裏種狀態都會走到mAdded的add。既然我們看到了add,那麼一樣對這兩種狀態相對的就是remove:

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

很明顯的成對出現,所以這個集合問題不大,只要用法無誤這個集合就是恆等的。

1.四、什麼樣的Fragment進到mActive集合

接下來,我們把目光移到mActive上。掃遍整個FragmentStore會發現,mActive只有一個場景會將集合特定位置置爲null:

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

這是惟一一個能夠回收mActive的機會。不過這個方法只會在當前Fragment處於removing纔會調用:

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是始終增長的!

實際也是如此,當咱們滑光全部的Fragmet,會發現mActive的數量之和就是全部Fragment的數量。好比這樣:

固然,這樣可怕麼?只能說不必定,由於這裏僅僅是持有了Fragment的實例,並不會包含View。(只要不屬於add狀態的Fragment的View是爲null的):

所以常規狀況下Fragment實例並不怎麼佔內存,畢竟此View上的內存是會被回收掉的。所以若是咱們不在Fragment中強引用一些其餘大內存對象,問題也不大...可是事實卻與之相反,咱們很容易在Fragment中留下大量成員變量,好比:

  • 一、爲了減小布局inflate的時間,咱們去緩存View。
  • 二、爲了緩存一些數據,咱們在Fragment中保留大量成員變量。

可是,咱們話說回來,這樣的操做有毛病麼?我的以爲沒毛病。可是在Google的這種設計下,那就很容易出問題。下面我們模擬一個這種case下出現OOM的場景:在Fragment上開闢一些大內存對象:

val array = IntArray(1024 * 1024 * 10)
複製代碼

當我滑動到第6個的Fragment時,崩了... java.lang.OutOfMemoryError: Failed to allocate a 41943052 byte allocation with 6959760 free bytes and 6MB until OOM

咱們dump一下內存:

而且不管咱們如何強制GC,都沒法回收這個內存。所以這也就是驗證了咱們上邊的問題,出問題的自己在於Google的機制,而壓死這個機制的最後一根稻草在於Fragment中的「濫用」。

其實咱們也不用太擔憂,這畢竟是極端狀況。不過當咱們的場景須要大量的Fragment時,是須要認真考慮這部分聊的問題。

那麼問題來了,這個坑點Google知道嗎?答案是知道,因此纔有了FragmentStatePagerAdapter,以及後來的ViewPager2。

1.五、額外聊聊android.os.TransactionTooLargeException

android.os.TransactionTooLargeException,這個異常我在開篇提到過,官網也有單獨的介紹。有經驗的老司機應該都遇到過,這個異常自己彷佛和我們今天聊的話題沒有直接關係。

可是我們上述聊的內容,很容易形成這個Exception。你們有興趣能夠作一下這個操做:

  • 一、把ViewPager的個數弄的很大,而後滑到最後。
  • 二、開發者選項裏打開 不保留活動
  • 三、按home鍵

八成會出現這個異常...若是遇不到,繼續加大ViewPager的個數!

我們這種場景出現這個問題:本質的緣由在於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()的時候是會保存mAdded集合和mActive集合的...我們剛纔也已經分析過去,mActive集合是一個全量數據集。因此Fragment足夠多,這裏的Parcel在傳遞的過程當中就爆炸了。

這裏我們引伸一下,Binder在通訊的過程當中最大的數據量是多少呢?官網給出的答案是:1M

2、小總結

我們第二部分聊的內容,其實只有在極端狀況下出現。平常開發時,咱們八成遇不到這種場景。可是當咱們瞭解了這些內容,就能夠在遇到這類問題時準確的預防或者根治。

固然正是由於這種種的緣由,也就有了後續的FragmentStatePagerAdapter甚至ViewPager2。

尾聲

本是這篇文章開始是想把FragmentStatePagerAdapter一併聊了...可是寫完這一部分的時候發現篇幅已經足夠長了,爲了不你們「消化不良」。後續的內容我們下一篇文章再聊。

整起來,我還能學!

仍是那個原則:我會力求把文章寫到我認爲正確爲止,所以因爲我的水平有限,不免出現紕漏,歡迎你們一塊兒討論,一塊兒共建標準答案

我是一個應屆生,最近和朋友們維護了一個公衆號,內容是咱們在從應屆生過渡到開發這一路所踩過的坑,以及咱們一步步學習的記錄,若是感興趣的朋友能夠關注一下,一同加油~

我的公衆號:鹹魚正翻身
相關文章
相關標籤/搜索