RecyclerView源码分析

public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2, NestedScrollingChild3

RecyclerView实现了NestedScrollingChild2, NestedScrollingChild3接口。因为NestedScrollingChild3继承自NestedScrollingChild2,NestedScrollingChild2又继承自NestedScrollingChild,
所以我们只看NestedScrollingChild接口即可。NestedScrollingChild主要是实现了嵌套滑动的功能。
那什么是嵌套滑动呢?即:子View在处理事件的时候,通过回调让父容器也可以处理滚动
嵌套逻辑相关类:
NestedScrollView 实现 NestedScrollingParent、NestedScrollingChild
NestedScrollingParent、NestedScrollingChild
NestedScrollingParentHelper、NestedScrollingChildHelper

嵌套逻辑相关方法:
onNestedPreScroll()、onNestedScroll()
onNestedPreFling()、onNestedFling()

都是在子View中处理的,通过回调方法给的, 没有再走传统的事件分发流程

那我们看下嵌套滑动的流程:

--> RecyclerView.onTouchEvent(MotionEvent e)
       // MotionEvent.ACTION_DOWN
   --> startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
           //找到实现NestedScrollingParent的父控件
       --> return getScrollingChildHelper().startNestedScroll(axes, type);
       // MotionEvent.ACTION_MOVE。
   --> if (dispatchNestedPreScroll(canScrollHorizontally ? dx : 0,canScrollVertically ? dy : 0, mReusableIntPair, mScrollOffset, TYPE_TOUCH)) {
       --> return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
           --> return dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, TYPE_TOUCH);@NestedScrollingChildHelper
               --> ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
                       // 先让实现NestedScrollingParent的父控件的onNestedPreScroll()
                   --> ((NestedScrollingParent) parent).onNestedPreScroll(target, dx, dy, consumed);

          dx -= mReusableIntPair[0];
          dy -= mReusableIntPair[1];
          mNestedOffsets[0] += mScrollOffset[0];
          mNestedOffsets[1] += mScrollOffset[1];
          getParent().requestDisallowInterceptTouchEvent(true);
       }
       // 将父控件移动的距离减去,剩下的就是自己需要滑动的距离了。
   --> mLastTouchX = x - mScrollOffset[0];
       mLastTouchY = y - mScrollOffset[1];
   --> if (scrollByInternal(canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0, e)) {
       --> scrollStep(x, y, mReusableIntPair);
               // mLayout是我们设置的LayoutManager。这里以LinearLayoutManager横向滚动为例。
           --> consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
               --> return scrollBy(dx, recycler, state);@LinearLayoutManager
                       // fill方法非常关键,recyclerView的缓存和复用代码都在这里。。。
                   --> final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false);
                           //缓存
                       --> recycleByLayoutState(recycler, layoutState);
                           //复用
                       --> layoutChunk(recycler, state, layoutState, layoutChunkResult);
                           --> View view = layoutState.next(recycler);
                               --> final View view = recycler.getViewForPosition(mCurrentPosition);
                                   --> return getViewForPosition(position, false);@RecyclerView
                                           // 主要的复用代码
                                       --> return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
                               // 测量,padding、margin、inset(分割线的空间)
                           --> measureChildWithMargins(view, 0, 0);
      
       --> dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset, TYPE_TOUCH, mReusableIntPair);
           --> getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type, consumed);
               --> dispatchNestedScrollInternal(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type, consumed);@NestedScrollingChildHelper
                   --> ViewParentCompat.onNestedScroll(parent, mView, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed);
                       // 调用实现NestedScrollingParent的父控件的onNestedScroll()
                       --> ((NestedScrollingParent) parent).onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
           getParent().requestDisallowInterceptTouchEvent(true);
       }

在上面代码中,我们追溯到了RecyclerView的复用逻辑的代码。那我们就继续深入了解下,RecyclerView是怎么进行复用的。

