自定义控件三部曲视图篇(七)——RecyclerView系列之四实现回收复用

本文详细探讨了RecyclerView的回收与复用原理,对比了LinearLayoutManager和自定义LayoutManager的回收复用情况。通过分析RecyclerView的回收复用机制,包括mAttachedScrap、mCachedViews和mRecyclerPool的作用,展示了如何在自定义LayoutManager中添加回收复用功能。文中还介绍了在滚动时如何处理回收和填充新项,并提供了具体的代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

怕什么真理无穷,进一寸有一寸的欢喜。 ----------胡适


系列文章: Android自定义控件三部曲文章索引: http://blog.youkuaiyun.com/harvic880925/article/details/50995268


一、View的回收与复用

1.1 RecyclerView是否会自动回收复用

想必大家都听说RecyclerView是可以回收复用的,但它会自动复用吗?我们上面写的例子会不会复用呢?

1.1.1 如何判断是否复用

首先,我们需要知道怎么判断RecyclerView是不是复用了View。我们知道在Adapter中有两个函数:

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
   
   
    …………
}

@Override
public void onBindViewHolder(ViewHolder holder, int position) {
   
   
    …………
}

其中onCreateViewHolder会在创建一个新View的时候调用,而onBindViewHolder会在已经存在View,绑定数据时调用。所以,如果是新创建的View,则会先调用onCreateViewHolder来创建View,然后调用onBindViewHolder来绑定数据,如果是复用的View,就只会调用onBindViewHolder而不会调用onCreateViewHolder

1.1.2 对比LinearLayoutManager与CustomLayoutManager

一、LinearLayoutManager回收复用情况
首先,我们在我们Demo中的RecyclerAdatper的onCreateViewHolderonBindViewHolder中添加上日志:

private int mCreatedHolder=0;
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
   
   
	mCreatedHolder++;
    Log.d("qijian", "onCreateViewHolder  num:"+mCreatedHolder);
    …………
}

@Override
public void onBindViewHolder(ViewHolder holder, int position) {
   
   
    Log.d("qijian", "onBindViewHolder");
    …………
}

在打日志的同时,用mCreatedHolder变量标识当前总共创建了多少个View.然后将LayoutManager设置为LinearLayoutManager:

public class LinearActivity extends AppCompatActivity {
   
   
    @Override
    protected void onCreate(Bundle savedInstanceState) {
   
   
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_linear);
		…………
        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
        linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
        mRecyclerView.setLayoutManager(linearLayoutManager);
        …………
    }
    …………
}    

操作步骤如下图所示:在刚启动后,然后下滑几个Item,然后再上滑几个Item,边操作边看日志情况:
在这里插入图片描述
所对应的日志情况如下:
在这里插入图片描述
从日志中可以看到,在页面出现时,由于页面初始化是空白的,所以此时都是通过onCreateViewHolder来创建View。在滑动之后,会发现,并不会再走onCreateViewHolder了,只会通过onBindViewHolder来绑定数据了。这就说明:在初始化时,是创建的View,在创建到一定数量(我手机上是23个)之后,就开始使用回收复用逻辑,把无用的View给复用起来。所以LinearLayoutManager是可以做到回收复用的。

二、CustomLayoutManager回收复用情况
接下来,我们将LinearLayoutManger改为CustomLayoutManager,来看下在上部分我们写好了CustomLayoutManager会不会自动回收复用:

public class LinearActivity extends AppCompatActivity {
   
   
    private ArrayList<String> mDatas = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
   
   
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_linear);
        …………
        RecyclerView mRecyclerView = (RecyclerView) findViewById(R.id.linear_recycler_view);
        mRecyclerView.setLayoutManager(new CustomLayoutManager());
		…………
    }
    …………
}    

同样的滑动方法,来看下日志:
在这里插入图片描述

可以看到,CustomLayoutManager会在初始化时一次性创建200个View。而在我们滚动时,即不会调用onCreateViewHolder也不会调用onBindViewHolder,这是为什么呢?
因为我们总共有200个数据,所以这里创建了200个View。也就是一次性将所有View创建完成,并加进RecyclerView.
正是因为所以的ItemView都已经加进RecyclerView了,所以可以实现滚动功能,但并没有实现回收复用。而且一次性创建所有Item的holderView,极易可能出现ANR。

1.2 RecyclerView的回收复用原理

从上面的对比中可以看出,RecyclerView确实是存在回收复用的,但回收复用是需要我们在自定义的LayoutManager中处理的,而不是会自动具有这个功能,那么问题来了,我们要怎么给自定义的LayoutManager添加上回收复用功能呢?
在讲解自定义回收复用之前,我们需要先了解RecyclerView是如何处理回收复用的。

1.2.1 简述RecyclerView的回收复用原理

