Android基础知识-RecyclerView的复用和回收机制


ListView复用、回收
  • AbsListView.RecycleBin
    RecycleBin 维护了两种类型列表,
    一种用于保存屏幕上可见的View,一个用于缓存滚动出屏幕的View

    ListView滑动过程中,子View完全移出屏幕后,会加入RecycleBin 缓存
    子View进入屏幕时,从RecycleBin 中获取缓存View,用于数据绑定。

RecyclerView 复用、回收

滚动屏幕时,列表先执行 复用流程、再执行回收流程

复用流程:2级缓存 mCachedViews 取 > 1级缓存 RecycledViewPool 取 > Adapter.onCreateViewHolder()

回收流程:遍历移除屏幕的 View,从 View的 LayoutParams 中取出 ViewHolder,塞入 2级缓存 mCachedViews
                 如果 mCachedViews 满了(容量2),则 mCachedViews 移除第一个,用来放要回收的 ViewHolder
                 如果 RecycledViewPool 对应 viewType 的 List 没满(容量5),则从 mCachedViews 移除的 ViewHolder 放入 RecycledViewPool。

1、复用

Recycler 用来缓存 ViewHolder 的结构体

Recycler 缓存结构

Recycler
List<ViewHolder> mAttachedScrap
存放暂时 remove 的 ViewHolder
List<ViewHolder> mChangedScrap
List<ViewHolder> mCachedViews
RecycledViewPool
ViewCacheExtension mViewCacheExtension
List<ViewHolder> mUnmodifiableAttachedScrap
未滑动的操作 触发 RecyclerView layout 过程
RecyclerView 会先把所有 children 先 remove 掉
然后重新 add 上去 完成一次 layout 的过程
暂时 remove 的 viewHolder 存放在 mAttachedScrap
数据发生变化时 涉及到的复用场景
滑动过程回收和复用处理的 List
该容器的 ViewHolder 的原本数据信息都在
可以直接添加到 RecyclerView 中显示
不需要再次重新 onBindViewHolder
ViewHolder 的数据信息已经重置
相当于新建的 ViewHolder
需要重新调用 onBindViewHolder 来绑定数据
留给我们自己扩展的缓存容器

mCachedViews 一级缓存:ViewHolder数据还在,只有原来的position可用,不需要重新绑定数据
mCachedViews 默认大小为 2

RecycledViewPool 二级缓存:ViewHolder数据重置,需要重新绑定数据
RecycledViewPool 根据不同的 item type 创建不同的 List,每个 List 默认大小为 5
复用时,只要 RecycledViewPool 中对应 type 的 List 有 ViewHolder 缓存,最后一个拿出来复用

复用流程
getViewForPosition 复用入口
tryGetViewHolderForPositionByDeadline 复用实现
不同场景会先从不同的 一级缓存 根据position 取ViewHolder
滑动时一级缓存 mCachedViews
一级缓存没找到会 从二级缓存 RecyclerViewPool 取
RecyclerViewPool 中没找到
Adapter.onCreateViewHolder 创建 ViewHolder
  • 回收
    回收流程
mCachedView 未满
mCachedView 已满
将最新的 ViewHolder
放入 mCachedViews
RecycledViewPool中
对应viewType的List已满
RecycledViewPool中
对应viewType的 List 未满
滑动列表
LinearLayoutManager#scrollVerticallyBy
LayoutManager#fill
Recycler # recyclerView
Recycler#recycleViewHolderInternal
存入 mCachedViews
mCachedViews 中移除第一个
丢弃mCachedViews 中移除的ViewHolder
把 mCachedViews 中移除的ViewHolder
数据重置后后放入 RecycledViewPool

如果 mCachedViews 超过了存储容量( 默认容量 2 ),移除 mCachedViews 第一个元素,放入RecycledViewPool
如果 RecycledViewPool对应 viewType 的 List 满了(默认每个 viewType 缓存容量 5),
则丢弃 mCachedViews 中移除的 ViewHolder

