下面来看一个案例:由该案例引出我们今天的主题。我们废话少说,直接上代码。
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的缓存复用原理:
- mAttachedScrap和mChangedScrap是缓存还在屏幕内的ViewHolder
- mCachedViews缓存移除屏幕之外的ViewHolder
- mViewCacheExtension自定义缓存
- 从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个。