大家好,又見面了,我是你們的朋友全棧君。
寫在前面

目前大多數的APP都采用的是幾個Tab标簽以及多個界面滑動的形式來提供多層次的互動體驗,最為常用的做法就是采用TabLayout+ViewPager+Fragment的方式,最近在公司項目中遇到類似的界面,也看了各個論壇很多份部落格,但是發現都沒有完全把這種方法的坑填完,是以寫下這篇部落格,一方面是對知識的總結,另一方面也能讓其他開發者們少走一些彎路,部落格内容主要分為四個章節:
- TabLayout+ViewPager+Fragment的簡單用法總結。
- 所使用的兩種PagerAdapter的差别分析及選擇。
- 懶加載政策。
- 卡頓及性能優化建議。
一般情況下上面四個章節的内容足以應付過來,但是往往在一些特殊的情況下,仍然會遇到一些不能解決的問題,這時就需要深入到源碼之中來具體問題具體分析。話不多說,接下來将進行使用總結。
TabLayout+ViewPager+Fragment的用法
首先,需要引入工具包:
implementation 'com.android.support:design:27.1.1'
implementation 'com.android.support:support-v4:27.1.1'
複制
用法其實非常簡單,有點類似于RecyclerView,其中主要關心四個對象:Tablayout、ViewPager、PagerAdapter、Fragment。前兩個就跟普通的View控件一樣,可以直接通過XML來進行布局以及在onCreate擷取相應的執行個體:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".activities.TabLayoutActivity">
<android.support.design.widget.TabLayout android:id="@+id/tl_tabs" android:layout_width="match_parent" android:layout_height="40dp" />
<android.support.v4.view.ViewPager android:id="@+id/vp_content" android:layout_width="match_parent" android:layout_height="match_parent" />
</LinearLayout>
複制
Fragment建議采用v4相容包下的,我們所需要使用的Fragment是需要自己來實作,但是和普通的Fragment沒什麼差別,是以也就省略了Fragment的建立步驟,而PagerAdapter有兩種實作可以使用,具體會在下一小節介紹,TabLayout+ViewPager+Fragment方法的使用流程:
- 建立存儲多個Fragment執行個體的清單
- 建立PagerAdapter執行個體并關聯到Viewpager中
- 将ViewPager關聯到Tablayout中
- 根據需求改寫Tablayout屬性*
最後一步不是必須的,為了更加清楚地描述這個調用流程,貼上一個示意圖:
貼上代碼:
public class TabLayoutActivity extends AppCompatActivity implements MyFragment.OnFragmentInteractionListener {
TabLayout tabLayout;
ViewPager viewPager;
List<Fragment> fragments = new ArrayList<>();
List<String> titles = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_tab_layout);
tabLayout = findViewById(R.id.tl_tabs);
viewPager = findViewById(R.id.vp_content);
fragments.add(MyFragment.newInstance("11111", "11111"));
fragments.add(MyFragment.newInstance("22222", "22222"));
fragments.add(MyFragment.newInstance("33333", "33333"));
fragments.add(MyFragment.newInstance("44444", "44444"));
fragments.add(MyFragment.newInstance("55555", "55555"));
titles.add("fragment1");
titles.add("fragment2");
titles.add("fragment3");
titles.add("fragment4");
titles.add("fragment5");
viewPager.setAdapter(new FragmentStatePagerAdapter(getSupportFragmentManager()) {
@Override
public Fragment getItem(int position) {
return fragments.get(position);
}
@Override
public int getCount() {
return fragments.size();
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
super.destroyItem(container, position, object);
}
@Nullable
@Override
public CharSequence getPageTitle(int position) {
return titles.get(position);
}
});
tabLayout.setupWithViewPager(viewPager);
}
@Override
public void onFragmentInteraction(Uri uri) {
}
}
複制
getPageTitle(int position)函數是傳回目前TabLayout的标簽标題的,當然,也可以不通過PagerAdapter中的這個函數傳回,采用下面的這種方式也可行(有多少個就addTab多少次):
tabLayout.addTab(tabLayout.newTab().setText("tab 1"));
複制
PagerAdapter
PagerAdapter是一個抽象類,它有兩個實作子類供我們使用,分别是FragmentStatePagerAdapter和FragmentPagerAdapter。建立這兩個類的執行個體需要傳入一個FragmentManager對象,像代碼那樣處理就行了,從類名就可以看出來它倆的最大差别就在“State-狀态”上,什麼意思呢?指的是所包含存儲的Fragment對象的狀态是否儲存。看源碼可以發現,FragmentStatePagerAdapter中比FragmentPagerAdapter多元護着兩個清單:
private ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
private ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
複制
而這兩個清單帶來的最大差别則展現在void destroyItem(ViewGroup container, int position, Object object)這個函數之中,看下FragmentStatePagerAdapter的函數源碼:
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
Fragment fragment = (Fragment) object;
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
+ " v=" + ((Fragment)object).getView());
while (mSavedState.size() <= position) {
mSavedState.add(null);
}
mSavedState.set(position, fragment.isAdded()
? mFragmentManager.saveFragmentInstanceState(fragment) : null);
mFragments.set(position, null);
mCurTransaction.remove(fragment);
}
複制
再看下FragmentPagerAdapter的這個函數:
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object
+ " v=" + ((Fragment)object).getView());
mCurTransaction.detach((Fragment)object);
}
複制
具體情況就不再往下分析啦,還有一個坑等下再說。
ViewPager還有一個比較重要的函數是:
viewPager.setOffscreenPageLimit(int limit);
複制
這個方法預設值為1,Google在開發ViewPager時,考慮到如果滑動的時候才建立Fragment執行個體時會帶來一定程度的卡頓,是以為ViewPager設定了緩存機制,而上述函數則是設定緩存Fragment的數量,示意圖如下:
也就是說,limit的值代表着還要緩存目前Fragment左右各limit個Fragment,一共會建立2*limit+1個Fragment。超出這個limit範圍的Fragment就會被銷毀,而上述兩種PagerAdapter的差别就是銷毀的流程不同!
這裡就不放Log圖給大家看,直接告訴大家,FragmentPagerAdapter在銷毀Fragment時不會調用onDestroy()方法,而帶了State的Adapter則會調用Fragment的onDestroy()方法,換言之,前者僅僅是銷毀了Fragment的View視圖而沒有銷毀Fragment這個對象,但是後者則徹徹底底地消滅了Fragment對象,這是很重要的知識要點哦~!也是下面談性能優化和懶加載的前提條件。
本小節最後,告訴大家一個關于如何選擇PagerAdapter的結論:
FragmentPagerAdapter适用于Fragment比較少的情況,它會把每一個Fragment儲存在記憶體中,不用每次切換的時候,去儲存現場,切換回來在重新建立,是以使用者體驗比較好。而對于Fragment比較多的情況,需要切換的時候銷毀以前的Fragment以釋放記憶體,就可以使用FragmentStatePagerAdapter。
暫時不懂這句話的含義沒關系,請接着往下面看。
懶加載政策
Android的View繪制流程是最消耗CPU時間片的操作,尤其是在ViewPager緩存Fragment的情況下,如果在View繪建的同時還進行多個Fragment的資料加載,那使用者體驗簡直是爆炸(不僅浪費流量,而且還造成不必要的卡頓)。。。是以,需要對Fragment們進行懶加載政策。什麼是懶加載?就是被動加載,當Fragment頁面可見時,才從網絡加載資料并顯示出來。那什麼時候Fragment可見呢?Fragment之中有這樣一個函數:
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
doYourJobs();
}
複制
當Fragment的可見狀态發生變化時就會調用這個函數,boolean參數isVisibleToUser代表目前的Fragment是否可見。
如果這麼簡單地調用函數就能實作懶加載的話,那也沒什麼好說的,但是這裡又有一個巨坑,則是因為這個setUserVisibleHint函數是遊離在Fragment生命周期之外的,它的執行有可能早于onCreate和onCreateView,然而既然要時間資料的加載,就必須要在onCreateView建立完視圖過後才能使用,不然就會傳回空指針崩潰,懶加載的重點也是在這兒,那麼我們來分析,實行懶加載必須滿足哪些條件呢?
1.View視圖加載完畢,即onCreateView()執行完成
2.目前Fragment可見,即setUserVisibleHint()的參數為true
3.初次加載,即防止多次滑動重複加載
複制
有了這兩個條件過後,便能夠正常執行懶加載過程,我們在Fragment全局變量之中增加對應的三個标志參數并賦上初始值:
boolean mIsPrepare = false; //視圖還沒準備好
boolean mIsVisible= false; //不可見
boolean mIsFirstLoad = true; //第一次加載
複制
當然在onCreateView中確定了View已經準備好時,将mPrepare置為true,在setUserVisibleHint中確定了目前可見時,mIsVisible置為true,第一次加載完畢後則将mIsFirstLoad置為false,避免重複加載。
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mIsPrepare = true;
lazyLoad();
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
//isVisibleToUser這個boolean值表示:該Fragment的UI 使用者是否可見
if (isVisibleToUser) {
mIsVisible = true;
lazyLoad();
} else {
mIsVisible = false;
}
}
複制
最後,貼上懶加載的lazyLoad()代碼:
一定要記住,隻要标志位改變,就要進行lazyLoad()函數的操作
private void lazyLoad() {
//這裡進行三個條件的判斷,如果有一個不滿足,都将不進行加載
if (!mIsPrepare || !mIsVisible||!mIsFirstLoad) {
return;
}
loadData();
//資料加載完畢,恢複标記,防止重複加載
mIsFirstLoad = false;
}
private void loadData() {
//這裡進行網絡請求和資料裝載
}
複制
當然,在最後,如果Fragment銷毀的話,還應該将三個标志位進行預設值初始化:
@Override
public void onDestroyView() {
super.onDestroyView();
mIsFirstLoad=true;
mIsPrepare=false;
mIsVisible = false;
}
複制
為什麼在onDestroyView中進行而不是在onDestroy中進行呢?這又要提到之前Adapter的差異,onDestroy并不一定會調用,讀者可以思考思考為什麼。
卡頓及性能優化建議
Fragment的加載最為耗時的步驟主要有兩個,一個是Fragment建立(尤其是建立View的過程),另一個就是讀取資料填充到View上的過程。懶加載能夠解決後者所造成的卡頓,但是針對前者來說,并沒有效果。
Google為了避免使用者因翻頁而造成卡頓,采用了緩存的形式,但是其實緩不緩存,隻要該Fragment會顯示,都會進行Fragment建立,都會耗費相應的時間,換言之,緩存隻不過将本應該在翻頁時的卡頓集中在啟動該Activity的時候一起卡頓。
優化方案一:設定緩存頁面數
viewPager.setOffscreenPageLimit(int limit)
能夠有效地一次性緩存多個Fragment,這樣就能夠解決在之後每次切換時不會建立執行個體對象,看起來也會流暢。但是這樣的做法,最大的缺點就是容易造成第一次啟動時非常緩慢!如果第一次啟動時間滿足要求的話,就使用這種簡單地辦法吧。
優化方案二:避免Fragment的銷毀
不管是FragmentStatePagerAdapter還是FragmentPagerAdapter,其中都有一個方法可以被覆寫:
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
// super.destroyItem(container, position, object);
}
複制
把中間的代碼注釋掉就行了,這樣就可以避免Fragment的銷毀過程,一般情況下能夠這樣使用,但是容易出現一個問題,我們再來看看FragmentStatePagerAdapter的源碼:
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
Fragment fragment = (Fragment) object;
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
+ " v=" + ((Fragment)object).getView());
while (mSavedState.size() <= position) {
mSavedState.add(null);
}
mSavedState.set(position, fragment.isAdded()
? mFragmentManager.saveFragmentInstanceState(fragment) : null);
mFragments.set(position, null);
mCurTransaction.remove(fragment);
}
複制
看到沒?這個過程之中包含了對FragmentInstanceState的儲存!這也是FragmentStatePagerAdapter的精髓之處,如果注釋掉,一旦Activity被回收進入異常銷毀狀态,Fragment就無法恢複之前的狀态,是以這種方法也是有纰漏和局限性的。FragmentPagerAdapter的源代碼就留給大家自己去研究分析,也會發現一些問題的哦。
優化方案三:避免重複建立View
優化Viewpager和Fragment的方法就是盡可能地避免Fragment頻繁建立,當然,最為耗時的都是View的建立。是以更加優秀的優化方案,就是在Fragment中緩存自身有關的View,防止onCreateView函數的頻繁執行,我就直接上源碼了:
public class MyFragment extends Fragment {
View rootView;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
if (rootView == null) {
rootView = inflater.inflate(R.layout.fragment_my, container, false);
}
return rootView;
@Override
public void onDestroyView() {
super.onDestroyView();
Log.d(TAG, "onDestroyView: " + mParam1);
mIsFirstLoad=true;
mIsPrepare=false;
mIsVisible = false;
if (rootView != null) {
((ViewGroup) rootView.getParent()).removeView(rootView);
}
}
複制
onCreateView中将會對rootView進行null判斷,如果為null,說明還沒有緩存目前的View,是以會進行過緩存,反之則直接利用。當然,最為重要的是需要在
onDestroyView()
方法中及時地移除rootView,因為每一個View隻能擁有一個Parent,如果不移除,将會重複加載而導緻程式崩潰。
其實ViewPager+Fragment的方式,ViewPager中顯示的就是Fragment中所建立的View,Fragment隻是一個控制器,并不會直接顯示于ViewPager之中,這一點容易被忽略。
暫時想到的優化方案就隻有這麼多了。
總結
本文主要講述兩個部分的知識:三駕馬車實作切頁展示的基礎方法以及如何優化性能表現和避免卡頓。其中,對于ViewPager+Fragment體系的卡頓原因進行了分析,也主要有兩個方面:建立Framgent執行個體(建立View)和資料加載導緻卡頓。後者卡頓通過懶加載的形式能夠完美解決,而前者因執行個體建立引起的卡頓則提出了三種不同的優化選擇,應該說,每一種方案都有利有弊,并沒有絕對的好與不好,在項目運用中,還是得根據需求和實際情況來進行選擇,當然,要從記憶體洩漏、卡頓時間、容錯率等多個方面來綜合考量。不過話說回來,最優的優化方案還是盡可能的精簡自己的View布局。
總之,Fragment是Android中最為重要的知識點之一,我在總結本部落格的過程之中也有很大的收獲,多看源碼了解問題的根源過後再對症下藥,不失為一種程式員的基本素養。
歡迎關注我的部落格
釋出者:全棧程式員棧長,轉載請注明出處:https://javaforall.cn/163203.html原文連結:https://javaforall.cn