局部刷新

1、notifyItemChanged(int position, Object payload)
payload参数,传入非空Object对象

2、onBindViewHolder(RecyclerHolder holder, int position, List payloads)
payloads.size 永远是 1,根据 payload参数值,实现局部刷新

private static final String DATA_ONE = "dataOne";
private static final String DATA_TWO = "dataTwo";

notifyItemChanged(position, changePos == 0 ? DATA_ONE : DATA_TWO);

@Override
public void onBindViewHolder(RecyclerHolder holder, int position, List<Object> payloads) {
    if (payloads.isEmpty()) {//payloads 为空,全量刷新
        onBindViewHolder(holder, position);
    } else {
        for (Object payload : payloads) {
            switch (String.valueOf(payload)) {
                case DATA_ONE://根据不同类型的 payload值,刷新不同的局部控件
                    holder.dataOne.setText(mDatas.get(position));
                    break;
                case DATA_TWO://根据不同类型的 payload值,刷新不同的局部控件
                    holder.dataTwo.setText(mDatas.get(position));
                    break;
            }
        }
    }
}

一、RecyclerView

RecyclerView 的回收复用机制的内部实现都是由 Recycler 内部类实现
1、复用机制

1)Recycler用来缓存 ViewHolder 的结构体

public final class Recycler {
	
	/* 一次遥控器按键的操作,不管有没有发生滑动,都会导致 RecyclerView 的重新 onLayout,
	 * 那要 layout 的话,RecyclerView 会先把所有 children 先 remove 掉,然后再重新 add 上去,完成一次 layout 的过程。
	 * 那么这暂时性的 remove 掉的 viewHolder 要存放在哪呢,就是放在这个 mAttachedScrap 中了
	 */
	final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
	
	//应该是在当数据发生变化时才会涉及到的复用场景
	ArrayList<ViewHolder> mChangedScrap = null;
	
	/* 滑动过程中的回收和复用都是先处理的这个 List,
	 * 这个集合里存的 ViewHolder 的原本数据信息都在,
	 * 所以可以直接添加到 RecyclerView 中显示,不需要再次重新 onBindViewHolder()
	 */
	final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

	private final List<ViewHolder> mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);

	/* 存在这里的 ViewHolder 的数据信息会被重置掉,
	 * 相当于 ViewHolder 是一个重创新建的一样,所以需要重新调用 onBindViewHolder 来绑定数据
	 */
	RecycledViewPool mRecyclerPool;
	//这个是留给我们自己扩展的
	private ViewCacheExtension mViewCacheExtension;
}
1) getViewForPosition() 这个方法是复用机制的入口
这个方法是 Recycler 开放给外部使用复用机制的api,外部调用这个方法就可以返回想要的 View,
而至于这个 View 是复用而来的,还是重新创建得来的,就都由 Recycler 内部实现,对外隐藏。

public View getViewForPosition(int position) {
	return getViewForPosition(position, false);
}

