RecyclerView缓存复用原理

下面来看一个案例:由该案例引出我们今天的主题。我们废话少说,直接上代码。


import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.brett.testwebview.recyclerview.adapter.StarAdapter
import com.brett.testwebview.recyclerview.bean.Star

class MainActivity : AppCompatActivity() {
    private val xiangxueWebViewService = BaseServiceLoader.load(XiangxueWebViewService::class.java)
    private val starList = mutableListOf<Star>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.layout_recyclerview)
        init()
        val recyclerView = findViewById<RecyclerView>(R.id.rv_list)
        recyclerView.layoutManager = GridLayoutManager(this,1)
        recyclerView.adapter = StarAdapter(this, starList)
    }

    private fun init() {
        for (i in 0..4) {
            for (j in 0..20) {
                if (i % 2 == 0) {
                    starList.add(Star("技术人员$j", "技术组$i"))
                } else {
                    starList.add(Star("测试人员$j", "测试组$i"))
                }
            }
        }
    }
}

import android.content.Context;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import com.brett.testwebview.R;
import com.brett.testwebview.recyclerview.bean.Star;

import org.jetbrains.annotations.NotNull;

import java.util.List;

/**
 * Created by Brett.li on 2022/5/25.
 */
public class StarAdapter extends RecyclerView.Adapter<StarAdapter.StarViewHolder> {

    private Context context;
    private List<Star> starList;

    public StarAdapter(Context context, List<Star> starList) {
        this.context = context;
        this.starList = starList;
    }

    @NonNull
    @NotNull
    @Override
    public StarAdapter.StarViewHolder onCreateViewHolder(@NonNull @NotNull ViewGroup parent, int viewType) {
        Log.e("StarAdapter","执行onCreateViewHolder方法");
        View view = LayoutInflater.from(context).inflate(R.layout.rv_item_star, null);
        return new StarViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull @NotNull StarAdapter.StarViewHolder holder, int position) {
        Log.e("StarAdapter","执行onBindViewHolder方法");
        holder.tv.setText(starList.get(position).getName());
    }

    @Override
    public int getItemViewType(int position) {
        return super.getItemViewType(position);
    }

    @Override
    public int getItemCount() {
        return starList == null ? 0 : starList.size();
    }


    public class StarViewHolder extends RecyclerView.ViewHolder {

        private TextView tv;

        public StarViewHolder(@NonNull @NotNull View itemView) {
            super(itemView);
            tv = itemView.findViewById(R.id.tv_star);
        }
    }
}
//layout_recyclerview
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorPrimary"
        android:paddingTop="150dp" />

</RelativeLayout>
//rv_item_star
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorAccent">

    <TextView
        android:id="@+id/tv_star"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:gravity="center"
        android:textSize="20sp" />

</RelativeLayout>

这个案例十分简单,就是布局中添加了一个recyclerview控件,我们重点来看下adapter里面的onCreateViewHolder和onBindViewHolder方法里面的日志,还有记得我们的layoutmanager设置,我们现在是设置为GridLayoutManager(this,1),网格布局的形式,且每行只有一个itemview。
现在,我们在做个实验,当我们滑动view的时候,我们观察下log日志的输出规律。
如下图所示:
在这里插入图片描述
一开始onCreateViewHolder和onBindViewHolder方法都会打印日志,这好理解,因为一开始view需要创建然后绑定数据嘛,随后当我们不断滑动的时候发现只打印onBindViewHolder方法的日志,onCreateViewHolder方法的日志不打印了,这是为什么?很简单,我们猜也能猜出个大概,Google做了缓存。之前创建的itemview并没有因为滑出屏幕之外就销毁了,而是被缓存起来了,然后从缓存的itemview里面绑定数据,因此只会打印onBindViewHolder方法的日志。
接下来,我们做个小改动,我们改动GridLayoutManager(this,3),看下实验结果会有什么变化。
在这里插入图片描述
一开始还是onCreateViewHolder和onBindViewHolder方法的日志交替出现,但是当我们不断滑动的时候,发现还是会偶尔出现onCreateViewHolder日志的。这又是为什么呢?难道Google会把缓存下来的itemview给删除掉了?既然这样,那么什么时候会去删除这个缓存下来的itemview呢?为了解决我们的疑惑,我们的首选方案自然是看源码了。
之前,那么如何看源码呢?自然是带着问题看源码。我们的问题是那么呢?——>recyclerview是如何缓存itemview的,itemview又是如何被销毁的?接下来,我们就在探究下recyclerview的缓存原理。
那么我们应该看哪里的源码呢?自然是onCreateViewHolder和onBindViewHolder方法是如何被调用了。我们点进这两个方法看下是什么回事。
在这里插入图片描述
在这里插入图片描述
发现,进入了RecyclerView类,而且onCreateViewHolder和onBindViewHolder方法还是一个抽象方法。原来RecyclerView是一个viewgroup,而且还实现了实现嵌套滑动的相关接口,这个我们这次就先不考虑。我们先看下什么地方调用方法,找到了在createViewHolder方法中被调用了。顺着这个思路,我们看下createViewHolder方法在哪里被调用的,以此类推,我们说下最终结论:fill --> layoutChunk -->layoutState.next(在该方法中调用了addView(view)) --> getViewForPosition --> tryGetViewHolderForPositionByDeadline -->createViewHolder–>onCreateViewHolder。那么有人会问,fill方法又是在哪里被调用的。以目前,我们得到线索,很难得知,因为fill方法在源码中很多地方被调用到,如果要一个个去看,大概率会迷失在源码中。因此,这是我们能得到的最靠谱的答案。那么我们就只能另找线索了。
在这里插入图片描述
我们在仔细回想一下,我们刚刚的操作:我们在滑动的时候,发现了日志的异常。有一个显眼的名词:滑动。既然是滑动的时候出现的,那么我们从onTouchEvent事件入手,可能可以找到一些线索。那么,我们应该从哪里的onTouchEvent入手呢?既然,我们滑动的时候recyclerview,那么当然是看下recyclerview的onTouchEvent方法做了什么操作。具体说是onTouchEvent的MOVE事件究竟做了什么操作。代码如下:

