你的ViewPager八成用錯了

前言

創(chuàng)作過程:2020年5月22下午4點左右開始寫洞翩,晚上9點55寫下尾聲。晚上11點-12點補充第五焰望、第六部分骚亿。

有段時間沒寫文章了,這次不是因為懶...而是的確很忙熊赖,最近在重構(gòu)項目里的一個重要模塊捌浩。

搞起來真的酸爽右犹,為了策應(yīng)其他組的模塊化,重構(gòu)的時候也進行了我們的模塊化處理,混亂的依賴也是x了狗了....

今天的文章內(nèi)容是關(guān)于ViewPager的掉分,很多同學(xué)可能會吐槽:怎么還寫這種“低級”的內(nèi)容赴恨!為什么晤郑?因為絕大多數(shù)的同學(xué)都用錯了负蚊,當(dāng)然這主要的原因是搜索引擎推出來的文章大多都是錯的!

本章的續(xù)章已經(jīng)出爐:你的ViewPager八成用錯了(2)內(nèi)存泄漏浆兰?內(nèi)存溢出磕仅?

正文

一、錯誤用法

不知道有多少同學(xué)是這樣用ViewPager的簸呈?

class TestViewPagerActivity : BaseActivity() {
    private lateinit var adapter: ViewPagerAdapter
    private val fragments = mutableListOf<Fragment>().apply {
        add(TestFragment1.newInstance("頁面-1")
        add(TestFragment2.newInstance("頁面-2"))
        add(TestFragment3.newInstance("頁面-3"))
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_test_view_pager)
        adapter = ViewPagerAdapter(fragmentData, supportFragmentManager)
        vp.adapter = adapter
    }

    inner class ViewPagerAdapter(val fragments: List<Fragment>, fm: FragmentManager) : FragmentPagerAdapter(fm) {
        override fun getItem(position: Int): Fragment {
            return fragments[position]
        }

        override fun getCount(): Int {
            return fragments.size
        }
    }
}

如果看到這的同學(xué)覺得這個用法沒什么問題榕订。那么毫無疑問這篇文章你必須要讀一讀,因為上述的用法完全曲解的Fragment在ViewPager中的應(yīng)用蝶棋。

二卸亮、正確用法

我猜有同學(xué)可能有疑問了,那正確用法是什么樣呢?

當(dāng)然有同學(xué)反駁:憑什么你說你的寫法是對的呢玩裙?這還用問嗎?還不是因為我大6沃薄3越Α!....Google的文檔了:ViewPager

class TestViewPagerActivity : BaseActivity() {
    private lateinit var adapter: ViewPagerAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_test_view_pager)
        adapter = ViewPagerAdapter(fragmentData, supportFragmentManager)
        vp.adapter = adapter
    }

    inner class ViewPagerAdapter(fm: FragmentManager) : FragmentPagerAdapter(fm) {
        override fun getItem(position: Int): Fragment {
            return when (position) {
                0 -> TestFragment1.newInstance("頁面-1")
                1 -> TestFragment2.newInstance("頁面-2")
                else -> TestFragment3.newInstance("頁面-3")
            }
        }

        override fun getCount(): Int {
            return 3
        }
    }
}

大家看出這倆種用法的不同了嗎鸯檬?沒錯不同點只在于getItem()方法的實現(xiàn)决侈。搞懂getItem()的調(diào)用,也就搞懂了Fragment在ViewPager里的正確用法喧务。所以接下來咱們直接上源碼直觀感受ViewPager的設(shè)計 赖歌。

三、FragmentPagerAdapter源碼

ViewPager對Fragment的支持非常的簡單功茴,整體流程:

    1. setAdapter時會基于當(dāng)前position進行初始化當(dāng)前Fragment
    1. 接下來會基于mOffscreenPageLimit的值對需要“預(yù)加載”的Fragment進行初始化
    1. 初始化該初始化的Fragment之后庐冯,調(diào)用commit()通知FragmentManager去attach Fragment