其实RecyclerView内部已经为我们实现了回收复用所必备的所有条件,但在LayoutManager中,我们需要写代码来标识每个holderView是否继续可用,还是要把它放在回收池里面去。很明显,我们在上面的实例代码中,我们只是通过layoutDecorated(……)来布局Item,而对已经滚出屏幕的HolderView没有做任何处理,更别说给他们添加已经被移除的标识了。所以我们写的CustomLayoutManager不能复用HolderView的原因也在这。下面我们来看看RecyclerView给我们已经做好了哪方面准备,我们先来整体理解下RecyclerView的回收复用原理,然后再写代码使我们的CustomLayoutManager具有回收复用功能。

1、RecyclerView的回收原则
从上面的讲述中,可以知道,我们在自定义的LayoutManager中只需要告诉RecyclerView哪些HolderView已经不用了即可(使用removeAndRecycleView(view, recycler)函数)。然后RecyclerView中用两级缓存(mCachedViews和mRecyclerPool)来保存这些已经被废弃(Removed)的HolderView。这两个缓存的区别是:mCachedViews是第一级缓存,它的size为2,只能保存两个HolderView。这里保存的始终是最新鲜被移除的HolderView,当mCachedViews满了以后,会利用先进先出原则,把老的HolderView存放在mRecyclerPool中。在mRecyclerPool中,它的默认size是5。这就是RecyclerView的回收原则。

2、Detach与Scrap
除了回收复用,有些同学在看自定义LayoutManager时,会经常在layoutChildren函数中看到一个函数:detachAndScrapAttachedViews(recycler);它又是来干嘛的呢?

试想一种场景,当我们插入了条Item或者删除了条Item又或者打乱Item顺序,怎么重新布局这些Item呢?这些情况都涉及到,如何将现有的屏幕上的Item布局到新位置的问题。最简单的方法,就是把每个item的HolderView先从屏幕上拿下来,然后再像排列积木一样,按照最新的位置要求,重新排列。

detachAndScrapAttachedViews(recycler);的作用就是把当前屏幕上所有的HolderView与屏幕分离,将它们从RecyclerView的布局中拿下来,然后存放在一个列表中,在重新布局时,像搭积木一样,把这些HolderView重新一个个放在新位置上去。将屏幕上的HolderView从RecyclerView的布局中拿下来后,存放的列表叫mAttachedScrap,它依然是一个List,就是用来保存从RecyclerView的布局中拿下来的HolderView列表。所以,大家可以查看所有自定义的LayoutManager,detachAndScrapAttachedViews(recycler);只会被用在onLayoutChildren函数中。就是因为onLayoutChildren函数是用来布局新的Item的,只有在布局时,才会先把HolderView detach掉然后再add进来重新布局。但大家需要注意的是mAttachedScrap中存储的就是新布局前从RecyclerView中剥离下来的当前在显示的Item的holderView。这些holderView并不参与回收复用。单纯只是为了先从RecyclerView中拿下来,再重新布局上去。对于新布局中没有用到的HolderView,会从mAttachedScrap移到mCachedViews中,让它参与复用。

3、RecyclerView的复用原则
至此,已经有了个三个存放RecyclerView的池子:mAttachedScrap、mCachedViews、mRecyclerPool。其实,除了系统提供的这三个池子,RecyclerView也允许我们自己扩展回收池,并给它预留了一个变量:mViewCacheExtension,不过我们一般不会用到,使用系统自带的回收池即可。

所以,在RecyclerView中,总共有四个池子:mAttachedScrap、mCachedViews、mViewCacheExtension、mRecyclerPool;
其中:

  1. mAttachedScrap不参与回收复用,只保存从在重新布局时,从RecyclerView中剥离的当前在显示的HolderView列表。
  2. 所以,mCachedViews、mViewCacheExtension、mRecyclerPool组成了回收复用的三级缓存,当RecyclerView要拿一个复用的HolderView时,获取优先级是mCachedViews > mViewCacheExtension > mRecyclerPool。由于一般而言我们是不会自定义mViewCacheExtension的。所以获取顺序其实就是mCachedViews > mRecyclerPool,在下面的讲述中,我也将不再牵涉mViewCacheExtension,大家这里知道即可。
  3. 其实,mCachedViews是不参与回收复用的,它的作用就是保存最新被移除的HolderView(通过removeAndRecycleView(view, recycler)方法),它的作用是在需要新的HolderView时,精确匹配是不是刚移除的那个,如果是,就直接返回给RecyclerView展示,如果不是它,那么即使这里有HolderView实例,也不会返回给RecyclerView,而是到mRecyclerPool中去找一个HolderView实例,返回给RecyclerView,让它重新绑定数据使用。
  4. 所以,在mAttachedScrap、mCachedViews中的holderView都是精确匹配的,真正被标识为废弃的是存放在mRecyclerPool中的holderView,当我们向RecyclerView申请一个HolderView来使用的时,如果在mAttachedScrap、mCachedViews精确匹配不到,即使他们中有HolderView也不会返回给我们使用,而是会到mRecyclerPool中去拿一个废弃的HolderView返回给我们。

4、RecyclerView的复用完整过程