case MotionEvent.ACTION_MOVE: {
                final int index = e.findPointerIndex(mScrollPointerId);
                if (index < 0) {
                    Log.e(TAG, "Error processing scroll; pointer index for id "
                            + mScrollPointerId + " not found. Did any MotionEvents get skipped?");
                    return false;
                }

                final int x = (int) (e.getX(index) + 0.5f);
                final int y = (int) (e.getY(index) + 0.5f);
                int dx = mLastTouchX - x;
                int dy = mLastTouchY - y;

                if (mScrollState != SCROLL_STATE_DRAGGING) {
                    boolean startScroll = false;
                    if (canScrollHorizontally) {
                        if (dx > 0) {
                            dx = Math.max(0, dx - mTouchSlop);
                        } else {
                            dx = Math.min(0, dx + mTouchSlop);
                        }
                        if (dx != 0) {
                            startScroll = true;
                        }
                    }
                    if (canScrollVertically) {
                        if (dy > 0) {
                            dy = Math.max(0, dy - mTouchSlop);
                        } else {
                            dy = Math.min(0, dy + mTouchSlop);
                        }
                        if (dy != 0) {
                            startScroll = true;
                        }
                    }
                    if (startScroll) {
                        setScrollState(SCROLL_STATE_DRAGGING);
                    }
                }

                if (mScrollState == SCROLL_STATE_DRAGGING) {
                    mReusableIntPair[0] = 0;
                    mReusableIntPair[1] = 0;
                    if (dispatchNestedPreScroll(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            mReusableIntPair, mScrollOffset, TYPE_TOUCH
                    )) {
                        dx -= mReusableIntPair[0];
                        dy -= mReusableIntPair[1];
                        // Updated the nested offsets
                        mNestedOffsets[0] += mScrollOffset[0];
                        mNestedOffsets[1] += mScrollOffset[1];
                        // Scroll has initiated, prevent parents from intercepting
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }

                    mLastTouchX = x - mScrollOffset[0];
                    mLastTouchY = y - mScrollOffset[1];

                    if (scrollByInternal(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            e)) {
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                    if (mGapWorker != null && (dx != 0 || dy != 0)) {
                        mGapWorker.postFromTraversal(this, dx, dy);
                    }
                }
            } break;

代码这么长,该如何看呢?看方法名,看方法名,看方法名。重要的事情说三遍。我们看到scrollByInternal方法可能是一个重要的方法。点进去看下:
在这里插入图片描述
在这里插入图片描述
在scrollStep方法里面,我们发现了一些线索:scrollHorizontallyBy、scrollVerticallyBy,多么明显的线索啊!看来我们的思路是正确的。mLayout又是个什么东西,它是一个LayoutManager。
在这里插入图片描述
我们点击GridLayoutManager类里面的scrollVerticallyBy方法看下,原来GridLayoutManager是继承了LinearLayoutManager的。按照同样的方法,我们一路跟踪到了LinearLayoutManager的scrollBy方法,发现在这个方法里面调用了fill方法。自此,我们的调用链全部连接起来了。
这里,我们总结下:滑动 Move 事件 --> scrollByInternal --> scrollStep --> mLayout.scrollVerticallyBy
–> scrollBy --> fill --> layoutChunk --> layoutState.next -->getViewForPosition --> tryGetViewHolderForPositionByDeadline -->createViewHolder–>onCreateViewHolder。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
现在,我们理清了调用链,那么RecyclerView缓存复用这段逻辑被隐藏在哪里呢?答案是tryGetViewHolderForPositionByDeadline方法,为什么是这个方法呢?因为在这个方法上面的注释有说明,省了我们看代码逻辑的功夫。
在这里插入图片描述
第一段英文,大家看得懂吧!recylerview做了三级缓存,分别是Recycler scrap,cache和RecycledViewPool,这三个拿不到缓存就直接创建。
我们先看下tryGetViewHolderForPositionByDeadline方法的第一段代码:
在这里插入图片描述
按照笔者的理解,这段代码的意思就是通过position从mChangedScrap和mAttachedScrap中拿到一个viewhodler(就是itemview)。但是笔者不理解这两个东西的作用是什么。查看了一些博客资料,有一种观点比较靠谱:mAttachedScrap和mChangedScrap是缓存还在屏幕内的ViewHolder。就姑且认为是这个作用吧!
接下来,看下第二段代码:
在这里插入图片描述
如果从mAttachedScrap和mChangedScrap中没有拿到ViewHolder,那么就会根据itemId从getScrapOrCachedViewForId方法中获取ViewHolder。我们看下getScrapOrCachedViewForId方法做了什么东西,一开始还是从mAttachedScrap里面寻找有无ViewHolder,如果找不到就从mCachedViews(是一个集合来的)里面寻找,代码的意思大体就是这样。
在这里插入图片描述
在这里插入图片描述
那么疑惑来了这个mCachedViews是来缓存哪些ViewHolder的?它与mAttachedScrap和mChangedScrap这两个缓存的ViewHolder有什么区别呢?很遗憾,笔者能力有限,没看出来,最后也是去查看了一些资料的。一些资料是说:缓存移除屏幕之外的ViewHolder。
我们再看第三段代码:
在这里插入图片描述
ViewCacheExtension是一个抽象类,需要我们自己实现相关的方法。很明显,这个是给用户自己管理ViewHolder的创建和缓存的。一般用的比较少吧,反正笔者没用过。
接下来,看下最后一段代码:
在这里插入图片描述

这段很好理解,从Viewpool里面拿ViewHolder,如果没有拿到就调用createViewHolder方法,而createViewHolder方法里面调用了onCreateViewHolder方法。
自此,我们明白了RecyclerView的缓存复用原理:

  1. mAttachedScrap和mChangedScrap是缓存还在屏幕内的ViewHolder
  2. mCachedViews缓存移除屏幕之外的ViewHolder
  3. mViewCacheExtension自定义缓存
  4. 从ViewPool缓存池

至于onBindViewHolder方法的调用时机,读者就自行去看下了,很简单的,相信大家一看就会。提示下tryGetViewHolderForPositionByDeadline里面就有相关的代码。
接下来,我们看一个比较重要的点:之前我们讲解缓存的时候有发现mAttachedScrap、mChangedScrap、mCachedViews这些集合,那么这些集合是什么时候被赋值的。换句话说就是RecyclerView是怎么往我们的四级缓存中添加数据的?
我们首先看下mAttachedScrap和mChangedScrap,很舒服就一个地方调用了add方法。
在这里插入图片描述在这里插入图片描述
经过一番逆向推导,发现LayoutManager的onLayoutChildren方法调用到了这个方法。而这个方法被dispatchLayoutStep1方法调用了,那dispatchLayoutStep1方法被哪个方法调用了——>onMeasure方法,没错就是View的绘制流程中的三个重要方法之一。
那么mCachedViews呢?recycleViewHolderInternal方法里面有对mCachedViews进行赋值。scrapOrRecycleView这个方法最后也是来到了onMeasure,与mAttachedScrap和mChangedScrap一样的逻辑。
在这里插入图片描述
而ViewPool呢?也是在recycleViewHolderInternal方法被调用的,这里就不在罗列了。
总结一句话:在ItemView绘制的时候,RecyclerView就会对缓存进行赋值操作。
至此,RecyclerView的缓存复用原理已经讲解完毕。插一句,为什么RecyclerView要做这么多级缓存呢?估计是为了性能吧。ViewPool这个缓存池大家可以好好看下,里面每种ItemType的BiewHolder最多会保存5个。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值