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中的缓存数据,当然这里建议要清除下,减少内存使用;
网友评论