RecyclerView显示乱序与缓存机制

遇到了一个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回收机制

之前怀疑是回收问题,找了一些关于回收的内容。

首先有四个缓存

  • 总共有mAttachedScrapmCachedViews
  • mViewCacheExtensionmRecyclerPool4级缓存,其中mAttachedScrap只保存布局时,屏幕上显示的viewholder,一般不参与回收、复用(拖动排序时会参与);
  • mCachedViews主要保存刚移除屏幕的viewholder,初始大小为2;
  • mViewCacheExtension为预留的缓存池,需要自己去实现;
  • mRecyclerPool则是最后一级缓存,当mCachedViews满了之后,viewholder会被存放在mRecyclerPool,继续复用。

其中,mAttachedScrapmCachedViews为精确匹配,即为对应positionviewholder才会被复用; 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中复用。

了解一下原理也挺有意思

发表评论