ListView 和 RecyclerView 复用机制区别

96
yuandalai
3.1 2019.02.18 16:25 字数 2048

ListView 和 RecyclerView 的复用机制还是有很大的差异, ListView 的复用是两级缓存的,而 RecyclerView 是四级缓存,在这里参考了腾讯Bugly干货分享里面详细比较了这两者缓存机制的区别。这里我想自己总结一下,方便记忆

介绍 ListView 和 RecyclerView 每一级的复用

ListView 的两级复用

是否 createView 是否 bindView 备注
mActiveViews 用于屏幕内快速复用
mScrapeViews
  • mActiveViews mActiveViews 是一个保存可以屏幕内可见,可以直接复用的 View 的数组。这里的备注说是用于屏幕内快速复用,在一开始还是很懵逼的,但是经过我的一番资料的查询终于搞清楚了,感谢代码如风的的博客,借用下它的配图(在下面)来分析下什么叫屏幕内快速复用,屏幕内快速复用其实也很好理解,重点来了,假如我们分析下面这个场景,屏幕里面可以完整的容纳六个 View,向下滑动,第0个位置的 View (从0开始计数)被滑出了屏幕,但是 mActiveViews 数组里面还有第1到第5个 View 可以直接复用。这就是备注里面所说的屏幕内快速复用,很合清理也不难理解。

  • mScrapeViews mScrapeViews 是一个保存移除屏幕的 View 的数组,移到这里的 View 都会恢复成一个空白的 View,同时也恢复所有的状态(意思就是把 View 恢复到刚创建时的状态,以后再进行数据绑定的时候可以直接使用),关于具体和 mActiveViews 在复用的时候谁先被调用等问题后面解释。

image.png

RecyclerView 的四级复用

是否 createView 是否 bindView 备注
mAttachedScrap 用于屏幕内快速复用
mCacheViews 默认2个
mViewCashExtension 不研究 不研究
mRecyclerPool 默认5个
  • mAttachedScrap 这个可以说和 ListView 的 mActiveViews 十分地相似,但是这里唯一要强调的就是 ListView 和 RecyclerView 复用的东西不同,ListView 复用的是 View,也就是在实现 ListView 的 Adapter 的时候实现的 getView 方法里的参数 convertView,但是 RecyclerView 复用的对象是 ViewHolder,当然 ListView 也可以由自己来实现 ViewHolder,关于 ViewHolder 的知识下面会讲的。

  • mCacheViews + mRecyclerPool 这两个加起来实现的效果和 ListView 的 mScrapeViews 实现的效果差不多,但是内部的细节却显得更加精妙,都是缓存的离开屏幕的 ViewHolder ,相当于对 mScrapeViews 进行了一个分级缓存,当 RecyclerView 对离开的屏幕的 ViewHolder 进行回收时先将 ViewHolder 回收进 mCacheViews,此时 ViewHolder 里面 View 还是被回收时的样子,没有变成初始形态,但当 mCacheViews 里面的个数超过规定的数量时,会把 mCacheViews 里最老的的 ViewHolder 转进 mRecyclerPool 里去,进入到这里就像进入到 ListView 的mScrapeViews,ViewHolder 所有的属性和状态都会被还原,当然在理论上实现多个 RecyclerView 公用同一个 mRecyclerPool ,这样子当我们在使用 ViewPager 的时候就会将 ViewHolder 存进一个共同的 mRecyclerPool 里面,可以一定程度上节省内存。

ViewHolder 的介绍

ViewHolder 和复用机制没有什么直接的关系,但是有时候会存在混淆,所以还是另外介绍下 ViewHolder,这里可以结合我们平时写的代码来解释它。
如果我们没有使用 ViewHolder,我们的代码就是下面这种情况。

@Override
public View getView(int position, View convertView, ViewGroup parent) {
        
        ImageView userImg;
        TextView userName;
        TextView userComment;        
        ListViewItem itemData = items.get(position);
        if(convertView == null){
            convertView = View.inflate(context, R.layout.list_item_layout, null);
        }        
        userImg = (ImageView) convertView.findViewById(R.id.user_header_img);
        userName = (TextView) convertView.findViewById(R.id.user_name);
        userComment = (TextView) convertView.findViewById(R.id.user_coomment);
        userImg.setImageResource(itemData.getUserImg());
        userName.setText(itemData.getUserName());
        userComment.setText(itemData.getUserComment());
       
        return convertView;
}

每次调用 getView 的时候会 findViewById ,这无疑是很浪费时间的,有什么方法可以避免每次都去 findViewById 呢?没错就是通过实现 ViewHolder,接下来就是 ViewHolder 的实现。

