前言
創(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的支持非常的簡單功茴,整體流程:
- setAdapter時會基于當(dāng)前position進行初始化當(dāng)前Fragment
- 接下來會基于mOffscreenPageLimit的值對需要“預(yù)加載”的Fragment進行初始化
- 初始化該初始化的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)酌住!
既然自己踩過坑店归,爭取能填上一個是一個!