上面简单讲解了几个池子的作用以后,我们再重新看下在RecyclerView需要一个HolderView的过程:
要从RecyclerView中拿到一个HolderView用来布局,我们一般是使用recycler.getViewForPosition(int position),它的意思就是给指定位置获取一个HolderView实例。recycler.getViewForPosition(int position)获取过程就比较有意思,它会先在mAttachedScrap中找,看要的View是不是刚刚剥离的,如果是就直接返回使用,如果不是,先在mCachedViews中查找,因为在mCachedViews中精确匹配,如果匹配到,就说明这个HolderView是刚刚被移除的,也直接返回,如果匹配不到就会最终到mRecyclerPool找,如果mRecyclerPool有现成的holderView实例,这时候就不再是精确匹配了,只要有现成的holderView实例就返回给我们使用,只有在mRecyclerPool为空时,才会调用onCreateViewHolder新建。

这里需要注意的是,在mAttachedScrapmCachedViews中拿到的HolderView,因为都是精确匹配的,所以都是直接使用,不会调用onBindViewHolder重新绑定数据,只有在mRecyclerPool中拿到的HolderView才会重新绑定数据。正是有mCachedViews的存在,所以只有在RecyclerView来回滚动时,池子的使用效率最高,因为凡是从mCachedViews中取的HolderView是直接使用的,不需要重新绑定数据。

RecyclerView的回收复用简要过程就是上面的内容了,过程初理解起来还是比较费劲的,大家需要多读几遍。下面我们将通过代码来讲解自定义CustomLayout的回收复用过程。

5、几个函数

  • public void detachAndScrapAttachedViews(Recycler recycler)
    仅用于onLayoutChildren中,在布局前,将所有在显示的HolderView从RecyclerView中剥离,将其放在mAttachedScrap中,以供重新布局时使用

  • View view = recycler.getViewForPosition(position)
    用于向RecyclerView申请一个HolderView,至于这个HolderView是从四个池子中的哪个池子里拿的,我们不需要关心,这些都是recycler.getViewForPosition(position)函数自己判断的,非常方便有没有,正是这个函数能为我们实现复用。

  • removeAndRecycleView(child, recycler)
    这个函数仅用于滚动的时候,在滚动时,我们需要把滚出屏幕的HolderView标记为Removed,这个函数的作用就是把已经不需要的HolderView标记为Removed。,想必大家在理解了上面的回收复用原理以后,也知道在我们把它标记为Removed以后,系统做了什么事了。在我们标记为Removed以为,会把这个HolderView移到mCachedViews中,如果mCachedViews已满,就利用先进先出原则,将mCachedViews中老的holderView移到mRecyclerPool中,然后再把新的HolderView加入到mCachedViews中。

可以看到,正是这三个函数的使用,可以让我们自定义的LayoutManager具有复用功能。

另外,还有几个常用,但经常出错的函数:

  • int getItemCount()
    得到的是Adapter中总共有多少数据要显示,也就是总共有多少个item

  • int getChildCount()
    得到的是当前RecyclerView在显示的item的个数,所以这就是getChildCount()getItemCount()的区别

  • View getChildAt(int position)
    获取某个可见位置的View,需要非常注意的是,它的位置索引并不是Adapter中的位置索引,而是当前在屏幕上的位置的索引。也就是说,要获取当前屏幕上在显示的第一个item的View,应该用getChidAt(0),同样,如果要得到当前屏幕上在显示的最后一个item的View,应该用getChildAt(getChildCount()-1)

  • int getPosition(View view)
    这个函数用于得到某个View在Adapter中的索引位置,我们经常将它与getChildAt(int position)联合使用,得到某个当前屏幕上在显示的View在Adapter中的位置,比如我们要拿到屏幕上在显示的最后一个View在Adapter中的索引:

View lastView = getChildAt(getChildCount() - 1);
int pos = getPosition(lastView);
1.2.2 CustomLayoutManager实现回收复用的原理

从上面的原理中可以看到,回收复用主要有两部分:

第一:在onLayoutChildren初始布局时:

  1. 使用 detachAndScrapAttachedViews(recycler)将所有的可见HolderView剥离
  2. 一屏中能放几个item就获取几个HolderView,撑满初始化的一屏即可,不要多创建

第二:在scrollVerticallyBy滑动时:

  1. 先判断在滚动dy距离后,哪些holderView需要回收,如果需要回收就调用removeAndRecycleView(child, recycler)先将它回收。
  2. 然后向系统获取HolderView对象来填充滚动出来的空白区域

下面我们就利用这个原理来实现CustomLayoutManager的回收复用功能。

二、 给CustomLayoutManager添加回收复用

2.1 修改onLayoutChildren

上面已经提到,在onLayoutChildren中,我们主要做两件事:

  1. 使用 detachAndScrapAttachedViews(recycler)将所有的可见HolderView剥离
  2. 一屏中能放几个item就获取几个HolderView,撑满初始化的一屏即可,不要多创建

关键问题在于,我们怎么知道在初始化时撑满一屏需要多少个item呢?
在这里,每个item的高度都是一致的,所以,只需要用RecyclerView的高度除以每个item的高度,就得到了能显示多少个item了。

所以,此时代码应该是:

private int mItemWidth,mItemHeight;
@Override
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值