--> fill(recycler, mLayoutState, state, false)
    --> tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;@RecyclerView
             // 第①步,mChangedScrap是通过动画获取
         --> holder = getChangedScrapViewForPosition(position);
                 //两种方式获取,通过position和stableId获取。stableId是为解决闪烁问题
             --> final ViewHolder holder = mChangedScrap.get(i);

             // 第②步,holder还为null,从mAttachedScrap、mCachedViews,通过position获取。
         --> holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
             --> final ViewHolder holder = mAttachedScrap.get(i);
             --> final ViewHolder holder = mCachedViews.get(i);
    
             // 第③步,holder还为null,从mAttachedScrap、mCachedViews,通过stableId获取。
         --> holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
             --> final ViewHolder holder = mAttachedScrap.get(i);
             --> final ViewHolder holder = mCachedViews.get(i);
    
             // 第④步,自定义复用,用起来比较麻烦,一般不用。缓存和复用都需要自己实现
         --> final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
         --> holder = getChildViewHolder(view);
             --> return getChildViewHolderInt(child);
                 --> return ((LayoutParams) child.getLayoutParams()).mViewHolder;
    
             // 第⑤步,holder还为null,从RecycledViewPool去找
         --> holder = getRecycledViewPool().getRecycledView(type);
                 // getRecycledViewPool() = RecycledViewPool
             --> final ScrapData scrapData = mScrap.get(viewType);
             --> final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
             --> return scrapHeap.remove(i);
    
             // 第⑥步,holder还为null,创建 ViewHolder 对象
         --> holder = mAdapter.createViewHolder(RecyclerView.this, type);
             --> final VH holder = onCreateViewHolder(parent, viewType);
    
             // 第⑦步,去处理数据
         --> bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
             --> mAdapter.bindViewHolder(holder, offsetPosition);

从代码分析可知,RecyclerView的复用是通过以下五种方式: mChangedScrap、mAttachedScrap、mCachedViews、mViewCacheExtension、RecycledViewPool
我们可以将其概括成四类,也就是四级缓存。
1、mChangedScrap、mAttachedScrap:用来缓存还在屏幕内的ViewHolder,局部刷新使用,主要是为了性能考虑。
2、mCachedViews:用来缓存已经移除到屏幕外的ViewHolder。
3、mViewCacheExtension:这个的创建和缓存完全由开发者自己控制,系统是没有向这个里面缓存数据的。
4、RecycledViewPool:ViewHolder的缓存池。

--> fill(recycler, mLayoutState, state, false)
    --> recycleByLayoutState(recycler, layoutState);
        //缓存分为向上滑动和向下滑动。看一个就够了
        --> if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
               recycleViewsFromEnd(recycler, scrollingOffset, noRecycleSpace);
            } else {
               recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
               --> recycleChildren(recycler, childCount - 1, i);
                      //移除和回收view
                   --> removeAndRecycleViewAt(i, recycler);
                       --> recycler.recycleView(view);
                               // 进行mCachedViews和RecycledViewPool缓存
                           --> recycleViewHolderInternal(holder);@Recycler
                                   // mViewCacheMax = DEFAULT_CACHE_SIZE = 2。可通过setViewCacheSize(int viewCount)设置
                                   // 如果mCachedViews已经满了,则取出位置为0的view。
                               --> if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                                       recycleCachedViewAt(0);
                                           // 从mCachedViews取出view,加到pool中。
                                       --> ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
                                           addViewHolderToRecycledViewPool(viewHolder, true);
                                           --> getRecycledViewPool().putRecycledView(holder);
                                                   // 通过itemType去获取ViewHolder集合。
                                               --> final int viewType = scrap.getItemViewType();
                                                   // int mMaxScrap = DEFAULT_MAX_SCRAP = 5。可通过setMaxRecycledViews(int viewType, int max)设置
                                                   final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
                                                       // 如果缓存池也满了,直接return。
                                                       if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
                                                           return;
                                                       }
                                                       // 复位。将viewHolder上的数据信息清除。
                                                       scrap.resetInternal();
                                                       scrapHeap.add(scrap);
                                           mCachedViews.remove(cachedViewIndex);
                                       cachedViewSize--;
                                   }
                                   // 再将新的添加到mCachedViews中
                               --> mCachedViews.add(targetCacheIndex, holder);
                                   // mCachedViews已经满了,那就存到pool中。
                               --> addViewHolderToRecycledViewPool(holder, true);
            }