View getViewForPosition(int position, boolean dryRun) {
	return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
/**
 * Recycler 的复用机制内部实现就在这个方法里
 */
ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
	...
	boolean fromScrapOrHiddenOrCache = false;
	ViewHolder holder = null;
	...
	// 1) Find by position from scrap/hidden list/cache
	if (holder == null) {
		holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
		...
	}
}

ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
	final int scrapCount = mAttachedScrap.size();

	// Try first for an exact, non-invalid match from scrap.
	for (int i = 0; i < scrapCount; i++) {
		/* 去 mAttachedScrap 中寻找 position 一致的 viewHolder,
		 * 这个 viewHolder 没有被移除,是有效的之类的条件,满足就返回这个 viewHolder
		 *
		 * 一次遥控器按键的操作,不管有没有发生滑动,都会导致 RecyclerView 的重新 onLayout,
		 * 那要 layout 的话,RecyclerView 会先把所有 children 先 remove 掉,然后再重新 add 上去,完成一次 layout 的过程。
		 * 那么这暂时性的 remove 掉的 viewHolder 要存放在哪呢,就是放在这个 mAttachedScrap 中了
		 *
		 *
		 * 网上一些分析的文章有说,RecyclerView 在复用时会按顺序去 mChangedScrap, mAttachedScrap 等等缓存里找,
		 * 没有找到再往下去找,从代码上来看是这样没错,但我觉得这样表述有问题。
		 * 因为就我们这篇文章基于 RecyclerView 的滑动场景来说,新卡位的复用以及旧卡位的回收机制,
		 * 其实都不会涉及到mChangedScrap 和 mAttachedScrap,
		 * 所以我觉得还是基于某种场景来分析相对应的回收复用机制会比较好。
		 * 就像mChangedScrap 我虽然没理解是干嘛用的,但我猜测应该是在当数据发生变化时才会涉及到的复用场景,
		 * 所以当我分析基于滑动场景时的复用时,即使我对这块不理解,影响也不会很大。
		 */
		final ViewHolder holder = mAttachedScrap.get(i);
		if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
				&& !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
			holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
			return holder;
		}
	}
	...
	// Search in our first-level recycled view cache.
	final int cacheSize = mCachedViews.size();
	for (int i = 0; i < cacheSize; i++) {
		/*
		 * mCachedViews 的大小默认为2。
		 * 遍历 mCachedViews,找到 position 一致的 ViewHolder,
		 * 之前说过,mCachedViews 里存放的 ViewHolder 的数据信息都保存着,
		 * 所以 mCachedViews 可以理解成,只有原来的卡位可以重新复用这个 ViewHolder,
		 * 新位置的卡位无法从 mCachedViews 里拿 ViewHolder出来用。*
		 */
		final ViewHolder holder = mCachedViews.get(i);
		if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
			if (!dryRun) {
				mCachedViews.remove(i);
			}
			return holder;
		}
	}
	return null;
}


ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
	...
	if (holder == null) {
		/**
		 * 【1】根据position根据不同场景,会依次去不同缓存中匹配查找ViewHolder
		 */
		holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
		//找到ViewHolder后
		if (holder != null) {
			//判断一下这个 ViewHolder 是否已经被 remove 掉,type 类型一致不一致
			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;
			}
		}
	}
	/**
	 * 【2】上面根据position 在 mCachedViews 中没找到匹配的 ViewHolder 的话,
	 *   这里会根据 id 重复上面的步骤再找一遍
	 */
	if (holder == null) {
		final int offsetPosition = mAdapterHelper.findPositionOffset(position);
		...
		final int type = mAdapter.getItemViewType(offsetPosition);
		// 2) Find from scrap/cache via stable ids, if exists
		if (mAdapter.hasStableIds()) {
			/* getScrapOrCachedViewForId() 根据 id查找 ViewHolder
			 *
			 * 这个 id 并不是我们在 xml 中设置的 android:id, 而是 Adapter 持有的一个属性,
			 * 默认是不会使用这个属性的,所以这个第5步其实是不会执行的,
			 * 除非我们重写了 Adapter 的 setHasStableIds()
			 */
			holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
			if (holder != null) {
				// update position
				holder.mPosition = offsetPosition;
				fromScrapOrHiddenOrCache = true;
			}
		}
		/**
		 * 【3】从扩展缓存中找 ViewHolder
		 * RecyclerView 提供给我们自定义实现的扩展类,
		 * 我们可以重写 getViewForPositionAndType() 方法来实现自己的复用策略
		 */					
		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.");
				}
			}
		}
		/**
		 * 【4】去 RecyclerViewPool 里取 ViewHolder,
		 * ViewPool 会根据不同的 item type 创建不同的 List,每个 List 默认大小为5个。
		 */
		if (holder == null) { // fallback to pool
			if (DEBUG) {
				Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
						+ position + ") fetching from shared pool");
			}
			/*
			 * ViewPool 会根据不同的 viewType 创建不同的集合来存放 ViewHolder,
			 * 那么复用的时候,只要 ViewPool 里相同的 type 有 ViewHolder 缓存的话,
			 * 就将最后一个拿出来复用,不用像 mCachedViews 需要各种匹配条件,只要有就可以复用。
			 */
			holder = getRecycledViewPool().getRecycledView(type);
			if (holder != null) {
				/* 重置 ViewHolder,这样 ViewHolder 就可以当作一个全新的 ViewHolder 来使用了,
				 * 这也就是为什么从这里拿的 ViewHolder 都需要重新 onBindViewHolder() 了。
				 */
				holder.resetInternal();
				if (FORCE_INVALIDATE_DISPLAY_LIST) {
					invalidateDisplayListInt(holder);
				}
			}
		}
		/**
		 * 【5】调用 Adapter.onCreateViewHolder()来创建一个新的ViewHolder
		 *  如果再ViewPool 中都找不到 ViewHolder,就要创建新的 ViewHolder
		 */
		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");
			}
		}
	}
	...
}

