android 全局缓存,【Android进阶】RecyclerView之缓存(二)
前言
上一篇,说了ItemDecoration,这一篇,我们来说说RecyclerView的回收复用逻辑。
问题
假如有100个item,首屏最多展示2个半(一屏同时最多展示4个),RecyclerView 滑动时,会创建多少个viewholder?
先别急着回答,我们写个 demo 看看
首先,是item的布局
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:id="@+id/tv_repeat"
android:layout_width="match_parent"
android:layout_height="200dp"
android:gravity="center" />
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="@color/colorAccent" />
然后是RepeatAdapter,这里使用的是原生的Adapter
public class RepeatAdapter extends RecyclerView.Adapter {
private List list;
private Context context;
public RepeatAdapter(List list, Context context) {
this.list = list;
this.context = context;
}
@NonNull
@Override
public RepeatViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
View view = LayoutInflater.from(context).inflate(R.layout.item_repeat, viewGroup, false);
Log.e("cheng", "onCreateViewHolder viewType=" + i);
return new RepeatViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull RepeatViewHolder viewHolder, int i) {
viewHolder.tv_repeat.setText(list.get(i));
Log.e("cheng", "onBindViewHolder position=" + i);
}
@Override
public int getItemCount() {
return list.size();
}
class RepeatViewHolder extends RecyclerView.ViewHolder {
public TextView tv_repeat;
public RepeatViewHolder(@NonNull View itemView) {
super(itemView);
this.tv_repeat = (TextView) itemView.findViewById(R.id.tv_repeat);
}
}
}
在Activity中使用
List list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
list.add("第" + i + "个item");
}
RepeatAdapter repeatAdapter = new RepeatAdapter(list, this);
rvRepeat.setLayoutManager(new LinearLayoutManager(this));
rvRepeat.setAdapter(repeatAdapter);
当我们滑动时,log如下:
image.png
可以看到,总共执行了7次onCreateViewHolder,也就是说,总共100个item,只创建了7个viewholder(篇幅问题,没有截到100,有兴趣的同学可以自己试试)
WHY?
通过阅读源码,我们发现,RecyclerView的缓存单位是viewholder,而获取viewholder最终调用的方法是Recycler#tryGetViewHolderForPositionByDeadline
源码如下:
@Nullable
RecyclerView.ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
...省略代码...
holder = this.getChangedScrapViewForPosition(position);
...省略代码...
if (holder == null) {
holder = this.getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
}
...省略代码...
if (holder == null) {
View view = this.mViewCacheExtension.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = RecyclerView.this.getChildViewHolder(view);
}
}
...省略代码...
if (holder == null) {
holder = this.getRecycledViewPool().getRecycledView(type);
}
...省略代码...
if (holder == null) {
holder = RecyclerView.this.mAdapter.createViewHolder(RecyclerView.this, type);
}
...省略代码...
}
从上到下,依次是mChangedScrap、mAttachedScrap、mCachedViews、mViewCacheExtension、mRecyclerPool最后才是createViewHolder
ArrayList mChangedScrap = null;
final ArrayList mAttachedScrap = new ArrayList();
final ArrayList mCachedViews = new ArrayList();
private RecyclerView.ViewCacheExtension mViewCacheExtension;
RecyclerView.RecycledViewPool mRecyclerPool;
mChangedScrap
完整源码如下:
if (RecyclerView.this.mState.isPreLayout()) {
holder = this.getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
由于isPreLayout方法取决于mInPreLayout,而mInPreLayout默认为false,即mChangedScrap不参与回收复用逻辑。
mAttachedScrap
完整源码如下:
RecyclerView.ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
int scrapCount = this.mAttachedScrap.size();
int cacheSize;
RecyclerView.ViewHolder vh;
for(cacheSize = 0; cacheSize < scrapCount; ++cacheSize) {
vh = (RecyclerView.ViewHolder)this.mAttachedScrap.get(cacheSize);
if (!vh.wasReturnedFromScrap() && vh.getLayoutPosition() == position && !vh.isInvalid() && (RecyclerView.this.mState.mInPreLayout || !vh.isRemoved())) {
vh.addFlags(32);
return vh;
}
}
}
这段代码什么时候会生效呢,那得找找什么时候将viewholder添加到mAttachedScrap的
我们在源码中全局搜索mAttachedScrap.add,发现是Recycler#scrapView()方法
void scrapView(View view) {
...省略代码...
this.mAttachedScrap.add(holder);
...省略代码...
}
什么时候调用scrapView()方法呢?
继续全局搜索,发现最终是Recycler#detachAndScrapAttachedViews()方法,这个方法又是什么时候会被调用的呢?
答案是LayoutManager#onLayoutChildren()。我们知道onLayoutChildren负责item的布局工作(这部分后面再说),所以,mAttachedScrap应该存放是当前屏幕上显示的viewhoder,我们来看下detachAndScrapAttachedViews的源码
public void detachAndScrapAttachedViews(@NonNull RecyclerView.Recycler recycler) {
int childCount = this.getChildCount();
for(int i = childCount - 1; i >= 0; --i) {
View v = this.getChildAt(i);
this.scrapOrRecycleView(recycler, i, v);
}
}
其中,childCount即为屏幕上显示的item数量。那同学们就要问了,mAttachedScrap有啥用?
答案当然是有用的,比如说,拖动排序,比如说第1个item和第2个item 互换,这个时候,mAttachedScrap就派上了用场,直接从这里通过position拿viewholder,都不用经过onCreateViewHolder和onBindViewHolder。
mCachedViews
完整代码如下:
cacheSize = this.mCachedViews.size();
for(int i = 0; i < cacheSize; ++i) {
RecyclerView.ViewHolder holder = (RecyclerView.ViewHolder)this.mCachedViews.get(i);
if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
if (!dryRun) {
this.mCachedViews.remove(i);
}
return holder;
}
}
我们先来找找viewholder是在什么时候添加进mCachedViews?是在Recycler#recycleViewHolderInternal()方法
void recycleViewHolderInternal(RecyclerView.ViewHolder holder) {
if (!holder.isScrap() && holder.itemView.getParent() == null) {
if (holder.isTmpDetached()) {
throw new IllegalArgumentException("Tmp detached view should be removed from RecyclerView before it can be recycled: " + holder + RecyclerView.this.exceptionLabel());
} else if (holder.shouldIgnore()) {
throw new IllegalArgumentException("Trying to recycle an ignored view holder. You should first call stopIgnoringView(view) before calling recycle." + RecyclerView.this.exceptionLabel());
} else {
boolean transientStatePreventsRecycling = holder.doesTransientStatePreventRecycling();
boolean forceRecycle = RecyclerView.this.mAdapter != null && transientStatePreventsRecycling && RecyclerView.this.mAdapter.onFailedToRecycleView(holder);
boolean cached = false;
boolean recycled = false;
if (forceRecycle || holder.isRecyclable()) {
if (this.mViewCacheMax > 0 && !holder.hasAnyOfTheFlags(526)) {
int cachedViewSize = this.mCachedViews.size();
if (cachedViewSize >= this.mViewCacheMax && cachedViewSize > 0) {
this.recycleCachedViewAt(0);
--cachedViewSize;
}
int targetCacheIndex = cachedViewSize;
if (RecyclerView.ALLOW_THREAD_GAP_WORK && cachedViewSize > 0 && !RecyclerView.this.mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
int cacheIndex;
for(cacheIndex = cachedViewSize - 1; cacheIndex >= 0; --cacheIndex) {
int cachedPos = ((RecyclerView.ViewHolder)this.mCachedViews.get(cacheIndex)).mPosition;
if (!RecyclerView.this.mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
break;
}
}
targetCacheIndex = cacheIndex + 1;
}
this.mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {
this.addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
}
RecyclerView.this.mViewInfoStore.removeViewHolder(holder);
if (!cached && !recycled && transientStatePreventsRecycling) {
holder.mOwnerRecyclerView = null;
}
}
} else {
throw new IllegalArgumentException("Scrapped or attached views may not be recycled. isScrap:" + holder.isScrap() + " isAttached:" + (holder.itemView.getParent() != null) + RecyclerView.this.exceptionLabel());
}
}
最上层是RecyclerView#removeAndRecycleViewAt方法
public void removeAndRecycleViewAt(int index, @NonNull RecyclerView.Recycler recycler) {
View view = this.getChildAt(index);
this.removeViewAt(index);
recycler.recycleView(view);
}
这个方法是在哪里调用的呢?答案是LayoutManager,我们写个demo效果看着比较直观
定义MyLayoutManager,并重写removeAndRecycleViewAt,然后添加log
class MyLayoutManager extends LinearLayoutManager {
public MyLayoutManager(Context context) {
super(context);
}
@Override
public void removeAndRecycleViewAt(int index, @NonNull RecyclerView.Recycler recycler) {
super.removeAndRecycleViewAt(index, recycler);
Log.e("cheng", "removeAndRecycleViewAt index=" + index);
}
}
将其设置给RecyclerView,然后滑动,查看日志输出情况
image.png
image.png
可以看到,每次有item滑出屏幕时,都会调用removeAndRecycleViewAt()方法,需要注意的是,此index表示的是该item在chlid中的下标,也就是在当前屏幕中的下标,而不是在RecyclerView的。
事实是不是这样的呢?让我们来看看源码,以LinearLayoutManager为例,默认是垂直滑动的,此时控制其滑动距离的方法是scrollVerticallyBy(),其调用的是scrollBy()方法
int scrollBy(int dy, Recycler recycler, State state) {
if (this.getChildCount() != 0 && dy != 0) {
this.mLayoutState.mRecycle = true;
this.ensureLayoutState();
int layoutDirection = dy > 0 ? 1 : -1;
int absDy = Math.abs(dy);
this.updateLayoutState(layoutDirection, absDy, true, state);
int consumed = this.mLayoutState.mScrollingOffset + this.fill(recycler, this.mLayoutState, state, false);
if (consumed < 0) {
return 0;
} else {
int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
this.mOrientationHelper.offsetChildren(-scrolled);
this.mLayoutState.mLastScrollDelta = scrolled;
return scrolled;
}
} else {
return 0;
}
}
关键代码是fill()方法中的recycleByLayoutState(),判断滑动方向,从第一个还是最后一个开始回收。
private void recycleByLayoutState(Recycler recycler, LinearLayoutManager.LayoutState layoutState) {
if (layoutState.mRecycle && !layoutState.mInfinite) {
if (layoutState.mLayoutDirection == -1) {
this.recycleViewsFromEnd(recycler, layoutState.mScrollingOffset);
} else {
this.recycleViewsFromStart(recycler, layoutState.mScrollingOffset);
}
}
}
扯的有些远了,让我们回顾下recycleViewHolderInternal()方法,当cachedViewSize >= this.mViewCacheMax时,会移除第1个,也就是最先加入的viewholder,mViewCacheMax是多少呢?
public Recycler() {
this.mUnmodifiableAttachedScrap = Collections.unmodifiableList(this.mAttachedScrap);
this.mRequestedCacheMax = 2;
this.mViewCacheMax = 2;
}
mViewCacheMax为2,也就是mCachedViews的初始化大小为2,超过这个大小时,viewholer将会被移除,放到哪里去了呢?带着这个疑问我们继续往下看
mViewCacheExtension
ViewCacheExtension 这个类需要使用者通过 setViewCacheExtension() 方法传入,RecyclerView自身并不会实现它,一般正常的使用也用不到。
mRecyclerPool
我们带着之前的疑问,继续看源码,之前提到mCachedViews初始大小为2,超过这个大小,最先放入的会被移除,移除的viewholder到哪里去了呢?我们来看recycleCachedViewAt()方法的源码
void recycleCachedViewAt(int cachedViewIndex) {
RecyclerView.ViewHolder viewHolder = (RecyclerView.ViewHolder)this.mCachedViews.get(cachedViewIndex);
this.addViewHolderToRecycledViewPool(viewHolder, true);
this.mCachedViews.remove(cachedViewIndex);
}
addViewHolderToRecycledViewPool()方法
void addViewHolderToRecycledViewPool(@NonNull RecyclerView.ViewHolder holder, boolean dispatchRecycled) {
RecyclerView.clearNestedRecyclerViewIfNotNested(holder);
if (holder.hasAnyOfTheFlags(16384)) {
holder.setFlags(0, 16384);
ViewCompat.setAccessibilityDelegate(holder.itemView, (AccessibilityDelegateCompat)null);
}
if (dispatchRecycled) {
this.dispatchViewRecycled(holder);
}
holder.mOwnerRecyclerView = null;
this.getRecycledViewPool().putRecycledView(holder);
}
可以看到,该viewholder被添加到mRecyclerPool中
我们继续看看RecycledViewPool的源码
public static class RecycledViewPool {
private static final int DEFAULT_MAX_SCRAP = 5;
SparseArray mScrap = new SparseArray();
private int mAttachCount = 0;
public RecycledViewPool() {
}
...省略代码...
}
static class ScrapData {
final ArrayList mScrapHeap = new ArrayList();
int mMaxScrap = 5;
long mCreateRunningAverageNs = 0L;
long mBindRunningAverageNs = 0L;
ScrapData() {
}
}
可以看到,其内部有一个SparseArray用来存放viewholder。
总结
总共有mAttachedScrap、mCachedViews、mViewCacheExtension、mRecyclerPool4级缓存,其中mAttachedScrap只保存布局时,屏幕上显示的viewholder,一般不参与回收、复用(拖动排序时会参与);
mCachedViews主要保存刚移除屏幕的viewholder,初始大小为2;
mViewCacheExtension为预留的缓存池,需要自己去实现;
mRecyclerPool则是最后一级缓存,当mCachedViews满了之后,viewholder会被存放在mRecyclerPool,继续复用。
其中,mAttachedScrap、mCachedViews为精确匹配,即为对应position的viewholder才会被复用;
mRecyclerPool为模糊匹配,只匹配viewType,所以复用时,需要调用onBindViewHolder为其设置新的数据。
回答之前的疑问
当滑出第6个item时,这时mCachedViews中存放着第1、2个item,屏幕上显示的是第3、4、5、6个item,再滑出第7个item时,不存在能复用的viewholder,所以调用onCreateViewHolder创建了一个新的viewholder,并且把第1个viewholder放入mRecyclerPool,以备复用。
android 全局缓存,【Android进阶】RecyclerView之缓存(二)相关推荐
- android全局livedata,Android 使用ViewModel,LiveData高效、简
一.思路 ViewModel作为View监听器和View进行绑定,LiveData数据存放在ViewModel数据持久化,并具有观察性.View通过监听LiveData的数据变化,进行相应的UI操作. ...
- android 全局浮动球,Android仿360悬浮小球自定义view实现
图片.png 图片.png 实现当前这种类似的效果 (360小球 悬浮桌面差不错类似).第一次接触到的童鞋就像我一样懵逼(研究过得童鞋,就知道,实现这种悬浮窗体,是需要添加悬浮窗 设置权限的,我会在下 ...
- android webview 更新,Android WebView缓存没更新解决方案
一:现象 客户端加载过H5页面A,后来H5修改为A'发布之后,在很长一段时间内,客户端一直展示的是A,没有更新为A'. 重启之后依然没有更新,只有清除缓存或者重装APP才会更新. 二:分析 根据现象得 ...
- android 根目录缓存,Android系统中内部存储和外部存储(公有目录、私有目录、缓存目录)详解...
首先,明确一个概念,Android内部存储和外部存储并非所谓的手机自带内存是内部存储,SD卡是外部存储云云. Android对内部存储和外部存储不是在物理上区分的,而是在逻辑上区分的.git 1.概念 ...
- android分享图片功能实现原理,Android:简单实现并理解图片三级缓存
学习Android网络开发的过程中,势必会经历很多痛苦的过程,其中一个大坑就是图片缓存,当然现在有很多现成的库非常方便,常常几行代码就可以实现想要的功能,但不懂其中的原理是不行的,所以对于刚开始学习网 ...
- Android微信页面缓存清理,安卓微信浏览器缓存如何清理
前言: 缓存这个问题的出现,真真切切的感受到微信浏览器这鬼东西对前端程序员充满了恶意,捋捋自己的发型,甚是恐慌(顿时有种想转php的冲动,有木有). 解决方案: 出现缓存问题导致用户出现很多车祸现场( ...
- android cache缓存,Android缓存Cache学习
Android缓存Cache学习 java 项目中须要用到缓存,我使用的是文件缓存,这里简单总结下:android 主要思路是封装一个缓存对象,将每一个界面须要缓存的数据做为缓存对象的属性,将缓存对象 ...
- Android:Android9.0使用 AndroidVideoCache时不能缓存播放视频的解决
一.问题现象: 项目中使用 https://github.com/danikula/AndroidVideoCache 作为视频缓存组件,但是在9.0手机上无法正常缓存,并且报错: 1.详细错误截图 ...
- Android UIL图片加载缓存源码分析-内存缓存
本篇文章我们来分析一下著名图片加载库Android-Universal-Image-Loader的图片缓存源码. 源码环境 版本:V1.9.5 GitHub链接地址:https://github.co ...
最新文章
- 冒号课堂§4.3:汇总范式
- outlook收不到html图片,急!求教高手!用outlook发送HTML格式邮件,图片不能显示是为什么...
- java bean 工厂模式_Spring框架通过工厂创建Bean的三种方式实现
- 洛谷P2286 [HNOI2004]宠物收养所 [STL,平衡树]
- 他让全世界凶手睡不着觉,现实版福尔摩斯,退休了4次又被拽回来工作,无敌实在是太寂寞了~...
- 接口测试文件上传(python+requests)
- xp 挂linux上网,XP系统挂载Linux NFS共享
- oracle vm win10,win10系统oraclevm卸载不了错误2503的解决方法
- java多线程之volatile理解
- python plt 如何画不同的数据图
- 大学生论文发表的费用需要多少
- 利用ffmpeg提取视频中的声音为MP3格式
- UDP网络通信的发包/收包过程/代理服务器的使用
- MCS51 系列单片机的中央处理器(CPU)
- Excel使用---excel2016___一般操作(搬,侵删)
- 艾美捷胆固醇肉豆蔻酸酯说明书和相关研究
- MyBatis 学习(七):深入 Mapper XML映射文件
- HTML5+CSS大作业——个人博客-功能齐全(48页) html大学生网站开发实践作业
- 计算机一级无法打开office,电脑中无法运行Office2016的两种解决方法
- MySQL之学生成绩表查询语句解析
热门文章
- vue中的created方法
- 金山办公业绩增长的另一面:雷军等密集减持,还涉上亿元侵权诉讼
- Stm32MP157-Linux(Ubuntu)——Ubuntu入门
- `spyder总是闪退?spyder打不开?spyder又又又又又出错啦?
- Keil MDK使用第7篇---Go To Definition 和 Go To referebce的区别
- 计算机按键的功能介绍时间,计算器的日期键是什么
- arcgis 字段计算器 条件赋值_ARCGIS中字段计算器的使用说明
- 20190827-Ubuntu查看内存槽的数目及内存信息
- 计算机考研视频哪个好,计算机考研视频课程哪个好
- Js 方法函数记录笔记