1.RecyclerView缓存原理
2.ListView和RecyclerView区别
4.如何让两个 RecyclerView 共用一个缓存,今日头条页面实例
5.如何解决RecyclerView滑动卡顿问题
6.快速滑动RecycleView卡顿解决办法
1.RecyclerView缓存原理
RecyclerView 是 ListView 的升级版本,更加先进和灵活。看名字我们就能看出一点端倪,没错,它主要的特点就是复用。回收的类在LayoutManager
回收原理:
注:官网上貌似把mAttachedScrap、mCachedViews当成一级了,为了方便区分,本文还是把他们当成两级缓存。
缓存涉及对象作用重新创建视图View(onCreateViewHolder)重新绑定数据(onBindViewHolder)
一级缓存mAttachedScrap缓存屏幕中可见范围的ViewHolderfalsefalse
二级缓存mCachedViews缓存滑动时即将与RecyclerView分离的ViewHolder,按子View的position或id缓存,默认最多存放2个falsefalse
三级缓存mViewCacheExtension开发者自行实现的缓存--
四级缓存mRecyclerPoolViewHolder缓存池,本质上是一个SparseArray,其中key是ViewType(int类型),value存放的是 ArrayList< ViewHolder>,默认每个ArrayList中最多存放5个ViewHolderfalsetrue
RecyclerView滑动时会触发onTouchEvent#onMove,回收及复用ViewHolder在这里就会开始
mAttachedScrap(第一屏,可见)----mCachedViews(刚刚移除的)--------mRecyclerPool(总的)
1).它会先在mAttachedScrap中找,看要的View是不是刚刚剥离的,如果是就直接返回使用,
2).如果不是,先在mCachedViews中查找,因为在mCachedViews中精确匹配,如果匹配到,就说明这个HolderView是刚刚被移除的,也直接返回,
3).如果匹配不到就会最终到mRecyclerPool找,如果mRecyclerPool有现成的holderView实例,这时候就不再是精确匹配了,只要有现成的holderView实例就返回给我们使用,只有在mRecyclerPool为空时,才会调用onCreateViewHolder新建。
具体分析
一.mAttachedScrap到底有什么用?
(第一屏,可见),第一次存放。用于插入一个数据进去的时候用到。滑动的时候不用到
二.mCachedViews它的作用就是保存最新被移除的HolderView
自定义ViewCacheExtension缓存作用,适用场景:ViewHolder位置固定、内容固定、数量有限时使用
缓存的存和取的过程:
取的原则:mCachedViews > mRecyclerPool
mAttachedScrap不参与回收复用,只保存从在重新布局时,从RecyclerView中剥离的当前在显示的HolderView列表。
所以,mCachedViews、mViewCacheExtension、mRecyclerPool组成了回收复用的三级缓存,当RecyclerView要拿一个复用的HolderView时,获取优先级是mCachedViews > mViewCacheExtension > mRecyclerPool。由于一般而言我们是不会自定义mViewCacheExtension的。所以获取顺序其实就是mCachedViews > mRecyclerPool,
存放过程:mCachedViews------mRecyclerPool(一个静态类)
在我们标记为Removed以为,会把这个HolderView移到mCachedViews中,如果mCachedViews已满,就利用先进先出原则,将mCachedViews中老的holderView移到mRecyclerPool中,然后再把新的HolderView加入到mCachedViews中。
举例:
上滑动:上面不可见的移动到mCachedViews然后是mRecyclerPool=========调用的方法getViewForPosition()
下面新的可见, 会从到mCachedViews找然后是mRecyclerPool============调用的方法removeAndRecycleView(child, recycler)
为什么这么设计多个缓存?优化效率:
这里需要注意的是,在mAttachedScrap和mCachedViews中拿到的HolderView,因为都是精确匹配的,所以都是直接使用,不会调用onBindViewHolder重新绑定数据,只有在mRecyclerPool中拿到的HolderView才会重新绑定数据。正是有mCachedViews的存在,所以只有在RecyclerView来回滚动时,池子的使用效率最高,因为凡是从mCachedViews中取的HolderView是直接使用的,不需要重新绑定数据。
mRecyclerPool容量是5
mCachedViews容量是2,他们最多是7个,为什么后面一直不用创建了呢?一般只创建一屏!
后面移出一个,然后就填充一个。
源码分析:
ViewgetViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
ViewHoldertryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
if (position <0 || position >=mState.getItemCount()) {
throw new IndexOutOfBoundsException("Invalid item position " + position
+"(" + position +"). Item count:" +mState.getItemCount());
}
boolean fromScrapOrHiddenOrCache =false;
ViewHolder holder =null;
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder !=null;
}
// 1) Find by position from scrap/hidden list/cache
if (holder ==null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder !=null) {
if (!validateViewHolderForOffsetPosition(holder)) {
// recycle holder (and unscrap if relevant) since it can't be used
if (!dryRun) {
// we would like to recycle this but need to make sure it is not used by
// animation logic etc.
holder.addFlags(ViewHolder.FLAG_INVALID);
if (holder.isScrap()) {
removeDetachedView(holder.itemView, false);
holder.unScrap();
}else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
recycleViewHolderInternal(holder);
}
holder =null;
}else {
fromScrapOrHiddenOrCache =true;
}
}
}
if (holder ==null) {
final int offsetPosition =mAdapterHelper.findPositionOffset(position);
if (offsetPosition <0 || offsetPosition >=mAdapter.getItemCount()) {
throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
+"position " + position +"(offset:" + offsetPosition +")."
+"state:" +mState.getItemCount());
}
final int type =mAdapter.getItemViewType(offsetPosition);
// 2) Find from scrap/cache via stable ids, if exists
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder !=null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache =true;
}
}
if (holder ==null &&mViewCacheExtension !=null) {
// We are NOT sending the offsetPosition because LayoutManager does not
// know it.
final View view =mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view !=null) {
holder = getChildViewHolder(view);
if (holder ==null) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+" a view which does not have a ViewHolder");
}else if (holder.shouldIgnore()) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+" a view that is ignored. You must call stopIgnoring before"
+" returning this view.");
}
}
}
if (holder ==null) {// fallback to pool
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
+ position +") fetching from shared pool");
}
holder = getRecycledViewPool().getRecycledView(type);
if (holder !=null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
if (holder ==null) {
long start = getNanoTime();
if (deadlineNs !=FOREVER_NS
&& !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
// abort - we have a deadline we can't meet
return null;
}
holder =mAdapter.createViewHolder(RecyclerView.this, type);
if (ALLOW_THREAD_GAP_WORK) {
// only bother finding nested RV if prefetching
RecyclerView innerView =findNestedRecyclerView(holder.itemView);
if (innerView !=null) {
holder.mNestedRecyclerView =new WeakReference<>(innerView);
}
}
long end = getNanoTime();
mRecyclerPool.factorInCreateTime(type, end - start);
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");
}
}
}
Demo地址:https://github.com/pengcaihua123456/shennandadao/tree/master
onCreateViewHolder()方法执行次数
onBindViewHolder()方法的执行次数
2.ListView和RecyclerView区别
1).缓存机制不一样
RecyclerView中mCacheViews(屏幕外)获取缓存时,是通过匹配pos获取目标位置的缓存,这样做的好处是,当数据源数据不变的情况下,无须重新bindView,而同样是离屏缓存,ListView从mScrapViews根据pos获取相应的缓存,但是并没有直接使用,而是重新getView(即必定会重新bindView)
ListView和RecyclerView最大的区别在于数据源改变时的缓存的处理逻辑,ListView是”一锅端”,将所有的mActiveViews都移入了二级缓存mScrapViews,而RecyclerView则是更加灵活地对每个View修改标志位,区分是否重新bindView。
2).Listview支持,HeaderView 和 FooterView 而RecyclerView支持横竖滑动LayoutManager
3). RecyclerView支持动画
4).局部刷新方式
第一次要createview和bindview()。没有任何缓存
4.如何让两个 RecyclerView 共用一个缓存
通过RecyclewView直接获回收池
RecyclerView.RecycledViewPool recycledViewPool=mRecyclerView.getRecycledViewPool();
使用多个RecyclerView,并且里面有相同item布局时,这时就可以通过setRecycledViewPool()设置同一个RecycledViewPool;
5.如何解决RecyclerView滑动卡顿问题
1)、根据需求修改RecyclerView默认的绘制缓存选项
recyclerView.setItemViewCacheSize(20);recyclerView.setDrawingCacheEnabled(true);recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);
当item会出现频繁的来回滑动时,可以通过setItemViewCacheSize()设置mCachedViews的数量,这个缓存主要是不需要重新进行绑定数据;
典型的是:用空间换时间的方法。
2).使用多个RecyclerView,并且里面有相同item布局时,这时就可以通过setRecycledViewPool()设置同一个RecycledViewPool;
因为,RecycleViewPool用来存放 mCachedViews 移除的ViewHolder。按照 Type 类型,默认对每个Type最多缓存 5 个。重点源码中它是被 public static 修饰,表示可以被其他RecyclerView 共享。
3).当是网格布局的时候,如果一行的item超过五个,需要通过setMaxRecycledViews()去重新设置缓存的最大个数;
4).可以使用setHasStableIds(true)进行设置(同时重写Adapter的getItemID()方法),这时会复用到scrap缓存;
源码里面:
ViewHoldertryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
if (position <0 || position >=mState.getItemCount()) {
throw new IndexOutOfBoundsException("Invalid item position " + position
+"(" + position +"). Item count:" +mState.getItemCount());
}
boolean fromScrapOrHiddenOrCache =false;
ViewHolder holder =null;
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder !=null;
}
5).局部刷新替代全局刷新。避免整个列表的数据更新,只更新受影响的布局。例如,加载更多时,不使用notifyDataSetChanged(),而是使用notifyItemRangeInserted(rangeStart, rangeEnd)
6).滑动监听
主要就是对onScrollStateChanged方法进行监听,然后通知adapter是否加载图片或复杂布局
7).measure()优化和减少requestLayout()调用
当RecyclerView宽高的测量模式都是EXACTLY时,onMeasure()方法不需要执行dispatchLayoutStep1()等方法来进行测量。而当RecyclerView的宽高不确定并且至少一个child的宽高不确定时,要measure两遍。
因此将RecyclerView的宽高模式都设置为EXACTLY有助于优化性能。
protected void onMeasure(int widthSpec, int heightSpec) {
// ......
if (mLayout.isAutoMeasureEnabled()) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}
// ......
}
还有一个方法RecyclerView.setHasFixedSize(true)可以避免数据改变时重新计算RecyclerView的大小
6.快速滑动RecycleView卡顿解决办法
(1)快速滑动RecycleView卡顿原因:
因为,列表上下滑动的时候,RecycleView会在执行复用策略,onCreateViewHolder和onBindViewHolder会执行。item视图创建或数据绑定的方法会随着滑动被多次执行,容易造成卡顿。
(2)解决快速滑动造成的卡顿
一般都采用滑动关闭数据加载优化:主要是设置RecyclerView.addOnScrollListener();通过自定义一个滑动监听类继承onScrollListener抽象类,实现滑动状态改变的方法onScrollStateChanged(recycleview,state),从而实现在滑动过程中不加载,当滚动静止时,刷新界面,实现加载。