RecyclerView的缓存机制和内存优化
RecyclerView 缓存需要用到的数据结构在 Recycler 类里面.
public final class Recycler {
final ArrayList mAttachedScrap = new ArrayList<>();
ArrayList mChangedScrap = null;
final ArrayList mCachedViews = new ArrayList();
//默认大小是2RecycledViewPool mRecyclerPool;
private ViewCacheExtension mViewCacheExtension;
}
这里面主要介绍一下 mAttachedScrap 和 mChangedScrap.他们都是在同一个函数中 add 的
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
......
holder.setScrapContainer(this, false);
mAttachedScrap.add(holder);
} else {
if (mChangedScrap == null) {
mChangedScrap = new ArrayList();
}
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);
}
}
【RecyclerView的缓存机制和内存优化】可知,已经被删除的或者不需要更新的的在 mAttachedScrap 中,未删除且需要更新的在 mChangedScrap 中.
(scrap 是废除的意思,那废除的 view 怎么回去添加到 mAttachedScrap 中呢?其实这个 mAttachedScrap 表示的是从屏幕上分离出来,但是又即将添加到屏幕上去的 view。比如说,RecyclerView 上下滑动,滑出一个新的 Item,此时会重新调用 LayoutManager 的 onLayoutChildren 方法,从而会将屏幕上所有的 view 先 scrap 掉,添加到 mAttachedScrap 里面去,然后重新布局的时候会从优先 mAttachedScrap 里面获取)
复用
RecyclerView 对 ViewHolder 的复用,我们得从 LayoutState 的 next 方法开始。最终来分析 RecyclerView 的 tryGetViewHolderForPositionByDeadline
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
......
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)) {
if (!dryRun) {
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 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 && mViewCacheExtension != null) {
final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
}
}
if (holder == null) { // fallback to pool
holder = getRecycledViewPool().getRecycledView(type);
......
}
if (holder == null) {
holder = mAdapter.createViewHolder(RecyclerView.this, type);
......
}
}
......
return holder;
}
整个函数内容比较多.我们分步骤来看
step0
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
当在 prelayout 阶段时, 走 getChangedScrapViewForPosition
(prelayout 是什么? https://www.jianshu.com/p/61fe3f3bb7ec 这里有提到,简单点就是 dispatchLayoutStep1. 要想真正开启 prelayout, 就和 processAdapterUpdatesAndSetAnimationFlags 中的 mRunSimpleAnimations 和 mRunPredictiveAnimations 参数有关)
接下来分析 getChangedScrapViewForPosition,很简单,就是从 mChangedScrap 寻找 viewhoder
step1
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
if (!validateViewHolderForOffsetPosition(holder)) {
if (!dryRun) {
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;
}
}
}
这一步理解起来比较容易,分别从mAttachedScrap、 mHiddenViews、mCachedViews 获取 ViewHolder。如果获取的 ViewHolder 是无效的,得做一些清理操作,然后重新放入到缓存里面,具体对应的缓存就是 mCacheViews 和 RecyclerViewPool (recycleViewHolderInternal)。
另外,有必要提一下 mHiddenViews.它在 ChildHelper 中.如果当前操作支持动画,就会调用到 RecyclerView 的 addAnimatingView 方法,在这个方法里面会将做动画的那个 View 添加到 mHiddenView 数组里面去。通常就是动画期间可以会进行复用,因为 mHiddenViews 只在动画期间才会有元素。所以在整个复用流程里可以不用考虑
step2
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 && mViewCacheExtension != null) {
final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
}
}
if (holder == null) { // fallback to pool
holder = getRecycledViewPool().getRecycledView(type);
......
}
if (holder == null) {
holder = mAdapter.createViewHolder(RecyclerView.this, type);
......
}
流程很简单,根据 id 从 mAttachedScrap 和 mCachedViews 中获取数据.然后从 mViewCacheExtension 中获取, 然后 mRecyclerPool ,然后 createViewHolder.
注意两点,一个是 getScrapOrCachedViewForId 的前置条件 hasStableIds.后面会说.还有一个是 ViewCacheExtension.它是一个 abstract 类,看注释就知道其实就是复用的已经存在的 view,应该是预留给后续开发用的.
总结下来整个流程就是
- 1.prelayout 下在 mChangedScrap 中寻找(需要更新的 viewhoder)
- 2.分别从mAttachedScrap、 mCachedViews 获取 ViewHolder
如果获取的 ViewHolder 是无效的,得做一些清理操作,然后重新放入到缓存里面,具体对应的缓存就是 mCacheViews 和 RecyclerViewPool
------上面是position,下面是type - 3.hasStableIds == true,根据 id 从 mAttachedScrap 和 mCachedViews 中获取数据
- 4.从 mRecyclerPool 中获取(大小默认是 5)
实在找不到,就 createViewHolder
接下来看一下这个方法
复用时候的代码
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
......
}
回收时候的代码
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {//TODO 这里
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
} else {
detachViewAt(index);
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
由此可见,当 hasStableIds = true 时,所有的 item 都进入 scrap 中,相当于提升了复用的效率
onViewRecycled
这个可以自己设置,也可以放在 Adapter 自己扩展.一般都是自己扩展
为啥专门提这个呢?看代码
void dispatchViewRecycled(ViewHolder holder) {
if (mRecyclerListener != null) {
mRecyclerListener.onViewRecycled(holder);
}
if (mAdapter != null) {
mAdapter.onViewRecycled(holder);
}
if (mState != null) {
mViewInfoStore.removeViewHolder(holder);
}
if (DEBUG) Log.d(TAG, "dispatchViewRecycled: " + holder);
}
接下来去寻找调用它的地方,只有一个地方 addViewHolderToRecycledViewPool.在往上就两处
- 1.recycleViewHolderInternal。 这里应该知道是啥
- 2.recycleCachedViewAt------从代码看出,recycleCachedViewAt 就是把 mCachedViews 中的数据移除然后放到 RecycledViewPool 中
所以,综合整个缓存机制以及我们的目标---内存优化.我们可以作如下优化:
- 1.如果图片大小可知,并且都比较小,那么可以设置 hasStableIds 为 true 来优化整个复用效率
- 2.如果图片比较大,或者大小不可知,那么我们可以在 onViewRecycled 函数中释放图片内存.但是 hasStableIds 肯定不能是 true 了.
这个博主写了关于RecyclerView的一系列文章,都可以阅读阅读加深理解
推荐阅读
- 热闹中的孤独
- JAVA(抽象类与接口的区别&重载与重写&内存泄漏)
- 放屁有这三个特征的,请注意啦!这说明你的身体毒素太多
- 一个人的旅行,三亚
- 布丽吉特,人生绝对的赢家
- 慢慢的美丽
- 尽力
- 一个小故事,我的思考。
- 家乡的那条小河
- 《真与假的困惑》???|《真与假的困惑》??? ——致良知是一种伟大的力量