@Override
public View getView(int position, View convertView, ViewGroup parent) {
        final ViewHolder holder;
        ListViewItem itemData = items.get(position);
        if(convertView == null){
            convertView = View.inflate(context, R.layout.list_item_layout, null);
            holder = new ViewHolder();
            holder.userImg = (ImageView) convertView.findViewById(R.id.user_header_img);
            holder.userName = (TextView) convertView.findViewById(R.id.user_name);
            holder.userComment = (TextView) convertView.findViewById(R.id.user_coomment);
            convertView.setTag(holder);
        }else{
            holder = (ViewHolder) convertView.getTag();
        }
        holder.userImg.setImageResource(itemData.getUserImg());
        holder.userName.setText(itemData.getUserName());
        holder.userComment.setText(itemData.getUserComment());
        return convertView;
}

static class ViewHolder{
        ImageView userImg;
        TextView userName;
        TextView userComment;
}

说白了 ViewHolder 是用来保持着 convertView 对里面每个子 View 的引用,避免每次都要 findViewById。

ListView 和 RecyclerView 是先回收还是先复用?

这里先给出答案,当然是先复用,因为我们还是来设想一下,如果屏幕刚好只能容纳10个 item 的时候,当滑动发生的时候会有一种状态是有11个 item 显示在屏幕当中,当然这时有两个 item 是不完全显示的,这种情况下 ListView 和 RecyclerView 不可能先把第1个 item 回收了,再去复用第11 个 item,那这势必不是一种很好的体验(一位会出现改显示出来的还没有显示,但是不该回收的却被回收了),所以正确的姿势绝对是先复用 item ,然后当第1个 item 完全看不见的时候,再去把它回收掉。

ListView 和 RecyclerView 的复用过程

ListView 的复用过程

我觉得 腾讯Bugly 里的流程图解释的很准确,所以这里借用下它的流程图,再加上一些自己的解读,废话不多说,先上图。

image.png

在这上面所有的步骤都很好解释,我这里就想说下其中的一个步骤就是 从mActiviViews 中通过匹配 pos 读取 view,读取成功的时候就会直接跳过 getView() 这个步骤,之前在我没搞懂 mActiviViews 的功能时,还不是很清楚这一步的含义,但是当我懂了 mActiviViews 是什么以后就彻底懂了。因为 mActiviViews 是屏幕上面可见的 View,所以我们并且此时 mActiviViews 上面还未还原,所以就可以直接通过在数据源中的位置来从 mActiviViews 找出对应的 View ,进而直接进行复用。

RecyclerView 的复用

同样借用 腾讯Bugly 俩面 RecyclerView 复用过程的流程图。

这里值得注意的是处于 mChacheViews 中的 ViewHolder 和 ListView 中的 mActiviViews 一样都是通过 pos 来确定是否使用其中的 ViewHolder(或者View)复用,mRecyclerPool 在找寻 ViewHolder 缓存的时候会根据 View 的类型来找寻不同的 mRecyclerPool,找到后重新 bindView(不同的类型是通过重写 RecyclerView 的 getViewType() 来实现的 )。

ListView 和 RecyclerView 的回收过程

ListView 的回收过程

ListView 的回收过程十分简单,就是完全滑出屏幕后就把 View 回收到 mScrapeViews 当中去,并把 View 还原成初始状态,所以说 ListView 中所有进行复用的 View 的数量加起来一定是一个定值,其大小和屏幕所能容纳下的 item 的个数有关

RecyclerView 的回收过程

RecyclerView 的回收过程就是一个标准的二级缓存,滑出屏幕的 ViewHolder 先缓存进 mCacheViews ,此时并不还原视图,当 mCacheViews 中的数量超过一定的限制以后(默认是2个,这个是可以由自己来决定的),将最先放入 mCacheViews 的 ViewHolder 放入到 mRecyclerPool 当中去,并且是根据 View 的 type 不同,放入不同的 mRecyclerPool 当中去,同时 mRecyclerPool 也有大小的限制(默认是 5 个),但是这种回收机制好处就在于可以保证 mCacheViews 和 mRecyclerPool 是最新的放到前面。

ListView 和 RecyclerView 的优缺点

我个人倾向是使用 RecyclerView 的,RecyclerView 的优点很多很多的,它的自由度更高,提供一种插拔式的体验(特别是体现在想快速的改变 RecyclerView 列表的表现形式的时候,默认实现了 ViewHolder,只用替换相应的 LayoutManager就可以实现),同时在实现一些多种类复杂的列表的时,想要自定义列表切换的动画的时候,优势也十分明显,但是 RecyclerView 也不是说就可以代替 ListView,它们在性能上面的差异不是很明显,各自都有自己的一套回收/复用机制,关键是在实现 RecyclerView 的时候 RecyclerView 要自己重新写 Adpater,这样子很麻烦,特别是项目里面列表出现的次数很多的时候,但是面对简单的需求的时候 ListView 就显得更加的轻松,它可以直接用默认的 Adapter 来实现。

编程学习心得
Web note ad 1