這3步走完,我們當(dāng)前的Fragment就已經(jīng)出來了坎穿。

接下來咱們通過源碼來具體理解一下上述的1展父、2返劲、3這幾個步驟。

當(dāng)我們setAdapter時栖茉,會走到popuate方法:

void populate(int newCurrentItem) { // ViewPager中
    // ....
    // 基于當(dāng)前position的位置判斷Item(Fragment)是否存在來決定篮绿,是否要初始化當(dāng)前的Fragment
    if (curItem == null && N > 0) {
        // 而這里會走到instantiateItem
        curItem = addNewItem(mCurItem, curIndex);
    }
    // 初始化當(dāng)前之后,會基于limit吕漂,初始化該預(yù)加載的....
    // 此方法在FragmentPagerAdapter中會調(diào)用fm的commit
    mAdapter.finishUpdate(this);
}

/**
 * 這里會調(diào)用instantiateItem()亲配,這里真正的實現(xiàn)在FragmentPagerAdapter里
 */
ItemInfo addNewItem(int position, int index) {
    ItemInfo ii = new ItemInfo();
    ii.position = position;
    ii.object = mAdapter.instantiateItem(this, position);
    ii.widthFactor = mAdapter.getPageWidth(position);
    if (index < 0 || index >= mItems.size()) {
        mItems.add(ii);
    } else {
        mItems.add(index, ii);
    }
    return ii;
}

一直走到這,我們才看到FragmentPagerAdapter對Fragment初始化的控制:

public Object instantiateItem(@NonNull ViewGroup container, int position) {
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }
    // 基于position找到itemId惶凝,這方法的默認實現(xiàn)就是position
    final long itemId = getItemId(position);

    // 生成一個tag
    String name = makeFragmentName(container.getId(), itemId);
    // 通過上邊生成的tag弃榨,在fragmentManager中試圖找到一個Fragment的實例
    Fragment fragment = mFragmentManager.findFragmentByTag(name);
    // 如果找到,直接調(diào)用attach
    if (fragment != null) {
        if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
        mCurTransaction.attach(fragment);
    } else {
        // 否則調(diào)用getItem()梨睁,基于我們自己的實現(xiàn)拿到Fragment實例鲸睛。
        fragment = getItem(position);
        if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
        mCurTransaction.add(container.getId(), fragment,
                makeFragmentName(container.getId(), itemId));
    }
    if (fragment != mCurrentPrimaryItem) {
        fragment.setMenuVisibility(false);
        if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
            mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
        } else {
            fragment.setUserVisibleHint(false);
        }
    }
    return fragment;
}

代碼的注釋詳細的說明了FragmentPagerAdapter如果基于當(dāng)前position進行初始化Fragment的邏輯。簡單再梳理一遍:

  • 基于一套規(guī)則生成的tag坡贺,通過findFragmentByTag()來找是否已經(jīng)生成過Fragment官辈。
  • 如果沒有,調(diào)用getItem()遍坟,拿到我們自己重寫后return的Fragment實例拳亿。

因為以上的流程,我們可以明確開篇第一種用法一定錯誤的愿伴!因為從源碼我們可以get到一個信息:對于Adapter來說肺魁,只有FragmentManage中找不到Fragment實例時才會調(diào)用getItem()去初始化Fragment。因此這其實是一種常見的懶加載機制隔节。

而開篇第一種寫鹅经,在初始化的時候就把所有Fragment都new了一遍,很明顯是無意義的怎诫!因為如果我們ViewPager有3個Fragment瘾晃,用戶不滑到第3個Fragment,那么new這個Fragment就是浪費的幻妓。

接下來咱們再聊一聊第2步中的mOffscreenPageLimit蹦误,有經(jīng)驗的老鐵們都知道這個是用于預(yù)加載的,而且這個值最低是1肉津。populate()方法中基于mOffscreenPageLimit來決定預(yù)加載position左右倆邊多少個Fragment强胰,1就意味著左右各預(yù)加載1個。