/**
 * ViewPool 会根据不同的 item type 创建不同的 List,每个 List 默认大小为5个。
 */
public static class RecycledViewPool {
	/*
	 * ViewPool 会根据不同的 viewType 创建不同的集合来存放 ViewHolder,
	 * 那么复用的时候,只要 ViewPool 里相同的 type 有 ViewHolder 缓存的话,
	 * 就将最后一个拿出来复用,不用像 mCachedViews 需要各种匹配条件,只要有就可以复用。
	 */
	public ViewHolder getRecycledView(int viewType) {
		final ScrapData scrapData = mScrap.get(viewType);
		if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
			final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
			return scrapHeap.remove(scrapHeap.size() - 1);//取出最后一个,拿来复用
		}
		return null;			
	}
}

重新 setLayoutManager(),重新 setAdapter(),或者 notifyDataSetChanged(),或者滑动等等之类的场景,
只要重新layout,就会去回收和复用 ViewHolder,所以这个复用机制需要考虑到各种各样的场景。

从以上步骤看,只考虑滑动场景,RecyclerView 至少有 2 级缓存,
mCachedViews为一级缓存:该缓存的ViewHolder数据还在,只有原来的position可用,不需要重新绑定数据。
RecyclerViewPool 为二级缓存:该缓存的ViewHolder数据会重置,需要重新绑定数据。

2、回收机制

回收机制的入口就有很多了,因为 Recycler 有各种结构体,比如mAttachedScrap,mCachedViews 等等,
不同结构体回收的时机都不一样,入口也就多了。

在 RecyclerView 滑动时,会交由 LinearLayoutManager 的 scrollVerticallyBy() 去处理,
然后 LayoutManager 会接着调用 fill() 方法去处理需要复用和回收的卡位,
最终会调用 Recycler.recyclerView() 这个方法开始进行回收工作。

public final class Recycler {
	
	public void recycleView(View view) {
		// This public recycle method tries to make view recycle-able since layout manager
		// intended to recycle this view (e.g. even if it is in scrap or change cache)
		ViewHolder holder = getChildViewHolderInt(view);
		if (holder.isTmpDetached()) {
			removeDetachedView(view, false);
		}
		if (holder.isScrap()) {
			holder.unScrap();
		} else if (holder.wasReturnedFromScrap()) {
			holder.clearReturnedFromScrapFlag();
		}
		//真正回收
		recycleViewHolderInternal(holder);
	}
	
