前言
寫作記錄: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ù)量。比如這樣:
當(dāng)然量没,這樣可怕么玉转?只能說(shuō)不一定,因?yàn)檫@里僅僅是持有了Fragment的實(shí)例殴蹄,并不會(huì)包含View究抓。(只要不屬于add狀態(tài)的Fragment的View是為null的):
因此常規(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)存:
并且無(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
二穷娱、小總結(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)答案幸斥!