由于mOffscreenPageLimit最小是1的原因妹沙,所以我們一次至少要加載2個Fragment偶洋。而有時我們又偏偏需要在滑動到某個Fragment的時候再執(zhí)行一些數(shù)據(jù)加載的操作。

在面對這種場景下初烘,我們一般都會用onHiddenChanged()/setUserVisibleHint()等方法來嘗試做可見性的邏輯回調(diào)涡真。其實如果項目中的fragment庫版本較新的時候會發(fā)現(xiàn)系統(tǒng)提供了更方便且優(yōu)雅的方式分俯。

四、更優(yōu)雅的滑動到當(dāng)前Fragment時加載數(shù)據(jù)

新版本下的fragment哆料,在使用FragmentStatePagerAdapter缸剪,我們會發(fā)現(xiàn)默認的構(gòu)造方法是過時的:

@Deprecated
public FragmentStatePagerAdapter(@NonNull FragmentManager fm) {
    this(fm, BEHAVIOR_SET_USER_VISIBLE_HINT);
}

會發(fā)現(xiàn)系統(tǒng)在構(gòu)造函數(shù)中增加了第二個參數(shù),除了默認BEHAVIOR_SET_USER_VISIBLE_HINT的东亦,系統(tǒng)還提供了BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT杏节。而這個行為和它的名字一樣,只有在滑動這個Fragment上時才會調(diào)這個Fragment的onResume()方法典阵。

但是注意是回調(diào)onResume()奋渔。而onResume之前的方法,已經(jīng)在getItem()中實例化Fragment的時候調(diào)完了壮啊。

因此我們僅僅想在當(dāng)前Fragment可見的時候做初始化操作嫉鲸,可以直接使用BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT。

五歹啼、getItemPosition()濫用

前段時間在公司項目中玄渗,看到有小伙伴重寫了getItemPosition()方法:

public int getItemPosition(@NonNull Object object) {
    return POSITION_NONE;
}

這么寫有沒有問題?說有也有狸眼,說沒有也沒有藤树!為什么這么模棱兩可的回答呢?因此這個方法很特殊拓萌。

這個方法的注釋是這么說的:return POSITION_UNCHANGED時岁钓,意味著當(dāng)前視圖沒有發(fā)生改變,return POSITION_NONE意味著發(fā)生改變微王。注釋可能有些抽象屡限,咱們結(jié)合源碼來理解這個方法。

這個方法只會在ViewPager的dataSetChanged()中被調(diào)用骂远,因此我們可以確認重寫這個方法只會在主動嘗試更新ViewPager時生效囚霸。

void dataSetChanged() {
    // for循環(huán)所有Fragment,然后基于getItemPosition()返回值判斷是否需要remove
    for (int i = 0; i < mItems.size(); i++) {
        final ItemInfo ii = mItems.get(i);
        final int newPos = mAdapter.getItemPosition(ii.object);

        if (newPos == PagerAdapter.POSITION_UNCHANGED) {
            continue;
        }

        if (newPos == PagerAdapter.POSITION_NONE) {
            // 可以看到激才,如果是POSITION_NONE,就會remove當(dāng)前i下的Fragment
            // 省略部分代碼
            mItems.remove(i);          
            mAdapter.destroyItem(this, ii.position, ii.object);
        }
        // 省略部分代碼
    }
    // 省略部分代碼
    // 此方法中會再次調(diào)用populate()去重新走初始化的操作
    setCurrentItemInternal(newCurrItem, false, true);
}

有了上述源碼的邏輯额嘿,其實我們就能夠明白getItemPosition()的意義:當(dāng)我們想使用notifyDataSetChanged()去刷新ViewPager時瘸恼,getItemPosition()的返回時決定當(dāng)前的Fragment是否需要被remove。因此當(dāng)我們不需要remove當(dāng)前的Fragment時册养,則return POSITION_UNCHANGED(這樣此Fragment就不會發(fā)生任何狀態(tài)變化)东帅,否者則return POSITION_NONE(這樣此Fragment就會被remove,然后重新初始化新的Fragment)球拦。我們就可以做出類似于RecyclerView的diff操作靠闭。

