在项目开发中我们经常会碰到这样的需求: 一个左右滑动切换的Viewpager
,page中是一个列表。这时大家一般都会采用Viewpager
加Fragment
\ 自定义View
的方式去实现,每个页面复用Adapter
,当page.size > 2
的时候,会出现白屏问题Adapter
设置或刷新不生效的问题
Example:
总共有3页,前两页正常加载,到第三页加载完以后左滑回第一页,这时会发现列表丢失了,变成空白页面了,无论是listView
的Adapter.notifyDatasetChanged
还是RecyclerView
的notifyXxxx
都没有生效。
排查原因:
- 首先发现直接用匿名类方式使用
Adapter
每次直接new对象是不会出这种问题的,但是这种实现方法很明显太浪费内存了
- 首先发现直接用匿名类方式使用
- 执行过
PagerAdapter#destroyItem()
方法的页面出现这个问题,我们以FragmentPagerAdapter
的源码来看:
- 执行过
@Override
public Object instantiateItem(ViewGroup container, int position) {
final long itemId = getItemId(position);
// Do we already have this fragment?
通过name作为tag标记在事务中缓存了Fragment
String name = makeFragmentName(container.getId(), itemId);
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
复用的时候直接执行attach()方法
mCurTransaction.attach(fragment);
} else {
fragment = getItem(position);
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), itemId));
}
······
return fragment;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
······
在viewpager销毁页面的时候执行了detach方法,回收了页面
mCurTransaction.detach((Fragment)object);
}
FragmentPageAdapter
在destoryItem()
中detach
掉了需要销毁的fragment
,在instantiateItem()
方法中获取Fragment
重新加载,在复用时直接调用FragmentTransaction.attach()
理一下Fragment的状态顺序,以index=0
的fragment
为例,
- 初次实例化的时候通过
FragmentTransaction.add()
添加到窗口时执行顺序为Fragment.onAttach()
→Activity.onAttachFragment(Fragment)
→Fragment.performCreate()
→Fragment.onCreate()
→Fragment.performCreateView()
→Fragment.onCreateView()
→Fragment.onViewCreated()
-
detach()
时顺序为Fragment.performDestroyView()
→Fragment.onDestroyView()
→Fragment.mContainer.removeView(Fragment.mView)
- 重新
attach()
时顺序为
Fragment.onCreateView()
→Fragment.onViewCreated()
onViewCreated()
执行的前提是onCreateView()
的返回view
不为空,
因此onViewCreated()
中的view
一定是非空的
public class RedFragment extends BaseFragment {
private MyAdapter mAdapter;
public static RedBagFragment getInstance(String type){
······return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle state) {
return inflater.inflate(R.layout.fragment_red, container, false);
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
mListView = (RecyclerView) view.findViewById(R.id.rv_data);······
}
复用时重新调用了fragment
的onCreateView()
,重新创建了一个新的view
,因此RecyclerView\ListView
已经是一个全新的对象了,而Adapter
仍然对应的是旧的RecyclerView\ListView
。代码自然就不会生效了,而是会展示一片空白
if(mAdapter ==null){
mAdapter =new MyRedBagAdapter(mData);
mListView.setAdapter(mAdapter);
}else {
mAdapter.notifyDataSetChanged();
}
解决方法:
- 1,复用
Fragment
的inflaterView
(推荐)
private View mInflated;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
if(mInflated == null){
mInflated = inflater.inflate(R.layout.fragment_red, container, false);
}
return mInflated;
}
- 2,在
onDestoryView
时释放adapter
(不推荐,内存浪费)
@Override
public void onDestroyView() {
super.onDestroyView();
mAdapter = null;
}
- 3,
在(不推荐,内存浪费 画蛇添足)adapter
中自定义关联方法,使得adapter
与ListView/RecyclerView
双向关联
在adapter中,定义attach
public void setAttachedView(View listView){
mListView = listView;
}
public View getAttachedView() {
return mListView;
}
在fragment中
void setAdapter(){
if(mAdapter == null || mAdapter.getAttachedView() != mListView){
mAdapter = new MyAdapter(mData);
mAdapter.setAttachedView(mListView);
mListView.setAdapter(mAdapter);
} else {
mAdapter.notifyDataSetChanged();
}
}
2 3两个方案比 1 差多了,在对象销毁前旧的View和adapter都是内存垃圾处于泄漏状态,而且多执行了inflate
和findViewById
操作,消耗更多的计算时间。
网友评论