遇到了一个Bug,用RecyclerView显示item时序号是乱的,感谢@ShopKeeper找到了问题。这两天通过查询资料也了解到recyclerView的缓存机制(虽然bug不在这,但了解一下也有好处,面试可能会用上)。
源代码 TextFragment.java
写了一个小Demo
package com.cztcode.test;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.util.ArrayList;
public class TestFragment extends Fragment {
private TextView mText;
private RecyclerView mRecyclerView;
private itemAdapter mItemAdapter;
private final String TAG="TestFragment";
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.recycler, container, false);
mRecyclerView = (RecyclerView) v.findViewById(R.id.recycler_view);
mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
updateUI();
return v;
}
private class ViewHolder extends RecyclerView.ViewHolder {
private Data mData;
public ViewHolder(LayoutInflater inflater, ViewGroup parent) {
super(inflater.inflate(R.layout.item, parent, false));
mText = (TextView) itemView.findViewById(R.id.itemTextView);
}
public void bind(Data data) {
mData = data;
mText.setText(mData.getNum());
Log.d(TAG,"Bind"+mData.getNum());
}
}
private class itemAdapter extends RecyclerView.Adapter {
private ArrayList<Data> mDataArrayList;
public itemAdapter(ArrayList<Data> dataArrayList) {
mDataArrayList = dataArrayList;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater layoutInflater = LayoutInflater.from(getActivity());//?
return new ViewHolder(layoutInflater, parent);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
Log.d(TAG,"position "+position);
Data data = mDataArrayList.get(position);
Log.d(TAG,"Data"+data.getNum());
((ViewHolder) holder).bind(data);
}
@Override
public int getItemCount() {
return mDataArrayList.size();
}
}
private void updateUI() {
com.cztcode.test.DataList dataList = com.cztcode.test.DataList.get();
ArrayList<Data> data = dataList.getData();
if (mItemAdapter == null) {
mItemAdapter = new itemAdapter(data);
mRecyclerView.setAdapter(mItemAdapter);
}
}
}
尝试找到bug
使用Log查看bind和onBindViewHolder,显示如下:
说明根据position绑定数据是正确的,那问题究竟在哪里呢。
原来是TextView变量写在了ViewHolder外,导致了回收的ViewHolder与新创建的ViewHolder写入一个变量时顺序错乱,将这个变量写在ViewHolder内就可以了,让每个item都会显示自己的数据,不会发生冲突。
出现问题的代码,之前将TextView放在了Fragment中。
更改后:
显示:
一切又恢复了正常
RecyclerView回收机制
之前怀疑是回收问题,找了一些关于回收的内容。
首先有四个缓存
- 总共有
mAttachedScrap
、mCachedViews
mViewCacheExtension
、mRecyclerPool
4级缓存,其中mAttachedScrap
只保存布局时,屏幕上显示的viewholder
,一般不参与回收、复用(拖动排序时会参与);mCachedViews
主要保存刚移除屏幕的viewholder
,初始大小为2;mViewCacheExtension
为预留的缓存池,需要自己去实现;mRecyclerPool
则是最后一级缓存,当mCachedViews
满了之后,viewholder
会被存放在mRecyclerPool
,继续复用。
其中,mAttachedScrap
、mCachedViews
为精确匹配,即为对应position
的viewholder
才会被复用; mRecyclerPool
为模糊匹配,只匹配viewType
,所以复用时,需要调用onBindViewHolder
为其设置新的数据。
mCachedViews
mCachedViews:滑动过程中的回收和复用都是先处理的这个 List,这个集合里存的 ViewHolder 的原本数据信息都在,所以可以直接添加到 RecyclerView 中显示,不需要再次重新 onBindViewHolder()。
mCachedViews 的大小默认为2。遍历 mCachedViews,找到 position 一致的 ViewHolder。由于这里根本没有重新绑定,所以会先匹配position,只有原来位置的ViewHolder会在这里调用。
然后滑动到第三个num3,再往回滑动。此时num1和num2在mCachedView中,直接从mCacheViews取出(匹配position)
这里需要注意,position匹配不上会去找id再匹配一遍。这个id而是 Adapter 持有的一个属性,默认是不会使用这个属性的,所以这里其实是不会执行的,除非我们重写了 Adapter 的 setHasStableIds()。
巧了,为了找这个bug我就重写了这个属性。。。是在网上看到reecyclerView乱序解决方法中的
然后重写getItemId方法,根据position位置判断。
这样看来此做法就没有作用了,都是比较position。我想设计这个比较方法时想让自定义id的生成方案,达到不同的回收目的。
网上还有一个更离谱的方法,将viewType设置成position。这样做就无法显示不同的viewType,而且导致RecyclerView的四级缓存无法发挥作用,因为mRecyclerPool是根据viewType来回收的,一个viewHolder一个viewType等于不回收。
默认DEFAULT_CACHE_SIZE为2,可通过Recyclerview.setItemViewCacheSize()动态设置。当遇到向上滑不更新视图时可设置为0,因为调用此缓存是不会绑定数据的。
mRecyclerPool
mRecyclerPool:存在这里的 ViewHolder 的数据信息会被重置掉,相当于 ViewHolder 是一个重创新建的一样,所以需要重新调用 onBindViewHolder 来绑定数据。
ViewPool 会根据不同的 viewType 创建不同的集合来存放 ViewHolder,那么复用的时候,只要 ViewPool 里相同的 type 有 ViewHolder 缓存的话,就将最后一个拿出来复用,不用像 mCachedViews 需要各种匹配条件,只要有就可以复用。拿到 ViewHolder 之后,还会再次调用 resetInternal() 来重置 ViewHolder,这样 ViewHolder 就可以当作一个全新的 ViewHolder 来使用了,这也就是为什么从这里拿的 ViewHolder 都需要重新 onBindViewHolder() 了。
写了一个演示,可以看见100个数据只创建了24个viewHolder,其它的大部分在ViewPool中复用。
了解一下原理也挺有意思