1.簡介
在項目中有時會使用ViewPager展示頁面,在頁面較少時很簡單直接使用FragmentPagerAdapter即可滿足(非本文重點略過)。但是如果有很多幾十甚至幾百時,如果還像原來那么使用,就會出現界面卡,內存占用高,甚至會出現OOM問題(不信的話自己可以嘗試下)。那么要如何才能高效、低耗內存的實現呢?下面就重點來講講。
2.正確使用
2.1 準備
重寫系統FragmentStatePagerAdapter類,為什么要重新呢?因為系統源碼中緩存采用的是ArrayList進行數據緩存,當Frgment數量過大時會出現錯誤,所以這里進行了重寫,采用LongSparseArray對Fragment和SaveState進行緩存,代碼如下:
package com.zhh.commonview.adapter;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.util.LongSparseArray;
import android.support.v4.view.PagerAdapter;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
/**
* Created by zhanghai on 2020/3/9.
*
* function:
* ......
* 由于篇幅注釋忽略,參考系統的FragmentStatePagerAdapter注釋
*/
public abstract class BaseFragmentStatePagerAdapter extends PagerAdapter {
private static final String TAG = "FragmentPagerAdapter";
@NonNull
private final FragmentManager mFragmentManager;
@Nullable
private FragmentTransaction mCurTransaction = null;
@Nullable
private Fragment mCurrentPrimaryItem = null;
@NonNull
private final LongSparseArray<Fragment> mFragments = new LongSparseArray<>();
@NonNull
private final LongSparseArray<Fragment.SavedState> mSavedStates = new LongSparseArray<>();
public BaseFragmentStatePagerAdapter(@NonNull FragmentManager fm) {
mFragmentManager = fm;
}
/**
* Clear cache data.
*/
public void clear(){
if(mFragments != null && mFragments.size() > 0){
mFragments.clear();
}
if(mSavedStates != null && mSavedStates.size() > 0){
mSavedStates.clear();
}
}
/**
* Return the Fragment associated with a specified position.
*/
public abstract Fragment getItem(int position);
/**
* 獲取指定position的fragment
* @return
*/
public Fragment getCurrentPrimaryItem(int position){
long tag = getItemId(position);
return mFragments.get(tag);
}
@Override
public void startUpdate(@NonNull ViewGroup container) {
if (container.getId() == View.NO_ID) {
throw new IllegalStateException("ViewPager with adapter " + this
+ " requires a view id");
}
}
@Override
@NonNull
public Object instantiateItem(ViewGroup container, int position) {
long tag = getItemId(position);
Fragment fragment = mFragments.get(tag);
// If we already have this item instantiated, there is nothing
// to do. This can happen when we are restoring the entire pager
// from its saved state, where the fragment manager has already
// taken care of restoring the fragments we previously had instantiated.
if (fragment != null) {
return fragment;
}
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
fragment = getItem(position);
// restore state
final Fragment.SavedState savedState = mSavedStates.get(tag);
if (savedState != null) {
fragment.setInitialSavedState(savedState);
}
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
mFragments.put(tag, fragment);
mCurTransaction.add(container.getId(), fragment, "f" + tag);
return fragment;
}
@Override
public void destroyItem(ViewGroup container, int position, @NonNull Object object) {
Fragment fragment = (Fragment) object;
int currentPosition = getItemPosition(fragment);
int index = mFragments.indexOfValue(fragment);
long fragmentKey = -1;
if (index != -1) {
fragmentKey = mFragments.keyAt(index);
mFragments.removeAt(index);
}
//item hasn't been removed
if (fragment.isAdded() && currentPosition != POSITION_NONE) {
mSavedStates.put(fragmentKey, mFragmentManager.saveFragmentInstanceState(fragment));
} else {
mSavedStates.remove(fragmentKey);
}
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
mCurTransaction.remove(fragment);
}
@Override
public void setPrimaryItem(ViewGroup container, int position, @Nullable Object object) {
Fragment fragment = (Fragment) object;
if (fragment != mCurrentPrimaryItem) {
if (mCurrentPrimaryItem != null) {
mCurrentPrimaryItem.setMenuVisibility(false);
mCurrentPrimaryItem.setUserVisibleHint(false);
}
if (fragment != null) {
fragment.setMenuVisibility(true);
fragment.setUserVisibleHint(true);
}
mCurrentPrimaryItem = fragment;
}
}
@Override
public void finishUpdate(ViewGroup container) {
if (mCurTransaction != null) {
mCurTransaction.commitNowAllowingStateLoss();
mCurTransaction = null;
}
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return ((Fragment) object).getView() == view;
}
@Override
public Parcelable saveState() {
Bundle state = null;
if (mSavedStates.size() > 0) {
// save Fragment states
state = new Bundle();
long[] stateIds = new long[mSavedStates.size()];
for (int i = 0; i < mSavedStates.size(); i++) {
Fragment.SavedState entry = mSavedStates.valueAt(i);
stateIds[i] = mSavedStates.keyAt(i);
state.putParcelable(Long.toString(stateIds[i]), entry);
}
state.putLongArray("states", stateIds);
}
for (int i = 0; i < mFragments.size(); i++) {
Fragment f = mFragments.valueAt(i);
if (f != null && f.isAdded()) {
if (state == null) {
state = new Bundle();
}
String key = "f" + mFragments.keyAt(i);
mFragmentManager.putFragment(state, key, f);
}
}
return state;
}
@Override
public void restoreState(@Nullable Parcelable state, ClassLoader loader) {
if (state != null) {
Bundle bundle = (Bundle) state;
bundle.setClassLoader(loader);
long[] fss = bundle.getLongArray("states");
mSavedStates.clear();
mFragments.clear();
if (fss != null) {
for (long fs : fss) {
mSavedStates.put(fs, (Fragment.SavedState) bundle.getParcelable(Long.toString(fs)));
}
}
Iterable<String> keys = bundle.keySet();
for (String key : keys) {
if (key.startsWith("f")) {
Fragment f = mFragmentManager.getFragment(bundle, key);
if (f != null) {
f.setMenuVisibility(false);
mFragments.put(Long.parseLong(key.substring(1)), f);
} else {
Log.w(TAG, "Bad fragment at key " + key);
}
}
}
}
}
/**
* Return a unique identifier for the item at the given position.
* <p>
* <p>The default implementation returns the given position.
* Subclasses should override this method if the positions of items can change.</p>
*
* @param position Position within this adapter
* @return Unique identifier for the item at position
*/
public long getItemId(int position) {
return position;
}
}
邏輯基本跟系統的FragmentStatePagerAdapter邏輯一致。新建一個adapter類繼承該Adapter,并重寫下方法:
@Override
public int getCount() {
return mItems == null ? 0 : mItems.size();
}
@Override
public long getItemId(int position) {
return mItems == null || mItems.size() <=0 ? super.getItemId(position) : mItems.get(position).hashCode();
}
@Override
public int getItemPosition(Object object) {
if(object instanceof BaseMoreFragmentAdv){
//TODO:這里是重點
BaseMoreFragmentAdv item = (BaseMoreFragmentAdv) object;
int itemValue = item.getFragmentIdentifier();
for (int i = 0; i < mItems.size(); i++) {
if (mItems.get(i).hashCode() == itemValue) {
return i;
}
}
}
return POSITION_NONE;
}
其中關鍵點是getItemPosition方法,這里通過判斷標記唯一性去檢查是否新new一個fragment。item.getFragmentIdentifier()
是在Fragment中實現的一個方法,該方法是返回Fragment的唯一標識,主要可通過傳入的數據進行綁定,我這里是通過重寫類的hashCode方法與之綁定,確保唯一性。
2.2 Adapter使用
通過數據去創建Fragment,并且不自己進行維護一個List去存儲Fragment。
private void initPagers() {
final List<ObjectBean> list = new ArrayList<>();
for (int i = 0;i < 200;i++){
list.add(new ObjectBean(String.format(Locale.CHINESE,"fragment_%d",i)));
}
CommonViewPagerFragmentStateAdapter adapter = new CommonViewPagerFragmentStateAdapter<ObjectBean>(getSupportFragmentManager(),list) {
@Override
public Fragment getItem(final int position) {
ObjectBean bean = list.get(position);
TestFragment f = new TestFragment();
Bundle bundle = new Bundle();
bundle.putInt("identifier",bean.hashCode());
bundle.putString("index",bean.index);
f.setArguments(bundle);
return f;
}
};
vp_pager.setAdapter(adapter);
}
代碼很簡單,很容易理解。
針對Fragment多的情況下:
請勿先創建Fragment保存到List中,然后把該list傳給Adapter。
請勿先創建Fragment保存到List中,然后把該list傳給Adapter。
請勿先創建Fragment保存到List中,然后把該list傳給Adapter。
錯誤例子:
List<TestFragment> fragments = new ArrayList<>();
for (int i = 0;i < 200;i++){
TestFragment f = new TestFragment();
String index = String.format(Locale.CHINESE,"fragment_%d",i);
Bundle bundle = new Bundle();
bundle.putInt("identifier",index.hashCode());
bundle.putString("index",index);
f.setArguments(bundle);
fragments.add(f);
}
CommonViewPagerFragmentStateAdapter adapter = new CommonViewPagerFragmentStateAdapter<ObjectBean>(getSupportFragmentManager(),fragments);
至于原因,我想源碼中注釋的一句話最能說明。如下:
* <p>This version of the pager is more useful when there are a large number
* of pages, working more like a list view. When pages are not visible to
* the user, their entire fragment may be destroyed, only keeping the saved
* state of that fragment. This allows the pager to hold on to much less
* memory associated with each visited page as compared to
* {@link FragmentPagerAdapter} at the cost of potentially more overhead when
* switching between pages.
大致意思是說,當有大量頁數時它會跟有用,類似列表。當頁面對用戶不可見時,整個fragment會被銷毀,僅保留Fragment對應的SaveState數據,這樣就能保證占用內存低。
所以如果你自己存儲了一份Fragment list,那么就起不到低占內存的作用。
3 擴展
其實看到上面就已經能滿足ViewPager+Fragment使用了。
那么為什么還有個擴展呢?其實這里主要是想講講我在使用的過程碰到的問題。
1.TransactionTooLargeException異常
這個問題主要是由于使用Bundle傳遞到Fragment中造成的,Bundle數據累計超過Android設定的上限。具體可參考https://blog.csdn.net/self_study/article/details/60136277,這里對該異常進行了詳細描述。
下面我就說一說我的解決方案:
Bundle傳的數據大,想要不拋異常,只能降低Bundle傳的數據,因此我是通過修改傳索引的方式,而非直接穿序列化的實體,然后在Fragment中通過索引從列表中獲取數據。例子如下,一看就明白。
Activity中:
mPagerAdapter = new CommonViewPagerFragmentStateAdapter<MistakeBean>(getSupportFragmentManager(), mistakeBeanList){
@Override
public Fragment getItem(int position) {
Bundle bundle = new Bundle();
...
//原來的使用方式
// bundle.putSerializable("bean", mistakeBeanList.get(position));
//修改后的使用方式
bundle.putInt("position",position);
...
RedoMistakeFragment_ fragment_ = new RedoMistakeFragment_();
fragment_.setArguments(bundle);
return fragment_;
}
};
在Fragment中通過索引獲取對應的數據:
......
private int mFragmentPosition;//當前fragment對應的索引
private void init() {
Bundle bundle = getArguments();
if (bundle != null) {
mFragmentPosition = bundle.getInt("position");
//通過索引獲取緩存列表中的實體數據
mBean = (MistakeBean) TaskCacheManager.getInstance().getQuestion(mFragmentPosition);
}
}
2.同界面UI刷新問題
在當前的界面中刷新UI,即重新執行ViewPager初始化時:
1.如果數據列表數量發生變化,需要清除adapter中的數據,可調用clear()
方法,否則會出現越界問題;
2.如果數據數量未發生變化,可不清除原adapter中的緩存數據,當然這里建議要清除下,減少內存使用;