	void recycleViewHolderInternal(ViewHolder holder) {
		...
		boolean cached = false;
		boolean recycled = false;
		if (DEBUG && mCachedViews.contains(holder)) {
			throw new IllegalArgumentException("cached view received recycle internal? "
					+ holder);
		}
		if (forceRecycle || holder.isRecyclable()) {
			if (mViewCacheMax > 0
					&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
							| ViewHolder.FLAG_REMOVED
							| ViewHolder.FLAG_UPDATE
							| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
				
				 /* 如果mCacheViews超过了存储容量,
				  * 就从mCacheViews里面取第一个放入RecyclerViewPool中
				  */
				int cachedViewSize = mCachedViews.size();
				if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
					recycleCachedViewAt(0);
					cachedViewSize--;
				}

				int targetCacheIndex = cachedViewSize;
				if (ALLOW_THREAD_GAP_WORK
						&& cachedViewSize > 0
						&& !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
					// when adding the view, skip past most recently prefetched views
					int cacheIndex = cachedViewSize - 1;
					while (cacheIndex >= 0) {
						int cachedPos = mCachedViews.get(cacheIndex).mPosition;
						if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
							break;
						}
						cacheIndex--;
					}
					targetCacheIndex = cacheIndex + 1;
				}
				//将最新的ViewHolder放入mCachedViews中
				mCachedViews.add(targetCacheIndex, holder);
				cached = true;
			}
			if (!cached) {
				addViewHolderToRecycledViewPool(holder, true);
				recycled = true;
			}
		} else {
			// NOTE: A view can fail to be recycled when it is scrolled off while an animation
			// runs. In this case, the item is eventually recycled by
			// ItemAnimatorRestoreListener#onAnimationFinished.

			// TODO: consider cancelling an animation when an item is removed scrollBy,
			// to return it to the pool faster
			if (DEBUG) {
				Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will "
						+ "re-visit here. We are still removing it from animation lists");
			}
		}
		// even if the holder is not removed, we still call this method so that it is removed
		// from view holder lists.
		mViewInfoStore.removeViewHolder(holder);
		if (!cached && !recycled && transientStatePreventsRecycling) {
			holder.mOwnerRecyclerView = null;
		}
	}
}

回收的逻辑比较简单,由 LayoutManager 来遍历移出屏幕的卡位,然后对每个卡位进行回收操作。

mCachedViews 优先级高于 RecyclerViewPool,
回收时,都是把 ViewHolder 放在 mCachedViews 里面,
如果 mCachedViews 满了,那就在 mCachedViews 里拿一个 ViewHolder 扔到 ViewPool 缓存里,
然后 mCachedViews 就可以空出位置来放新回收的 ViewHolder 了。

复用时,也是先到 mCachedViews 里找 ViewHolder,但需要各种匹配条件,
概括一下就是只有原来位置的卡位可以复用存在 mCachedViews 里的 ViewHolder,
如果 mCachedViews 里没有,那么才去 ViewPool 里找。

在 ViewPool 里的 ViewHolder 都是跟全新的 ViewHolder 一样,
只要 type 一样,有找到,就可以拿出来复用,重新绑定下数据即可。

3、问题

Q1:如果向下滑动,新一行的5个卡位的显示会去复用缓存的 ViewHolder,第一行的5个卡位会移出屏幕被回收,
那么在这个过程中,是先进行复用再回收?还是先回收再复用?还是边回收边复用?
也就是说,新一行的5个卡位复用的 ViewHolder 有可能是第一行被回收的5个卡位吗?

答:先复用再回收,新一行的5个卡位先去目前的 mCachedViews 和 ViewPool 的缓存中寻找复用,
没有就重新创建,然后移出屏幕的那行的5个卡位再回收缓存到 mCachedViews 和 ViewPool 里面,
所以新一行5个卡位和复用不可能会用到刚移出屏幕的5个卡位。

Q2: 向下滑动一行后再上滑一行,在这个过程中,为什么当 RecyclerView 再次向上滑动重新显示第一行的5个卡位时,
只有后面3个卡位触发了 onBindViewHolder() 方法,重新绑定数据呢?明明5个卡位都是复用的。

