Android(安卓)ListView复用机制详解
最近用到RecyclerView,想研究RecyclerView和ListView复用机制的区别,这篇文章以解析源码的方式解析ListView复用机制。
ListView复用是通过AbsListView的RecycleBin内部类来实现的,源码注释如下:
/** * The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of * storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the * start of a layout. By construction, they are displaying current information. At the end of * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that * could potentially be used by the adapter to avoid allocating views unnecessarily. */
RecycleBin是2级的存储结构, ActiveViews: 当前屏幕上的活动View
ScrapViews: 废弃View,可复用的旧View
再看RecycleBin的成员变量
//回收Listener,当View变为可回收,即ScrapView时,会通过mRecyclerListener通知注册者,listener可通过setRecyclerListener注册private RecyclerListener mRecyclerListener;/** * The position of the first view stored in mActiveViews. */// 第一个活动view的position,即第一个可视view的positionprivate int mFirstActivePosition;/** * Views that were on screen at the start of layout. This array is populated at the start of * layout, and at the end of layout all view in mActiveViews are moved to mScrapViews. * Views in mActiveViews represent a contiguous range of Views, with position of the first * view store in mFirstActivePosition. */// 活动view的集合private View[] mActiveViews = new View[0];/** * Unsorted views that can be used by the adapter as a convert view. *//**废弃的可修复view集合,复用时传递到Adapter#getView方法的convertView参数。 * 因为item type可能大于1,只有view type相同的view之间才能复用,所以是个二维数组 */private ArrayList[] mScrapViews;// ListView item type数量private int mViewTypeCount;// 当前的废弃view数组,定义这个成员是为了在mViewTypeCount为1时使用方便,不需要去取mScrapViews的第一个元素private ArrayList mCurrentScrap;// 被跳过的,不能复用的view集合。view type小于0或者处理transient状态的view不能被复用。private ArrayList mSkippedScrap;// 处于transient状态的view集合,处于transient状态的view不能被复用,如view的动画正在播放,// transient是瞬时、过渡的意思,关于transient状态详见android.view.View#PFLAG2_HAS_TRANSIENT_STATEprivate SparseArray mTransientStateViews;// 如果adapter的hasStableIds方法返回true,处于过度状态的view保存到这里。因为需要保存view的position,而且处于过度状态的view一般很少,// 这2个成员用了稀疏数组。具体不需要case,知道是保存转换状态view的集合就行。private LongSparseArray mTransientStateViewsById;
从RecycleBin成员变量的定义基本可以看出复用的原理:
1. 废弃的view保存在一个数组中,复用时从中取出
2. 拥有相同view type的view之间才能复用,所以mScrapViews是个二维数组
3. 处于transient状态的view不能被复用
再来看方法
setViewTypeCount: 这个方法在给ListView设置adapter时调用,取值是adapter#getViewTypeCount()public void setViewTypeCount(int viewTypeCount) { if (viewTypeCount < 1) { throw new IllegalArgumentException("Can't have a viewTypeCount < 1"); } //noinspection [] scrapViews = new ArrayList[viewTypeCount]; for (int i = 0; i < viewTypeCount; i++) { scrapViews[i] = new ArrayList(); } mViewTypeCount = viewTypeCount; mCurrentScrap = scrapViews[0]; mScrapViews = scrapViews; }
markChildrenDirty: 当ListView size或position变化时,设置mScrapViews和transient views的forceLayout flag,在下一次被复用时会重新布置。
public void markChildrenDirty() { if (mViewTypeCount == 1) { final ArrayList scrap = mCurrentScrap; final int scrapCount = scrap.size(); for (int i = 0; i < scrapCount; i++) { scrap.get(i).forceLayout(); } } else { final int typeCount = mViewTypeCount; for (int i = 0; i < typeCount; i++) { final ArrayList scrap = mScrapViews[i]; final int scrapCount = scrap.size(); for (int j = 0; j < scrapCount; j++) { scrap.get(j).forceLayout(); } } } if (mTransientStateViews != null) { final int count = mTransientStateViews.size(); for (int i = 0; i < count; i++) { mTransientStateViews.valueAt(i).forceLayout(); } } if (mTransientStateViewsById != null) { final int count = mTransientStateViewsById.size(); for (int i = 0; i < count; i++) { mTransientStateViewsById.valueAt(i).forceLayout(); } } }
shouldRecycleViewType: 判断有当前viewType的view是否应该被回收复用,viewType小于0的不复用
public boolean shouldRecycleViewType(int viewType) { return viewType >= 0; }
fillActiveViews: 将屏幕上所有活动view填充到mActiveViews中,childCount为需保存的最小view个数,即当前屏幕上ListView所展示的view数,注意不是adapter的item数。 firstActivePosition为第一个可视view的position,即view在adapter中的
/** * Fill ActiveViews with all of the children of the AbsListView. * * @param childCount The minimum number of views mActiveViews should hold * @param firstActivePosition The position of the first view that will be stored in * mActiveViews */ void fillActiveViews(int childCount, int firstActivePosition) { if (mActiveViews.length < childCount) { mActiveViews = new View[childCount]; } mFirstActivePosition = firstActivePosition; //noinspection MismatchedReadAndWriteOfArray final View[] activeViews = mActiveViews; for (int i = 0; i < childCount; i++) { View child = getChildAt(i); AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams(); // Don't put header or footer views into the scrap heap if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { // Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views. // However, we will NOT place them into scrap views. activeViews[i] = child; // Remember the position so that setupChild() doesn't reset state. lp.scrappedFromPosition = firstActivePosition + i; } } }
getActiveView: 查找mActiveView中指定position的view,找到后将从mActiveViews中移除
/** * Get the view corresponding to the specified position. The view will be removed from * mActiveViews if it is found. * * @param position The position to look up in mActiveViews * @return The view if it is found, null otherwise */ View getActiveView(int position) { int index = position - mFirstActivePosition; final View[] activeViews = mActiveViews; if (index >=0 && index < activeViews.length) { final View match = activeViews[index]; activeViews[index] = null; return match; } return null; }
getScrapView: 查找复用的view,先通过position从adapter中找到view type,然后再从相应的scrap中查找
/** * @return A view from the ScrapViews collection. These are unordered. */ View getScrapView(int position) { final int whichScrap = mAdapter.getItemViewType(position); if (whichScrap < 0) { return null; } if (mViewTypeCount == 1) { return retrieveFromScrap(mCurrentScrap, position); } else if (whichScrap < mScrapViews.length) { return retrieveFromScrap(mScrapViews[whichScrap], position); } return null; }
retrieveFromScrap: 从指定的scrap中查找可复用的view,分别3个优先级: 1. 如adapter有stable ids,则先比较id 2. 如adapter无stable ids,则查找是否有在当前position被回收利用的view 3. 如优先级1,2都没查找到,则取最近加入scrap中的view
private View retrieveFromScrap(ArrayList scrapViews, int position) { final int size = scrapViews.size(); if (size > 0) { // See if we still have a view for this position or ID. for (int i = 0; i < size; i++) { final View view = scrapViews.get(i); final AbsListView.LayoutParams params = (AbsListView.LayoutParams) view.getLayoutParams(); if (mAdapterHasStableIds) { final long id = mAdapter.getItemId(position); if (id == params.itemId) { return scrapViews.remove(i); } } else if (params.scrappedFromPosition == position) { final View scrap = scrapViews.remove(i); clearAccessibilityFromScrap(scrap); return scrap; } } final View scrap = scrapViews.remove(size - 1); clearAccessibilityFromScrap(scrap); return scrap; } else { return null; } }
addScrapView: 将可复用的view加入scrap数组中
/** * Puts a view into the list of scrap views. * * If the list data hasn't changed or the adapter has stable IDs, views * with transient state will be preserved for later retrieval. * * @param scrap The view to add * @param position The view's position within its parent */ void addScrapView(View scrap, int position) { final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams(); if (lp == null) { // Can't recycle, but we don't know anything about the view. // Ignore it completely. return; } lp.scrappedFromPosition = position; // Remove but don't scrap header or footer views, or views that // should otherwise not be recycled. final int viewType = lp.viewType; if (!shouldRecycleViewType(viewType)) { //不能被复用的view type,并且不为header或footer时加入skipped scrap // Can't recycle. If it's not a header or footer, which have // special handling and should be ignored, then skip the scrap // heap and we'll fully detach the view later. if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { getSkippedScrap().add(scrap); } return; } scrap.dispatchStartTemporaryDetach(); // The the accessibility state of the view may change while temporary // detached and we do not allow detached views to fire accessibility // events. So we are announcing that the subtree changed giving a chance // to clients holding on to a view in this subtree to refresh it. notifyViewAccessibilityStateChangedIfNeeded( AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE); // Don't scrap views that have transient state. final boolean scrapHasTransientState = scrap.hasTransientState(); if (scrapHasTransientState) { //view处于transient状态 if (mAdapter != null && mAdapterHasStableIds) { //adpater has stable ids,加入mTransientStateViewsById中 // If the adapter has stable IDs, we can reuse the view for // the same data. if (mTransientStateViewsById == null) { mTransientStateViewsById = new LongSparseArray<>(); } mTransientStateViewsById.put(lp.itemId, scrap); } else if (!mDataChanged) { //数据未变化,加入mTransientStateViews中 // If the data hasn't changed, we can reuse the views at // their old positions. if (mTransientStateViews == null) { mTransientStateViews = new SparseArray<>(); } mTransientStateViews.put(position, scrap); } else { //adapter为null或adpater不为null,无stable ids,且数据变化,则丢弃scrap view // Otherwise, we'll have to remove the view and start over. getSkippedScrap().add(scrap); } } else { //不处于transient状态,将scrap view加入mScarpViews中 if (mViewTypeCount == 1) { //view type count为1时,为方便,直接操作mCurrentScrap mCurrentScrap.add(scrap); } else { mScrapViews[viewType].add(scrap); } if (mRecyclerListener != null) { mRecyclerListener.onMovedToScrapHeap(scrap); } } }
scrapActiveViews: 将mActiveViews中剩余的所有view移到mScrapViews中
/** * Move all views remaining in mActiveViews to mScrapViews. */ void scrapActiveViews() { final View[] activeViews = mActiveViews; final boolean hasListener = mRecyclerListener != null; final boolean multipleScraps = mViewTypeCount > 1; ArrayList scrapViews = mCurrentScrap; final int count = activeViews.length; for (int i = count - 1; i >= 0; i--) { final View victim = activeViews[i]; if (victim != null) { final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) victim.getLayoutParams(); final int whichScrap = lp.viewType; activeViews[i] = null; if (victim.hasTransientState()) { // Store views with transient state for later use. victim.dispatchStartTemporaryDetach(); if (mAdapter != null && mAdapterHasStableIds) { if (mTransientStateViewsById == null) { mTransientStateViewsById = new LongSparseArray(); } long id = mAdapter.getItemId(mFirstActivePosition + i); mTransientStateViewsById.put(id, victim); } else if (!mDataChanged) { if (mTransientStateViews == null) { mTransientStateViews = new SparseArray(); } mTransientStateViews.put(mFirstActivePosition + i, victim); } else if (whichScrap != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { // The data has changed, we can't keep this view. removeDetachedView(victim, false); } } else if (!shouldRecycleViewType(whichScrap)) { // Discard non-recyclable views except headers/footers. if (whichScrap != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { removeDetachedView(victim, false); } } else { // Store everything else on the appropriate scrap heap. if (multipleScraps) { scrapViews = mScrapViews[whichScrap]; } victim.dispatchStartTemporaryDetach(); lp.scrappedFromPosition = mFirstActivePosition + i; scrapViews.add(victim); if (hasListener) { mRecyclerListener.onMovedToScrapHeap(victim); } } } } pruneScrapViews(); }
剩下的方法由于篇幅所限,不一一贴出源码,在此只对方法的功能做下说明:
//清除所有scrap view和transient viewvoid clear();//获取当前position下的transient view,如无则返回nullView getTransientStateView(int position);//获取mSkippedScrapprivate ArrayList getSkippedScrap();//清除mSkippedScrapvoid removeSkippedScrap();//裁剪mScarpViews,确保size不大于mActiveViews的size;移除已不是transient state的viewprivate void pruneScrapViews();//将所有scrap views放入指定的views中,这个方法没弄明白,不知何时调用void reclaimScrapViews(List views);//为每一个scrap和active view设置缓存背影色void setCacheColorHint(int color);//从ListView层次中移除viewprivate void removeDetachedView(View child, boolean animate);
未完待续
更多相关文章
- 初学Android,手机管理器之监听手机来电(六十三)
- android与C# WebService基于ksoap通信(Android篇)
- Android实现信号强度监听的方法
- android中各种组件的生命周期问题
- 禁止应用获取手机信息
- android中返回页面并刷新
- 源码分析android 系统framework(一)之Activity 与 Window 与 View
- Android(安卓)TabLayout 不显示标题的解决方法
- Android开发指南(39) —— Testing Fundamentals