更详细文档入口
1.RecyclerView基础组件
核心类 | 作用 |
---|---|
LayoutManager | 管理ItemView的布局显示 |
ViewHodler | ItemView的容器 |
ItemDecoration | ItemView边界绘制,比如分割线 |
ItemAnimator | ItemView的添加、删除、移动动画 |
Recycler | 管理ItemView的回收和复用 |
2.RecyclerView与ListView的差异
实现了可回收复用的ViewHolder |
实现的Adapter返回的是ViewHolder而不是View |
通过LayoutManager来统一管理布局 |
实现了针对Item的增删动画ItemAnimator类 |
实现了针对Item间隔的配置ItemDecoration类 |
支持局部刷新 |
3.RecyclerView的四级缓存
层级 | 参数 | 使用场景 |
---|---|---|
一级 | mAttachedScrap | 添加和删除ItemView刷新界面时使用 |
一级 | mChangedScrap | 执行动画时使用 |
二级 | mCachedViews | 默认容量是2,超过时会被保存到mRecyclerPool,ItemView超出屏幕范围后先回收到mCachedViews |
三级 | mViewCacheExtension | 开发者自己扩展使用 |
四级 | mRecyclerPool | 当mCachedViews缓存满时,默认每个type类型最多缓存5个 |
4.RecyclerView缓存策略分析
Recyclerview版本:
implementation "androidx.recyclerview:recyclerview:1.2.1"
接下来通过一个简单的Demo来验证分析RecyclerView的缓存策略
private void initView(View root) {
mRecyclerView = root.findViewById(R.id.recycler_view);
mRecyclerView.setAdapter(new FVAdapter());
GridLayoutManager layoutManager = new GridLayoutManager(getActivity(), 1);
layoutManager.setItemPrefetchEnabled(false);
mRecyclerView.setLayoutManager(layoutManager);
}
private void initData() {
mData = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
mData.add(i);
}
}
public class FVAdapter extends RecyclerView.Adapter {
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
Button view = new Button(getActivity());
view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 216));
Log.d(TAG, "onCreateViewHolder");
return new VH(view);
}
@SuppressLint({"SetTextI18n", "ResourceAsColor"})
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
VH vh = (VH) holder;
Button view = (Button) vh.itemView;
view.setText("View Position:" + position + "\nHolder Index:" + vh.vh_index);
Log.d(TAG, "onBindViewHolder view position:" + position + " Holder Index:" + vh.vh_index);
}
@Override
public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {
super.onViewRecycled(holder);
VH vh = (VH) holder;
Log.d(TAG, "onViewRecycled Holder Index:" + vh.vh_index);
}
@Override
public int getItemCount() {
return mData.size();
}
}
public int index = 0;
public class VH extends RecyclerView.ViewHolder {
public int vh_index;
public VH(@NonNull View itemView) {
super(itemView);
vh_index = index++;
}
}
Demo代码很简单,创建1000个Item,每个Item上显示位置和ViewHolder的下标。每屏最多显示5个Item:
mAttachedScrap、mChangedScrap仅在触发onLayout时影响缓存,在滚动场景下,分析流程时可先排除对缓存的影响。
另外,LayoutManager有两个默认属性,会影响缓存策略:
public final void setItemPrefetchEnabled(boolean enabled) {
if (enabled != mItemPrefetchEnabled) {
mItemPrefetchEnabled = enabled;
mPrefetchMaxCountObserved = 0;
if (mRecyclerView != null) {
mRecyclerView.mRecycler.updateViewCacheSize();
}
}
}
/**
* Number of items to prefetch when first coming on screen with new data.
*/
private int mInitialPrefetchItemCount = 2;
public void setInitialPrefetchItemCount(int itemCount) {
mInitialPrefetchItemCount = itemCount;
}
从定义上看,mCacheViews的size默认为2,mRecyclerPool的每个Type对应的size默认为5个。
int mViewCacheMax = DEFAULT_CACHE_SIZE;
static final int DEFAULT_CACHE_SIZE = 2;
public static class RecycledViewPool {
private static final int DEFAULT_MAX_SCRAP = 5;
...
}
为了方便分析,设置setItemPrefetchEnabled(false),排除缓存size动态变化对分析流程的额外影响:
1 | mCacheViews和mRecyclerPool中未命中缓存,新建一个ViewHolder |
2 | mCacheViews中有缓存,但是位置不匹配,未命中缓存,新建一个ViewHolder |
3 | mCacheViews中满缓存,但是位置不匹配,未命中缓存,新建一个ViewHolder |
4 | mCacheViews中满缓存,但是位置不匹配;mRecyclerPool有缓存,命中type对应的缓存,使用缓存且调研onBindViewHolder方法 |
按以上方式分析,理论上在滚动时最多只需要创建8个(5 + 2 + 1)ViewHolder,但事实上是这样吗?
首先看下手机端的手指滑动场景:
14:50:20.264 onCreateViewHolder
14:50:20.266 onBindViewHolder view position:0 Holder Index:0
14:50:20.278 onCreateViewHolder
14:50:20.278 onBindViewHolder view position:1 Holder Index:1
14:50:20.283 onCreateViewHolder
14:50:20.284 onBindViewHolder view position:2 Holder Index:2
14:50:20.289 onCreateViewHolder
14:50:20.290 onBindViewHolder view position:3 Holder Index:3
14:50:20.295 onCreateViewHolder
14:50:20.295 onBindViewHolder view position:4 Holder Index:4
14:50:26.702 onCreateViewHolder
14:50:26.704 onBindViewHolder view position:5 Holder Index:5
14:50:26.749 onCreateViewHolder
14:50:26.751 onBindViewHolder view position:6 Holder Index:6
14:50:26.779 onCreateViewHolder
14:50:26.780 onBindViewHolder view position:7 Holder Index:7
14:50:26.786 onViewRecycled Holder Index:0
14:50:26.788 onBindViewHolder view position:8 Holder Index:0
14:50:26.801 onViewRecycled Holder Index:1
14:50:26.803 onBindViewHolder view position:9 Holder Index:1
14:50:26.810 onViewRecycled Holder Index:2
14:50:26.811 onBindViewHolder view position:10 Holder Index:2
14:50:26.823 onViewRecycled Holder Index:3
14:50:26.825 onBindViewHolder view position:11 Holder Index:3
14:50:26.840 onViewRecycled Holder Index:4
14:50:26.843 onBindViewHolder view position:12 Holder Index:4
14:50:26.874 onViewRecycled Holder Index:5
14:50:26.876 onBindViewHolder view position:13 Holder Index:5
14:50:26.890 onViewRecycled Holder Index:6
14:50:26.892 onBindViewHolder view position:14 Holder Index:6
14:50:26.907 onViewRecycled Holder Index:7
14:50:26.909 onBindViewHolder view position:15 Holder Index:7
14:50:26.923 onViewRecycled Holder Index:0
14:50:26.926 onBindViewHolder view position:16 Holder Index:0
手指无论如何滚动,最多只创建8个ViewHolder,和预期一致。然而,通过实体键(比如电视端使用遥控器)却出现了不一致的情况:
1.慢速滚动
17:20:56.628 onCreateViewHolder
17:20:56.628 onBindViewHolder view position:0 Holder Index:0
17:20:56.631 onCreateViewHolder
17:20:56.631 onBindViewHolder view position:1 Holder Index:1
17:20:56.634 onCreateViewHolder
17:20:56.635 onBindViewHolder view position:2 Holder Index:2
17:20:56.639 onCreateViewHolder
17:20:56.639 onBindViewHolder view position:3 Holder Index:3
17:20:56.644 onCreateViewHolder
17:20:56.644 onBindViewHolder view position:4 Holder Index:4
17:20:59.294 onCreateViewHolder
17:20:59.294 onBindViewHolder view position:5 Holder Index:5
17:21:00.194 onCreateViewHolder
17:21:00.195 onBindViewHolder view position:6 Holder Index:6
17:21:00.886 onCreateViewHolder
17:21:00.886 onBindViewHolder view position:7 Holder Index:7
17:21:01.157 onViewRecycled Holder Index:0
17:21:01.814 onBindViewHolder view position:8 Holder Index:0
17:21:02.073 onViewRecycled Holder Index:1
17:21:02.624 onBindViewHolder view position:9 Holder Index:1
17:21:02.890 onViewRecycled Holder Index:2
17:21:04.544 onBindViewHolder view position:10 Holder Index:2
17:21:04.808 onViewRecycled Holder Index:3
17:21:06.044 onBindViewHolder view position:11 Holder Index:3
17:21:06.307 onViewRecycled Holder Index:4
17:21:06.973 onBindViewHolder view position:12 Holder Index:4
17:21:07.240 onViewRecycled Holder Index:5
17:21:07.814 onBindViewHolder view position:13 Holder Index:5
17:21:08.073 onViewRecycled Holder Index:6
17:21:08.714 onBindViewHolder view position:14 Holder Index:6
17:21:08.974 onViewRecycled Holder Index:7
17:21:09.583 onBindViewHolder view position:15 Holder Index:7
17:21:09.857 onViewRecycled Holder Index:0
17:21:10.484 onBindViewHolder view position:16 Holder Index:0
17:21:10.758 onViewRecycled Holder Index:1
在实体键慢速滚动场景下,结果与预期一致。
2.快速滚动
17:22:48.759 onCreateViewHolder
17:22:48.760 onBindViewHolder view position:0 Holder Index:0
17:22:48.762 onCreateViewHolder
17:22:48.763 onBindViewHolder view position:1 Holder Index:1
17:22:48.765 onCreateViewHolder
17:22:48.766 onBindViewHolder view position:2 Holder Index:2
17:22:48.769 onCreateViewHolder
17:22:48.770 onBindViewHolder view position:3 Holder Index:3
17:22:48.773 onCreateViewHolder
17:22:48.773 onBindViewHolder view position:4 Holder Index:4
17:22:51.242 onCreateViewHolder
17:22:51.243 onBindViewHolder view position:5 Holder Index:5
17:22:51.298 onCreateViewHolder
17:22:51.298 onBindViewHolder view position:6 Holder Index:6
17:22:51.347 onCreateViewHolder
17:22:51.348 onBindViewHolder view position:7 Holder Index:7
17:22:51.399 onCreateViewHolder
17:22:51.400 onBindViewHolder view position:8 Holder Index:8
17:22:51.455 onCreateViewHolder
17:22:51.455 onBindViewHolder view position:9 Holder Index:9
17:22:51.510 onCreateViewHolder
17:22:51.510 onBindViewHolder view position:10 Holder Index:10
17:22:51.519 onViewRecycled Holder Index:0
17:22:51.551 onBindViewHolder view position:11 Holder Index:0
17:22:51.577 onViewRecycled Holder Index:1
17:22:51.596 onBindViewHolder view position:12 Holder Index:1
17:22:51.627 onViewRecycled Holder Index:2
17:22:51.647 onBindViewHolder view position:13 Holder Index:2
17:22:51.660 onViewRecycled Holder Index:3
17:22:51.699 onBindViewHolder view position:14 Holder Index:3
17:22:51.708 onViewRecycled Holder Index:4
17:22:51.748 onBindViewHolder view position:15 Holder Index:4
17:22:51.758 onViewRecycled Holder Index:5
一共创建了11个ViewHolder,和预期不符,这是为什么呢?
首先,先看看按键事件是如何实现RecyclerView滚动的:
图一中标记1,会根据按键方向找到合适的焦点:
找不到焦点时,会先去查找mCachesView中是否有对应Position的缓存,如果没有再从mRecyclerPool中查找是否对应Type的缓存,如果没有命中缓存,则会新建一个ViewHolder,然后bindViewHolder,这个ViewHolder就是即将获取焦点的ItemView。
查找到可获取焦点的ItemView后,根据图一中的流程,因为要requestFocus,最终会通过标记2流程实现RecyclerView的实现。
再看下postOnAnimation实现:
public void postOnAnimation(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
attachInfo.mViewRootImpl.mChoreographer.postCallback(
Choreographer.CALLBACK_ANIMATION, action, null);
} else {
// Postpone the runnable until we know
// on which thread it needs to run.
getRunQueue().post(action);
}
}
简述下Choreographer绘制流程:
因此图一标记2位置实现RecyclerView的滚动是在doFrame队列中CALLBACK_ANIMATION阶段时才得到了处理。
在此滚动阶段才可能完成ViewHolder的回收,流程如下:
以下是一张比较抽象的流程图,主要说明在按键事件下Recycler的大概流程:
有一个关键函数fill,在图二、图四中都会调用到:
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
// max offset we should set is mFastScroll + available
final int start = layoutState.mAvailable;
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
// TODO ugly bug fix. should not happen
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
...
}
图二流程调用如下,在未找到可获取焦点子控件失败时尝试填充新的子控件,其中滚动的最大距离maxScroll是RecyclerView最高高度的MAX_SCROLL_FACTOR(三分之一):
private static final float MAX_SCROLL_FACTOR = 1 / 3f;
...
@Override
public View onFocusSearchFailed(View focused, int focusDirection,
RecyclerView.Recycler recycler, RecyclerView.State state) {
...
final int maxScroll = (int) (MAX_SCROLL_FACTOR * mOrientationHelper.getTotalSpace());
updateLayoutState(layoutDir, maxScroll, false, state);
// 在调用fill前把mScrollingOffset置为无效,此次不会回收ViewHolder
mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN;
mLayoutState.mRecycle = false;
fill(recycler, mLayoutState, state, true);
...
}
图四流程调用如下,在滚动时尝试填充新的子控件:
int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
...
updateLayoutState(layoutDirection, absDelta, true, state);
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
...
final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta;
// 真正实现视觉上的滚动效果
mOrientationHelper.offsetChildren(-scrolled);
...
}
private void updateLayoutState(int layoutDirection, int requiredSpace,
boolean canUseExistingSpace, RecyclerView.State state) {
...
mLayoutState.mScrollingOffset = scrollingOffset;
}
从上面的源码可以看到只有在完成滚动后才可能进行ViewHolder的回收。
可知快速滚动场景下,由于按键事件不断触发,若不能命中缓存,就会不断新建ViewHolder;而只有RecyclerView的滚动需要等到下一次doFrame时才会执行,此后才会决定是否回收ViewHolder。
那么来分析下RecycleView的滚动的具体实现。
实际上RecycleView是在doFrame中一点一点像素滚动的,滚动的距离是通过时间和距离计算出来的:
//计算此次滚动所需要的时间
private int computeScrollDuration(int dx, int dy) {
final int absDx = Math.abs(dx);
final int absDy = Math.abs(dy);
final boolean horizontal = absDx > absDy;
final int containerSize = horizontal ? getWidth() : getHeight();
float absDelta = (float) (horizontal ? absDx : absDy);
final int duration = (int) (((absDelta / containerSize) + 1) * 300);
return Math.min(duration, MAX_SCROLL_DURATION);
}
可以看到动画的时间是通过滚动距离计算出来的,并且一定大于300ms。
//计算此次滚动时刻可以滚动的距离
public boolean computeScrollOffset() {
if (isFinished()) {
return false;
}
switch (mMode) {
case SCROLL_MODE:
long time = AnimationUtils.currentAnimationTimeMillis();
// Any scroller can be used for time, since they were started
// together in scroll mode. We use X here.
final long elapsedTime = time - mScrollerX.mStartTime;
final int duration = mScrollerX.mDuration;
if (elapsedTime < duration) {
final float q = mInterpolator.getInterpolation(elapsedTime / (float) duration);
mScrollerX.updateScroll(q);
mScrollerY.updateScroll(q);
} else {
abortAnimation();
}
break;
...
}
return true;
}
根据图四的流程,再来看看回收ViewHodler的条件, 以列表向上滚动为例:
private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrollingOffset,
int noRecycleSpace) {
...
// ignore padding, ViewGroup may not clip children.
final int limit = scrollingOffset - noRecycleSpace; ...
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (mOrientationHelper.getDecoratedEnd(child) > limit
|| mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
// stop here
recycleChildren(recycler, 0, i);
return;
}
}
}
}
...
private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) {
...
if (endIndex > startIndex) {
for (int i = endIndex - 1; i >= startIndex; i--) {
removeAndRecycleViewAt(i, recycler);
}
} else {
for (int i = startIndex; i > endIndex; i--) {
removeAndRecycleViewAt(i, recycler);
}
}
}
被回收的ViewHolder根据type类型放在List的尾部
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList scrapHeap = getScrapDataForType(viewType).mScrapHeap;
if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
return;
}
if (DEBUG && scrapHeap.contains(scrap)) {
throw new IllegalArgumentException("this scrap item already exists");
}
scrap.resetInternal();
scrapHeap.add(scrap);
}
复用ViewHolder时根据type类型取List尾部的第一个
public ViewHolder getRecycledView(int viewType) {
final ScrapData scrapData = mScrap.get(viewType);
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
final ArrayList scrapHeap = scrapData.mScrapHeap;
for (int i = scrapHeap.size() - 1; i >= 0; i--) {
if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
return scrapHeap.remove(i);
}
}
}
return null;
}
回收的关键在于
mOrientationHelper.getDecoratedEnd(child) > limit || mOrientationHelper.getTransformedEndWithDecoration(child) > limit
在这篇文章的Demo中,以上函数是获取child的最底部(包括bottomMargin)位置,当找到第一个底部大于limit的View时,可以回收所有在这个视图以上的ItemView。
其中limit = scrollingOffset - noRecycleSpace,并且noRecycleSpace在本文中为0,从updateLayoutState可以看出scrollingOffset就是在不添加新的View前可滚动的距离。
上图右侧是用户长按方向键下,每响应一个KeyEvent.KEYCODE_DPAD_DOWN事件,会在RecyclerView中addView一次(有可使用缓存则使用缓存,未命中缓存则新建)。左侧是触发滚动流程,在滚动发生前检查是否有子View的底部高度大于limit,从图中可见即是否大于目标滚动距离,只有大于目标滚动距离时才开始回收ViewHolder。因为在滚动动画执行期间(不小于300ms),如果有按键事件再次触发focusSearch,就会因为addView刷新mScrollingOffset,本应该在动画结束时触发回收的条件将不满足,所以就出现了缓存Size超过预期的现象。
为什么在手指滑动场景时没有这样的现象?
从这里看到相比按键事件,在事件触发后并不需要立即就填充子View,而是在doFrame期间填充的,这样就不会出现limit滚动期间被外界修改的问题,因此ViewHolder的Size总是符合预期的。
5.多行多列
试想一下,长按方向键下快速滚动场景下,ViewHolder的创建和回收情况是怎样的?
优化思路
理论上可以优化成Fling滚动,等到滚动结束后再计算焦点所在的位置。
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
boolean mScrolled = false;
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
mScrolled = false;
View item = recyclerView.findChildViewUnder(0, rowHeight);
if (item != null) {
item.requestFocus();
}
}
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dx != 0 || dy != 0) {
mScrolled = true;
}
}
});
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (event.getRepeatCount() != 0) {
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
mRecyclerView.fling(0, rowHeight);
} else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
mRecyclerView.fling(0, -rowHeight);
}
return true;
} else {
return false;
}
}