答:滑动场景下涉及到的回收和复用的结构体是 mCachedViews 和 ViewPool,前者默认大小为2,后者为5。
所以,当第三行显示出来后,第一行的5个卡位被回收,回收时先缓存在 mCachedViews,满了再移出旧的到 ViewPool 里,
所以5个卡位有2个缓存在 mCachedViews 里,3个缓存在 ViewPool,至于是哪2个缓存在 mCachedViews,这是由 LayoutManager 控制。

上面讲解的例子使用的是 GridLayoutManager,滑动时的回收逻辑则是在父类 LinearLayoutManager 里实现,
回收第一行卡位时是从后往前回收(最后回收的两个会放在 mCachedViews),
所以最新的两个卡位是0、1,会放在 mCachedViews 里,而2、3、4的卡位则放在 ViewPool 里。

所以,当再次向上滑动时,第一行5个卡位会去两个结构体里找复用,
之前说过,mCachedViews 里存放的 ViewHolder 只有原本位置的卡位才能复用,
所以0、1两个卡位都可以直接去 mCachedViews 里拿 ViewHolder 复用,而且这里的 ViewHolder 是不用重新绑定数据的,

至于2、3、4卡位则去 ViewPool 里找,刚好 ViewPool 里缓存着3个 ViewHolder,
所以第一行的5个卡位都是用的复用的,而从 ViewPool 里拿的复用需要重新绑定数据,
才会这样只有三个卡位需要重新绑定数据。

Q3:接下去不管是向上滑动还是向下滑动,滑动几次,都不会再有 onCreateViewHolder() 的日志了,
也就是说 RecyclerView 总共创建了17个 ViewHolder,但有时一行的5个卡位只有3个卡位需要重新绑定数据,
有时却又5个卡位都需要重新绑定数据,这是为什么呢?

答:有时一行只有3个卡位需要重新绑定的原因跟Q2一样,
因为 mCachedView 里正好缓存着当前位置的 ViewHolder,本来就是它的 ViewHolder 当然可以直接拿来用。

而至于为什么会创建了17个 ViewHolder,那是因为再第四行的卡位要显示出来时,ViewPool 里只有3个缓存,
而第四行的卡位又用不了 mCachedViews 里的2个缓存,因为这两个缓存的是0、1卡位的 ViewHolder,
所以就需要再重新创建2个 ViewHodler 来给第四行最后的两个卡位使用。

二、ListView的原理和复用机制

1、Adapter

adapter 在 ListView 和数据源之间起到了一个桥梁的作用,
ListView 不会直接和数据源打交道,而是会借助 Adapter 这个桥梁来去访问真正的数据源

2、RecycleBin 机制(AbsListView.RecycleBin)

RecycleBin 维护了两种类型列表,
一个用于保存展示在屏幕上的View,一个用于缓存滚动出屏幕的View