总结:
cacheView的大小默认是2,从cacheView复用viewHolder,不需要绑定数据,不需调用onBindViewHolder
缓存池的大小默认是5,从缓存池中复用viewHolder,需要重新绑定数据,需调用onBindViewHolder
如果从cacheView和缓存池中没有获取到ViewHolder,则调用onCreateViewHolder

上面我们只看到了cacheView和缓存池这两级缓存,那还有两级缓存是在哪里呢?我们来看下onMeasure和onLayout方法。

--> onMeasure(int widthSpec, int heightSpec)
    --> final boolean measureSpecModeIsExactly = widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
        // 如果设置的宽高模式是EXACTLY,那就直接返回。那具体是在哪里测量的呢?接着看onLayout方法
        if (measureSpecModeIsExactly || mAdapter == null) {
            return;
        }
        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
        }
        dispatchLayoutStep2();
--> onLayout(boolean changed, int l, int t, int r, int b)
    --> dispatchLayout();
        -->  if (mState.mLayoutStep == State.STEP_START) {
                 // 执行dispatchLayoutStep1会设置mState.mLayoutStep值为State.STEP_LAYOUT
                 dispatchLayoutStep1();
                 --> mState.mLayoutStep = State.STEP_LAYOUT;
                 mLayout.setExactMeasureSpecsFrom(this);
                 // 执行dispatchLayoutStep2会设置mState.mLayoutStep值为State.STEP_ANIMATIONS
                 dispatchLayoutStep2();
                 --> mState.mLayoutStep = State.STEP_ANIMATIONS;
             } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth() || mLayout.getHeight() != getHeight()) {
                 mLayout.setExactMeasureSpecsFrom(this);
                 dispatchLayoutStep2();
             } else {
                 mLayout.setExactMeasureSpecsFrom(this);
             }
             dispatchLayoutStep3();

dispatchLayoutStep1()、dispatchLayoutStep2()、dispatchLayoutStep3()都只会执行一次,dispatchLayoutStep3()只在onLayout中有调用。

--> dispatchLayoutStep1()
        // 动画前布局
    --> mViewInfoStore.addToPreLayout(holder, animationInfo);


    //具体的测量和布局都在dispatchLayoutStep2()中
--> dispatchLayoutStep2();
    --> mLayout.onLayoutChildren(mRecycler, mState);
        --> detachAndScrapAttachedViews(recycler);
            --> scrapOrRecycleView(recycler, i, v);
                --> recycler.scrapView(view);
                    -->  if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID) || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
                             if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
                                 throw new IllegalArgumentException("Called scrap view with an invalid view."
                                         + " Invalid views cannot be reused from scrap, they should rebound from"
                                         + " recycler pool." + exceptionLabel());
                             }
                             holder.setScrapContainer(this, false);
                             // 缓存到mAttachedScrap中
                             mAttachedScrap.add(holder);
                         } else {
                             if (mChangedScrap == null) {
                                 mChangedScrap = new ArrayList<ViewHolder>();
                             }
                             holder.setScrapContainer(this, true);
                             // 缓存到mChangedScrap
                             mChangedScrap.add(holder);
                         }
        --> fill(recycler, mLayoutState, state, false);


--> dispatchLayoutStep3()
        // 动画后布局
    --> mViewInfoStore.addToPostLayout(holder, animationInfo);

总结:
mAttachedScrap、mChangedScrap缓存还在屏幕内的ViewHolder

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,458评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,030评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,879评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,278评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,296评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,019评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,633评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,541评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,068评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,181评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,318评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,991评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,670评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,183评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,302评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,655评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,327评论 2 358

推荐阅读更多精彩内容