基于自身產(chǎn)品邏輯帐我,合理的重寫getItemPosition(),避免不必要Fragment的銷毀重建愧膀。

六拦键、如何主動get到ViewPager的Fragment實例

我們都知道,F(xiàn)ragmentManager為我們提供了findFragmentById()/findFragmentByTag()檩淋。同樣對于ViewPager也是如此芬为,在第三部分源碼分析的時候,我們知道FragmentPagerAdapter中獲取也是通過findFragmentByTag()嘗試獲取當(dāng)前Fragment的實例蟀悦,而tag的實現(xiàn)來自makeFragmentName(container.getId(), itemId)

private static String makeFragmentName(int viewId, long id) {
    return "android:switcher:" + viewId + ":" + id;
}

所以媚朦,我們獲取ViewPager中的Fragment也可以借助這種方式。千萬不要像搜索引擎里推出的那些答案:主動調(diào)用什么getItem()日戈!有了上邊源碼的分析询张,我猜大家已經(jīng)get到這些用法錯的是多么離譜!U懔丁份氧!

尾聲

OK,本次想聊的就是這么多~以后的文章鼓拧,我會力求在絕對正確的情況下再發(fā)出來半火,盡可能的不要誤人子弟!

畢竟就今天的ViewPager而言季俩,其實我一開始也是用那種錯誤的寫法钮糖,沒錯,就是受搜索引擎推出來的錯誤文章所誤導(dǎo)酌住!

既然自己踩過坑店归,爭取能填上一個是一個!

我是一個應(yīng)屆生酪我,最近和朋友們維護了一個公眾號消痛,內(nèi)容是我們在從應(yīng)屆生過渡到開發(fā)這一路所踩過的坑,以及我們一步步學(xué)習(xí)的記錄都哭,如果感興趣的朋友可以關(guān)注一下秩伞,一同加油~

個人公眾號:咸魚正翻身
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市欺矫,隨后出現(xiàn)的幾起案子纱新,更是在濱河造成了極大的恐慌,老刑警劉巖穆趴,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件脸爱,死亡現(xiàn)場離奇詭異,居然都是意外死亡未妹,警方通過查閱死者的電腦和手機簿废,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進店門空入,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人族檬,你說我怎么就攤上這事歪赢。” “怎么了导梆?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵轨淌,是天一觀的道長。 經(jīng)常有香客問我看尼,道長递鹉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任藏斩,我火速辦了婚禮躏结,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘狰域。我一直安慰自己媳拴,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布兆览。 她就那樣靜靜地躺著屈溉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪抬探。 梳的紋絲不亂的頭發(fā)上子巾,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天,我揣著相機與錄音小压,去河邊找鬼线梗。 笑死,一個胖子當(dāng)著我的面吹牛怠益,可吹牛的內(nèi)容都是我干的仪搔。 我是一名探鬼主播,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼蜻牢,長吁一口氣:“原來是場噩夢啊……” “哼烤咧!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起抢呆,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤髓削,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后镀娶,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡揪罕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年梯码,在試婚紗的時候發(fā)現(xiàn)自己被綠了宝泵。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,427評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡轩娶,死狀恐怖儿奶,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情鳄抒,我是刑警寧澤闯捎,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布,位于F島的核電站许溅,受9級特大地震影響瓤鼻,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜贤重,卻給世界環(huán)境...
    茶點故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一茬祷、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧并蝗,春花似錦祭犯、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至键畴,卻和暖如春最盅,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背镰吵。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工檩禾, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人疤祭。 一個月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓盼产,卻偏偏與公主長得像,于是被迫代替她去往敵國和親勺馆。 傳聞我的和親對象是個殘疾皇子戏售,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,440評論 2 359