ListView滑动过程中,子View完全移出屏幕后,会加入RecycleBin 缓存
子View进入屏幕时,从RecycleBin 中获取缓存View,用于数据绑定。

	public abstract class AbsListView extends AdapterView<ListAdapter> implements ... {
		
		class RecycleBin {
			private RecyclerListener mRecyclerListener;
		 
			//表示ListView中第一个可见元素的position值
			private int mFirstActivePosition;
		 
			/* 屏幕上可见的View,存放在 mActiveViews 数组中
			 * Head  Foot,不会存在 mActiveViews 里
			 */
			private View[] mActiveViews = new View[0];
		 
			/**
			 *离开屏幕后,View会被移动到 mScrapViews 数组中
			 * mViewTypeCount > 1时,存储该数组中。
			 * 数组下标为viewType,数组元素为该viewType对应的废弃view列表
			 */
			private ArrayList<View>[] mScrapViews;
			
			/**
			 * 用于存储废弃(滚动出屏幕)的View,
			 *
			 * 只有一种数据类型,ViewTypeCount == 1时,存该列表中
			 */
			private ArrayList<View> mCurrentScrap;
			//布局类型数量
			private int mViewTypeCount;
		 
			/**
			 * ListView中所有屏幕上可见的View
			 * 
			 * @param childCount 表示要存储的view的数量
			 * @param firstActivePosition 表示ListView中第一个可见元素的position值
			 */
			void fillActiveViews(int childCount, int firstActivePosition) {
				if (mActiveViews.length < childCount) {
					mActiveViews = new View[childCount];
				}
				mFirstActivePosition = firstActivePosition;
				final View[] activeViews = mActiveViews;
				for (int i = 0; i < childCount; i++) {
					View child = getChildAt(i);
					AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
					if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
						//View塞入数组中
						activeViews[i] = child;
						...
					}
				}
			}
		 
			/**
			 * 获取 ListView中 position 位置的 View
			 *
			 * mActiveViews当中所存储的View,一旦被获取了之后就会从mActiveViews当中移除,
			 * 下次获取同样位置的View将会返回null,也就是说mActiveViews不能被重复利用
			 * 
			 * @param position 表示元素在ListView当中的位置
			 */
			View getActiveView(int position) {
				//将position值转换成mActiveViews数组对应的下标值
				int index = position - mFirstActivePosition;
				final View[] activeViews = mActiveViews;
				if (index >= 0 && index < activeViews.length) {
					//从数组中取出匹配的View
					final View match = activeViews[index];
					//View取出后,删除数组中对应位置的数据
					activeViews[index] = null;
					return match;
				}
				return null;//没有找到,返回null
			}
			
			/**
			 * 将一个废弃(滚动出屏幕)的View进行缓存
			 * head foot不会缓存
			 */
			void addScrapView(View scrap) {
				AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
				if (lp == null) {
					return;
				}
				// Don't put header or footer views or views that should be ignored
				// into the scrap heap
				int viewType = lp.viewType;
				//viewType < 0 时,会返回false
				if (!shouldRecycleViewType(viewType)) {
					if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
						removeDetachedView(scrap, false);
					}
					return;
				}
				...
				if (mViewTypeCount == 1) {//只有一种布局类型
					dispatchFinishTemporaryDetach(scrap);
					mCurrentScrap.add(scrap);//存到 mCurrentScrap 列表中
				} else {
					dispatchFinishTemporaryDetach(scrap);
					//多布局,根据viewType存到 mScrapViews 数组中
					mScrapViews[viewType].add(scrap);
				}
				if (mRecyclerListener != null) {
					mRecyclerListener.onMovedToScrapHeap(scrap);
				}
			}
			
			/**
			 * 根据position 从废弃缓存中取出一个View
			 * 这些废弃View,没有顺序,直接从列表尾部获取一个 scrapView 返回
			 */
			View getScrapView(int position) {
				ArrayList<View> scrapViews;
				if (mViewTypeCount == 1) {//单布局,从mCurrentScrap从取出末尾的 scrapView返回
					scrapViews = mCurrentScrap;
					int size = scrapViews.size();
					if (size > 0) {
						return scrapViews.remove(size - 1);
					} else {
						return null;
					}
				} else {//多布局
					//根据position获取viewType
					int whichScrap = mAdapter.getItemViewType(position);
					if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
						//根据viewType获取缓存列表
						scrapViews = mScrapViews[whichScrap];
						int size = scrapViews.size();
						if (size > 0) {
							return scrapViews.remove(size - 1);
						}
					}
				}
				return null;
			}
		 
			public void setViewTypeCount(int viewTypeCount) {
				if (viewTypeCount < 1) {
					throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
				}
				// noinspection unchecked
				ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
				for (int i = 0; i < viewTypeCount; i++) {
					scrapViews[i] = new ArrayList<View>();
				}
				mViewTypeCount = viewTypeCount;
				mCurrentScrap = scrapViews[0];
				mScrapViews = scrapViews;
			}
		}
	}

推荐阅读:
Android ListView工作原理完全解析,带你从源码的角度彻底理解 https://blog.csdn.net/guolin_blog/article/details/44996879
RecyclerView的缓存机制 https://blog.csdn.net/wzy_1988/article/details/81569156

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值