掌握这17张图,没人比你更懂RecyclerView的预加载广西博白最大老板是谁

作者: 小吴 Mon Nov 27 01:04:07 SGT 2023
阅读(38)
上一篇文章,我们为了减少描述问题的维度,于演示之前附加了许多限制条件,比如禁用了RecyclerView的预拉取机制。实际上,预拉取(prefetch)机制作为RecyclerView的重要特性之一,常常与缓存复用机制一起配合使用、共同协作,极大地提升了RecyclerView整体滑动的流畅度。并且,这种特性在ViewPager2中同样得以保留,对ViewPager2滑动效果的呈现也起着关键性的作用。因此,我们ViewPager2系列的第二篇,就是要来着重介绍RecyclerView的预拉取机制。预拉取是指什么?在计算机术语中,预拉取指的是在已知需要某部分数据的前提下,利用系统资源闲置的空档,预先拉取这部分数据到本地,从而提高执行时的效率。具体到RecyclerView预拉取的情境则是:利用UI线程正好处于空闲状态的时机预先拉取待进入屏幕区域内的一部分列表项视图并缓存起来从而减少因视图创建或数据绑定等耗时操作所引起的卡顿。预拉取是怎么实现的?正如把缓存复用的实际工作委托给了其内部的Recycler类一样,RecyclerView也把预拉取的实际工作委托给了一个名为GapWorker的类,其内部的工作流程,可以用以下这张思维导图来概括:接下来我们就循着这张思维导图,来一一拆解预拉取的工作流程。1.发起预拉取工作通过查找对GapWorker对象的引用,我们可以梳理出3个发起预拉取工作的时机,分别是:RecyclerView被拖动(Drag)时@OverridepublicbooleanonTouchEvent(MotionEvente){...switch(action){...caseMotionEvent.ACTION_MOVE:{...if(mScrollState==SCROLL_STATE_DRAGGING){...//处于拖动状态并且存在有效的拖动距离时if(mGapWorker!=null&&(dx!=0||dy!=0)){mGapWorker.postFromTraversal(this,dx,dy);}}}break;...}...returntrue;}RecyclerView惯性滑动(Fling)时101-1.gifclassViewFlingerimplementsRunnable{...@Overridepublicvoidrun(){...if(!smoothScrollerPending&&doneScrolling){...}else{...if(mGapWorker!=null){mGapWorker.postFromTraversal(RecyclerView.this,consumedX,consumedY);}}}...}RecyclerView嵌套滚动时privatevoidnestedScrollByInternal(intx,inty,@NullableMotionEventmotionEvent,inttype){...if(mGapWorker!=null&&(x!=0||y!=0)){mGapWorker.postFromTraversal(this,x,y);}...}2.执行预拉取工作GapWorker是Runnable接口的一个实现类,意味着其执行工作的入口必然是在run方法。finalclassGapWorkerimplementsRunnable{@Overridepublicvoidrun(){...prefetch(nextFrameNs);...}}在run方法内部我们可以看到其调用了一个prefetch方法,在进入该方法之前,我们先来分析传入该方法的参数。//查询最近一个垂直同步信号发出的时间,以便我们可以预测下一个finalintsize=mRecyclerViews.size();longlatestFrameVsyncMs=0;for(inti=0;i1ms)Displaydisplay=ViewCompat.getDisplay(this);floatrefreshRate=60.0f;//默认的刷新率为60fpsif(!isInEditMode()&&display!=null){floatdisplayRefreshRate=display.getRefreshRate();if(displayRefreshRate>=30.0f){refreshRate=displayRefreshRate;}}mGapWorker.mFrameIntervalNs=(long)(1000000000/refreshRate);//1000000000纳秒=1秒也即假定在默认60fps的刷新率下,每一帧刷新的间隔时间应为16.67ms。再由该方法的形参命名deadlineNs可知,传入的参数表示的是预抓取工作完成的最后期限:voidprefetch(longdeadlineNs){...}综合一下就是,预抓取的工作必须在下一个垂直同步信号发出之前,也即下一帧开始绘制之前完成。什么意思呢?这是由于从Android5.0(API等级21)开始,出于提高UI渲染效率的考虑,Android系统引入了RenderThread机制,即渲染线程。这个机制负责接管原先主线程中繁重的UI渲染工作,使得主线程可以更加专注于与用户的交互,从而大幅提高页面的流畅度。但这里有一个问题。当UI线程提前完成工作,并将一个帧传递给RenderThread渲染之后,就会进入所谓的休眠状态,出现了大量的空闲时间,直至下一帧开始绘制之前。如图所示:一方面,这些UI线程上的空闲时间并没有被利用起来,相当于珍贵的线程资源被白白浪费掉;另一方面,新的列表项进入屏幕时,又需要在UI线程的输入阶段(Input)就完成视图创建与数据绑定的工作,这会推迟UI线程及RenderThread上的其他工作,如果这些被推迟的工作无法在下一帧开始绘制之前完成,就有可能造成界面上的丢帧卡顿。GapWorker正是选择在此时间窗口内安排预拉取的工作,也即把创建和绑定的耗时操作,移到UI线程的空闲时间内完成,与原先的RenderThread并行执行。但这个预拉取的工作同样必须在下一帧开始绘制之前完成,否则预拉取的列表项视图还是会无法被及时地绘制出来,进而导致丢帧卡顿,于是才有了前面表示最后期限的传入参数。了解完这个参数的含义后,让我们继续往下阅读源码。2.1构建预拉取任务列表voidprefetch(longdeadlineNs){buildTaskList();...}进入prefetch方法后可以看到,预拉取的第一个动作就是先构建预拉取的任务列表,其内部又可分为以下3个事项:2.1.1收集预拉取的列表项数据privatevoidbuildTaskList(){//1.收集预拉取的列表项数据finalintviewCount=mRecyclerViews.size();inttotalTaskCount=0;for(inti=0;i0?LayoutState.LAYOUT_END:LayoutState.LAYOUT_START;finalintabsDelta=Math.abs(delta);//收集与预拉取相关的重要数据,并存储到LayoutStateupdateLayoutState(layoutDirection,absDelta,true,state);collectPrefetchPositionsForLayoutState(state,mLayoutState,layoutPrefetchRegistry);}}这一事项主要是依据RecyclerView滚动的方向,收集即将进入屏幕的、待预拉取的列表项数据,其中,最关键的2项数据是:待预拉取项的position值——用于预加载项位置的确定待预拉取项与RecyclerView可见区域的距离——用于预拉取任务的优先级排序我们以最简单的LinearLayoutManager为例,看一下这2项数据是怎样收集的,其最关键的实现就在于前面的updateLayoutState方法。假定此时我们的手势是向上滑动的,则其进入的是layoutToEnd==true的判断:privatevoidupdateLayoutState(intlayoutDirection,intrequiredSpace,booleancanUseExistingSpace,RecyclerView.Statestate){...if(layoutToEnd){...//步骤1,获取滚动方向上的第一个项finalViewchild=getChildClosestToEnd();//步骤2,确定待预拉取项的方向mLayoutState.mItemDirection=mShouldReverseLayout?LayoutState.ITEM_DIRECTION_HEAD:LayoutState.ITEM_DIRECTION_TAIL;//步骤3,确认待预拉取项的positionmLayoutState.mCurrentPosition=getPosition(child)+mLayoutState.mItemDirection;mLayoutState.mOffset=mOrientationHelper.getDecoratedEnd(child);//步骤4,确认待预拉取项与RecyclerView可见区域的距离scrollingOffset=mOrientationHelper.getDecoratedEnd(child)-mOrientationHelper.getEndAfterPadding();}else{...}...mLayoutState.mScrollingOffset=scrollingOffset;}步骤1,获取RecyclerView滚动方向上的第一项,如图中①所示:步骤2,确定待预拉取项的方向。不用反转布局的情况下是ITEM_DIRECTION_TAIL,该值等于1,如图中②所示:步骤3,确认待预拉取项的position值。由滚动方向上的第一项的position值加上步骤2确定的方向值相加得到,对应的是RecyclerView待进入屏幕区域的下一个项,如图中③所示:步骤4,确认待预拉取项与RecyclerView可见区域的距离,该值由以下2个值相减得到:getEndAfterPadding:指的是RecyclerView去除了Padding后的底部位置,并不完全等于RecyclerView的高度。getDecoratedEnd:指的是由列表项的底部位置,加上列表项设立的外边距,再加上列表项间隔的高度计算得到的值。我们用一张图来说明一下:首先,图中的①表示一个完整的屏幕可见区域,其中:深灰色区域对应的是RecyclerView设立的上下内边距,即Padding值。中灰色区域对应的是RecyclerView的列表项分隔线,即Decoration。浅灰色区域对应的是每一个列表项设立的外边距,即Margin值。RecyclerView的实际可见区域,是由虚线a和虚线b所包围的区域,即去除了上下内边距之后的区域。getEndAfterPadding方法返回的值,即是虚线b所在的位置。图中的②是对RecyclerView底部不可见区域的透视图,假定现在position=2的列表项的底部正好贴合到RecyclerView可见区域的底部,则getDecoratedEnd方法返回的值,即是虚线c所在的位置。接下来,如果按前面的步骤4进行计算,即用虚线c所在的位置减去的虚线b所在的位置,得到的就是图中的③,即刚好是列表项的外边距加上分隔线的高度。这个结果就是待预拉取列表项与RecyclerView可见区域的距离。随着向上滑动的手势这个距离值逐渐变小,直到正好进入RecyclerView的可见区域时变为0,随后开始预加载下一项。这2项数据收集到之后,就会调用GapWorker的addPosition方法,以交错的形式存放到一个int数组类型的mPrefetchArray结构中去:@OverridepublicvoidaddPosition(intlayoutPosition,intpixelDistance){...//根据实际需要分配新的数组,或以2的倍数扩展数组大小finalintstoragePosition=mCount*2;if(mPrefetchArray==null){mPrefetchArray=newint[4];Arrays.fill(mPrefetchArray,-1);}elseif(storagePosition>=mPrefetchArray.length){finalint[]oldArray=mPrefetchArray;mPrefetchArray=newint[storagePosition*2];System.arraycopy(oldArray,0,mPrefetchArray,0,oldArray.length);}//交错存放position值与距离mPrefetchArray[storagePosition]=layoutPosition;mPrefetchArray[storagePosition+1]=pixelDistance;mCount++;}需要注意的是,RecyclerView每次的预拉取并不限于单个列表项,实际上,它可以一次获取多个列表项,比如使用了GridLayoutManager的情况。2.1.2根据预拉取的数据填充任务列表privatevoidbuildTaskList(){...//2.根据预拉取的数据填充任务列表inttotalTaskIndex=0;for(inti=0;i=mTasks.size()){task=newTask();mTasks.add(task);}else{task=mTasks.get(totalTaskIndex);}finalintdistanceToItem=prefetchRegistry.mPrefetchArray[j+1];//与RecyclerView可见区域的距离小于滑动的速度,该列表项必定可见,任务需要立即执行task.immediate=distanceToItem<=viewVelocity;task.viewVelocity=viewVelocity;task.distanceToItem=distanceToItem;task.view=view;task.position=prefetchRegistry.mPrefetchArray[j];totalTaskIndex++;}}...}Task是负责存储预拉取任务数据的实体类,其所包含属性的含义分别是:position:待预加载项的Position值distanceToItem:待预加载项与RecyclerView可见区域的距离viewVelocity:RecyclerView的滑动速度,其实就是滑动距离immediate:是否立即执行,判断依据是与RecyclerView可见区域的距离小于滑动的速度view:RecyclerView本身从第2个for循环可以看到,其是以2为偏移量进行遍历,从mPrefetchArray中分别取出前面存储的position值与距离的。2.1.3对任务列表进行优先级排序填充任务列表完毕后,还要依据实际情况对任务进行优先级排序,其遵循的基本原则就是:越可能快进入RecyclerView可见区域的列表项,其预加载的优先级越高。privatevoidbuildTaskList(){...//3.对任务列表进行优先级排序Collections.sort(mTasks,sTaskComparator);}staticComparatorsTaskComparator=newComparator(){@Overridepublicintcompare(Tasklhs,Taskrhs){//首先,优先处理未清除的任务if((lhs.view==null)!=(rhs.view==null)){returnlhs.view==null?1:-1;}//然后考虑需要立即执行的任务if(lhs.immediate!=rhs.immediate){returnlhs.immediate?-1:1;}//然后考虑滑动速度更快的intdeltaViewVelocity=rhs.viewVelocity-lhs.viewVelocity;if(deltaViewVelocity!=0)returndeltaViewVelocity;//最后考虑与RecyclerView可见区域距离最短的intdeltaDistanceToItem=lhs.distanceToItem-rhs.distanceToItem;if(deltaDistanceToItem!=0)returndeltaDistanceToItem;return0;}};2.2调度预拉取任务voidprefetch(longdeadlineNs){...flushTasksWithDeadline(deadlineNs);}预拉取的第二个动作,则是将前面填充并排序好的任务列表依次调度执行:privatevoidflushTasksWithDeadline(longdeadlineNs){for(inti=0;i=mViewCacheMax&&cachedViewSize>0){//当前已经放满recycleCachedViewAt(0);//移除mCachedView结构中的第1个cachedViewSize--;//总数减1}//默认从尾部添加inttargetCacheIndex=cachedViewSize;//处理预拉取的情况if(ALLOW_THREAD_GAP_WORK&&cachedViewSize>0&&!mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)){//从最后一个开始,跳过所有最近预拉取的对象排在其前面intcacheIndex=cachedViewSize-1;while(cacheIndex>=0){intcachedPos=mCachedViews.get(cacheIndex).mPosition;//添加到最近一个非预拉取的对象后面if(!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)){break;}cacheIndex--;}targetCacheIndex=cacheIndex+1;}mCachedViews.add(targetCacheIndex,holder);也就是说,虽然缓存复用的对象和预拉取的对象共用同一个mCachedViews结构,但二者是分组存放的,且缓存复用的对象是排在预拉取的对象前面的。这么说或许还是很难理解,我们用几张示意图来演示一下就懂了:1.假定现在mCachedViews中同时有2种类型的ViewHolder对象,黑色的代表缓存复用的对象,白色的代表预拉取的对象;2.现在,有另外一个缓存复用的对象想要放到mCachedViews中,按源码的做法,默认会从尾部添加,即targetCacheIndex=3:a13.png3.随后,需要进一步确认放入的位置,它会从尾部开始逐个遍历,判断是否是预拉取的ViewHolder对象,判断的依据是该ViewHolder对象的position值是否存在mPrefetchArray结构中:booleanlastPrefetchIncludedPosition(intposition){if(mPrefetchArray!=null){finalintcount=mCount*2;for(inti=0;i