怕什么真理无穷,进一寸有一寸的欢喜。 ----------胡适
系列文章: 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的onCreateViewHolder
和onBindViewHolder
中添加上日志:
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;
其中:
- mAttachedScrap不参与回收复用,只保存从在重新布局时,从RecyclerView中剥离的当前在显示的HolderView列表。
- 所以,mCachedViews、mViewCacheExtension、mRecyclerPool组成了回收复用的三级缓存,当RecyclerView要拿一个复用的HolderView时,获取优先级是mCachedViews > mViewCacheExtension > mRecyclerPool。由于一般而言我们是不会自定义mViewCacheExtension的。所以获取顺序其实就是mCachedViews > mRecyclerPool,在下面的讲述中,我也将不再牵涉mViewCacheExtension,大家这里知道即可。
- 其实,mCachedViews是不参与回收复用的,它的作用就是保存最新被移除的HolderView(通过
removeAndRecycleView(view, recycler)
方法),它的作用是在需要新的HolderView时,精确匹配是不是刚移除的那个,如果是,就直接返回给RecyclerView展示,如果不是它,那么即使这里有HolderView实例,也不会返回给RecyclerView,而是到mRecyclerPool中去找一个HolderView实例,返回给RecyclerView,让它重新绑定数据使用。 - 所以,在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
新建。
这里需要注意的是,在mAttachedScrap
和mCachedViews
中拿到的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初始布局时:
- 使用
detachAndScrapAttachedViews(recycler)
将所有的可见HolderView剥离 - 一屏中能放几个item就获取几个HolderView,撑满初始化的一屏即可,不要多创建
第二:在scrollVerticallyBy滑动时:
- 先判断在滚动dy距离后,哪些holderView需要回收,如果需要回收就调用
removeAndRecycleView(child, recycler)
先将它回收。 - 然后向系统获取HolderView对象来填充滚动出来的空白区域
下面我们就利用这个原理来实现CustomLayoutManager的回收复用功能。
二、 给CustomLayoutManager添加回收复用
2.1 修改onLayoutChildren
上面已经提到,在onLayoutChildren中,我们主要做两件事:
- 使用
detachAndScrapAttachedViews(recycler)
将所有的可见HolderView剥离 - 一屏中能放几个item就获取几个HolderView,撑满初始化的一屏即可,不要多创建
关键问题在于,我们怎么知道在初始化时撑满一屏需要多少个item呢?
在这里,每个item的高度都是一致的,所以,只需要用RecyclerView的高度除以每个item的高度,就得到了能显示多少个item了。
所以,此时代码应该是:
private int mItemWidth,mItemHeight;
@Override