寫做記錄:5月27日晚上寫下第一版,30日下午補充一些內容...結束java
前幾天發佈了一篇文章:你的ViewPager八成用錯了。關於分析FragmentPagerAdapter的...沒想到引發個各路英雄豪傑的激烈討論。這其中有兩個頗有意義的點:android
今天這篇文章,我們就來聊一聊上面倆個話題。緩存
如下源碼基於:implementation "androidx.fragment:fragment:1.2.0"ide
錯誤的用法引發內存泄漏。佈局
說實話,我其實的確沒有留意過這個點。當評論中的同窗提到這一點的時候,我想了想彷佛能夠「說得通」:Activity相對較Fragment,應該生命週期會更長,若是在Activity直接強引用全部的Fragment的實例。按理說的確會有泄漏問題。post
不過這個結論的前提是基於:Activity比Fragment生命週期更長,若是不是這樣的話,也談不上存在內存泄漏。因此爲了求證這個結論我們仍是從源碼中一探究竟。學習
首先可以肯定的是,不管正誤用法都不會存在內存泄漏問題。spa
可是會有可能存在內存溢出,而且錯誤的寫法更容易出現。而其實咱們線上場景也遇到過這類問題,當時咱們是有30+個Fragment,而後在低端手機上爆出了不少這樣的crash:設計
java.lang.RuntimeException: android.os.TransactionTooLargeException: data parcel size 3117432 bytes 這個crash出現的緣由,下文會展開。代理
接下來我們聊一下爲何不會出現內存泄漏。
首先我們都知道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也就不會被回收。那麼也就有了上面的結論:你們生命週期同樣長,其實談不上什麼內存泄漏。
可是,我們上邊提到過,雖然沒有內存泄漏,可是存在內存溢出!那麼這又是誰的鍋呢?此次我們能夠放心,這個鍋還真不是我們開發者的問題 !沒錯,這口鍋必須得穩穩的扣在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<>();
複製代碼
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;
複製代碼
很明顯的成對出現,所以這個集合問題不大,只要用法無誤這個集合就是恆等的。
接下來,我們把目光移到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中留下大量成員變量,好比:
可是,咱們話說回來,這樣的操做有毛病麼?我的以爲沒毛病。可是在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。
android.os.TransactionTooLargeException,這個異常我在開篇提到過,官網也有單獨的介紹。有經驗的老司機應該都遇到過,這個異常自己彷佛和我們今天聊的話題沒有直接關係。
可是我們上述聊的內容,很容易形成這個Exception。你們有興趣能夠作一下這個操做:
八成會出現這個異常...若是遇不到,繼續加大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
我們第二部分聊的內容,其實只有在極端狀況下出現。平常開發時,咱們八成遇不到這種場景。可是當咱們瞭解了這些內容,就能夠在遇到這類問題時準確的預防或者根治。
固然正是由於這種種的緣由,也就有了後續的FragmentStatePagerAdapter甚至ViewPager2。
本是這篇文章開始是想把FragmentStatePagerAdapter一併聊了...可是寫完這一部分的時候發現篇幅已經足夠長了,爲了不你們「消化不良」。後續的內容我們下一篇文章再聊。
整起來,我還能學!
仍是那個原則:我會力求把文章寫到我認爲正確爲止,所以因爲我的水平有限,不免出現紕漏,歡迎你們一塊兒討論,一塊兒共建標準答案!