我们都知道ListView的baseAdapter中,使用了一个view的缓存回收机制,我们经常被告知会把不可见的view缓存起来,并且在新的view显示时会重用之前回收的view,实际中在开发时会使用convertView去进行相关处理。那么我们不禁会好奇,这个缓存回收机制到底是怎么实现的?我们不该仅仅会使用BaseAdapter重写各个方法就够了,我们需要往深处去挖点宝藏。今天我们就来看看listView和adapter在视图回收和缓存环节是怎么做到的。
AbsListView ListView是一个继承自AbsListView的类,要想深入这一部分,我们需要看看AbsListView的源码。AbsListView还是比较复杂的,但是我们可以在ListView的scrollListItemsBy,layoutChildren等方法中看到几个叫mRecycler 和recycleBin的对象,看命名似乎是和视图回收机制重用等有关,mRecycler就是来自AbsListView的,我们可以继续看下去。
1 2 3 4 5 * The data set used to store unused views that should be reused during the next layout * to avoid creating new ones */ final RecycleBin mRecycler = new RecycleBin();
涉及到这个功能,recycleBin是一个RecycleBin对象,RecycleBin是AbsListView的内部类。那我们就来研究一下RecycleBin这个类。 我们先看看这个注释。
1 2 3 4 5 6 7 * 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实现了布局中view的重用。RecycleBin有两个层级的存储。
ActiveViews , 布局开始时要在屏幕中显示的view
ScrapViews, 布局结束后所有的ActiveViews就降级为ScrapViews。ScrapViews就是旧view,主要是可能被adapter为了避免不必要的视图分配空间而重用。 好了,RecycleBin的结构我们搞懂了,那么关于ListView的核心问题就变成了RecycleBin是怎么运用ActiveViews和ScrapViews的了。换句话说就是ActiveViews和ScrapViews是怎么产生、怎么添加、怎么交换的?
嗯,继续看源码。 private View[] mActiveViews = new View[0]; private ArrayList[] mScrapViews;
mActiveViews就是一个ActiveViews堆,可以看到mActiveViews是一个View数组。 mScrapViews是一个ScrapViews堆,是一个ArrayList数组。这里为什么要这样设计存储?我们先卖个关子。 在研究这两个不同层级的View堆前,我们先看看在ListView中怎么使用RecycleBin的。
setAdapter方法中,mRecycler.clear();
setAdapter方法中,设计到了viewType的操作,因为会有不同的视图结构,mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
onMeasure方法中,
1 2 3 if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(((LayoutParams) child.getLayoutParams()).viewType)) { mRecycler.addScrapView(child, 0 ); }
makeAndAddView方法中,mRecycler.getActiveView(position);
layoutChildren方法中,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 final RecycleBin recycleBin = mRecycler;if (dataChanged) { for (int i = 0 ; i < childCount; i++) { recycleBin.addScrapView(getChildAt(i), firstPosition+i); } } else { recycleBin.fillActiveViews(childCount, firstPosition); } detachAllViewsFromParent(); recycleBin.removeSkippedScrap(); …… recycleBin.scrapActiveViews();
其中在layoutChildren中的用法特别典型,我们具体来看一看。 以上的这段代码可以大概看出一些逻辑思路:
先判断数据是否有改变,如果改变了就将当前的children加到ScrapViews中,否则加到ActiveViews中。
removeSkippedScrap,把旧的view都删掉。
最后将以上没有被重用的缓存的view都回收掉。将当前的ActiveVies 移动到 ScrapViews。
以上是我们通过这一段代码的一个猜测分析,现在一步步看看源码。 dataChanged是一个AdapterView的boolean变量。 其中ListView 继承自AbsListView, AbsListView 继承自AdapterView,AdapterView继承自ViewGroup. 对dataChanged的赋值主要是在AdapterView中的内部类AdapterDataSetObserver中进行的。我们知道listView的adapter使用了观察者模式。这个是怎么做到的? 我们先看看AdapterDataSetObserver的源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 class AdapterDataSetObserver extends DataSetObserver { private Parcelable mInstanceState = null ; @Override public void onChanged () { mDataChanged = true ; mOldItemCount = mItemCount; mItemCount = getAdapter().getCount(); if (AdapterView.this .getAdapter().hasStableIds() && mInstanceState != null && mOldItemCount == 0 && mItemCount > 0 ) { AdapterView.this .onRestoreInstanceState(mInstanceState); mInstanceState = null ; } else { rememberSyncState(); } checkFocus(); requestLayout(); } @Override public void onInvalidated () { mDataChanged = true ; if (AdapterView.this .getAdapter().hasStableIds()) { mInstanceState = AdapterView.this .onSaveInstanceState(); } mOldItemCount = mItemCount; mItemCount = 0 ; mSelectedPosition = INVALID_POSITION; mSelectedRowId = INVALID_ROW_ID; mNextSelectedPosition = INVALID_POSITION; mNextSelectedRowId = INVALID_ROW_ID; mNeedSync = false ; checkFocus(); requestLayout(); } public void clearSavedState () { mInstanceState = null ; } }
代码很简单,继承自DataSetObserver,重写了onChanged和onInvalidated两个方法,对mDataChanged的操作都是在数据发生改变后将mDataChanged设为true,那么在哪里会变成false呢?AdapterView中已经没有了。我们还需要回到ListView中继续看。
在ListView中搜索这个变量会发现,将其变为false还是同样的在layoutChildren中,并且是完成了对mActiveViews和mScrapViews的各种操作之后才变为false。 并且在makeAndView中使用了false时的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private View makeAndAddView (int position, int y, boolean flow, int childrenLeft, boolean selected) { View child; if (!mDataChanged) { child = mRecycler.getActiveView(position); if (child != null ) { setupChild(child, position, y, flow, childrenLeft, selected, true ); return child; } } child = obtainView(position, mIsScrap); setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0 ]); return child; }
makeAndAddView能够获取一个view并且把它添加到child的list中,并且返回了这个child,这个child可以是新view,也可以是没有使用过的view convert过来的,或者说是从缓存中重用的view。 这当中有一个getActiveView方法,就是我们在之前提到的RecycleBin的第四个用法,也是在mDataChanged为false时一个处理方法。 好了,对mDataChanged的分析和“寻找”先到这里,我们接着看看RecycleBin的用法。
既然是我们之前讲过的处理流程,我们先看看最初的一个RecycleBin使用情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 void addScrapView (View scrap, int position) { final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams(); if (lp == null ) { return ; } lp.scrappedFromPosition = position; ...... final boolean scrapHasTransientState = scrap.hasTransientState(); if (scrapHasTransientState) { if (mAdapter != null && mAdapterHasStableIds) { if (mTransientStateViewsById == null ) { mTransientStateViewsById = new LongSparseArray<View>(); } mTransientStateViewsById.put(lp.itemId, scrap); } else if (!mDataChanged) { if (mTransientStateViews == null ) { mTransientStateViews = new SparseArray<View>(); } mTransientStateViews.put(position, scrap); } else { if (mSkippedScrap == null ) { mSkippedScrap = new ArrayList<View>(); } mSkippedScrap.add(scrap); } }else { if (mViewTypeCount == 1 ) { mCurrentScrap.add(scrap); } else { mScrapViews[viewType].add(scrap); } if (mRecyclerListener != null ) { mRecyclerListener.onMovedToScrapHeap(scrap); } } }
我们可以看到,这里的逻辑还算很清晰,在关于view的重用的判断时,涉及到一个概念还是需要解释一下,就是view的瞬态。 View.hasTransientState()的代码就不贴了,主要是几个flag的运算,虽然就一行,但是需要前后联系,感兴趣的朋友可以去看看源码。 我们这里主要理解瞬态,当我们说一个view有瞬态时,我们指app无需再关心这个view的保存与恢复,注释指出一般用来播放动画或者记录选择的位置等相似的行为。 当一个view标记位有瞬态时,在RecycleBin中,就有可能不会降级到ScrapView,而是mTransientStateViews或者mTransientStateViewsById将其保存起来,以便于 后来的重用。
1 2 private SparseArray<View> mTransientStateViews;private LongSparseArray<View> mTransientStateViewsById;
在RecycleBin中涉及到瞬态的存储结构是上述代码中的两个,其实就是SparseArray,一个存的是position的key,一个存的是id。
这里介绍了mTransientStateViews的写,我们再看看mTransientStateViews的读。这里的读也是读的SparseArray,虽然功能类似于HashMap,但是读数据时使用 的是valueAt方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 View getTransientStateView (int position) { if (mAdapter != null && mAdapterHasStableIds && mTransientStateViewsById != null ) { long id = mAdapter.getItemId(position); View result = mTransientStateViewsById.get(id); mTransientStateViewsById.remove(id); return result; } if (mTransientStateViews != null ) { final int index = mTransientStateViews.indexOfKey(position); if (index >= 0 ) { View result = mTransientStateViews.valueAt(index); mTransientStateViews.removeAt(index); return result; } } return null ; }
上面这份代码就是用来获取瞬时态的view的,首先从mTransientStateViewsById中读取,如果没有就从mTransientStateViews中读取。 这个方法是属于RecycleBin的,但是这个是在哪里调用的呢?答案是AbsListView的obtainView方法。
这里是一个比较关键的地方了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 View obtainView (int position, boolean [] isScrap) { ...... final View transientView = mRecycler.getTransientStateView(position); if (transientView != null ) { final LayoutParams params = (LayoutParams) transientView.getLayoutParams(); if (params.viewType == mAdapter.getItemViewType(position)) { final View updatedView = mAdapter.getView(position, transientView, this ); if (updatedView != transientView) { setItemViewLayoutParams(updatedView, position); mRecycler.addScrapView(updatedView, position); } } ...... return transientView; } final View scrapView = mRecycler.getScrapView(position); final View child = mAdapter.getView(position, scrapView, this ); if (scrapView != null ) { if (child != scrapView) { mRecycler.addScrapView(scrapView, position); } else { isScrap[0 ] = true ; child.dispatchFinishTemporaryDetach(); } } ...... setItemViewLayoutParams(child, position); ...... return child; }
阅读代码可以发现,在obtainView中,最重要的一个方法就是调用了adapter的getView方法,这个方法也是我们平常重写的方法,getView返回的是一个view。 而在对这个view的获取过程,有一个很明显的两层处理,首先就是尝试获取我们之前分析介绍的transientView,也就是拥有瞬时态的view,通过transientView 以完成复用。如果这一步走的失败了,也就是transientView为null时,或者说这个view并不拥有瞬时态,那么就从ScrapView中获取一个scrapView,这里可能要对 scrapView多做一些处理,我在上面都省略了,不影响逻辑大局,感兴趣的朋友可以去看看源码。最后返回的是scrapView。
getTransientStateView我们已经介绍过了,现在来看看失败之后的getScrapView。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 View getScrapView (int position) { if (mViewTypeCount == 1 ) { return retrieveFromScrap(mCurrentScrap, position); } else { final int whichScrap = mAdapter.getItemViewType(position); if (whichScrap >= 0 && whichScrap < mScrapViews.length) { return retrieveFromScrap(mScrapViews[whichScrap], position); } } return null ; } private View retrieveFromScrap (ArrayList<View> scrapViews, int position) { final int size = scrapViews.size(); if (size > 0 ) { 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 ; } }
上面这份代码还是写的很清楚了,根据不同的viewType来进行不同策略的取scrapView,但是实质上都是走到了retrieveFromScrap。 思路还是比较清晰,但是需要注意的是在get到scrapView后,在scrapView堆中这个view就会被移除掉。以便以后不停的循环往复的重用。
以上的分析都是在layoutChildren中判断数据改变后的过程,当数据没有改变时,会走到fillActiveViews方法。这个方法能够将AbsListView的所有子view都 装到activeViews中。代码很简单,就是一个for循环,将listView的所有子view一一存到activeViews中。
接着我们最初的分析,这些都走完之后,会执行recycleBin的removeSkippedScrap方法。还记得我们介绍的addScrapView方法吗,当一个view是有瞬时态的,但是 却没有一个保存到mTransientStateViewsById或者mTransientStateViews中时,会存在mSkippedScrap中。这是都会通过removeSkippedScrap全部清空。
在layoutChildren中,涉及到RecycleBin的最后还有一个方法就是scrapActiveViews。因为已经完成了children的布局layout位置的摆放等,所以这个时候需要刷新 缓存,scrapActiveViews这部分代码可能处理的过程有点多,但是最重要的一件事就是将现在mActiveViews还剩下的views都会移到mScrapViews中。
这个迁移过程也是和addScrapView的过程差不多。一开始是先对mActiveViews遍历,每次保存当前结点victim,并将mActiveViews[i]置空,然后对victim进行迁移 操作,如果符合条件就加到mTransientStateViewsById或者mTransientStateViews中,否则就加到mScrapViews中。
好了,至此在addScrapView中对RecycleBin进行的一系列操作就讲完了,我们看看下一步是什么。
ListView的makeAndAddView方法。 makeAndAddView的代码我们之前也贴过了,当判断数据没有发生改变时,会走到RecycleBin的getActiveView方法。 那我们来看看getActiveView。
1 2 3 4 5 6 7 8 9 10 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 ; }
getActiveView的代码就很简单了,根据丢过来的position计算出一个真实有效的index,然后从activeViews中获取相应的view。没什么可讲的。
ok,关于RecycleBin的几个主要方法和执行流程就介绍完了。
我们可以暂时回顾小结一下,实际上RecycleBin的主要结构就是三个,一个是activeView堆,结构是一个View数组,另一个是scrapView堆,结构是一个 ArrayList数组。还有一个是transientViews,结构是SparseArray,主要通过Id或者position存取。 几个结构可以理解为层级不同,activeView比scrapView高一点,当触发了某种条件或者机制后,child的view就会从activieView中移到transientViews或者scrapView中进行缓存。 当ListView需要obtainView时,会先从有瞬时态的sparseArray中获取view,当失败时就会去scrapViews中获取view。 当然,这些过程又是和一个boolean变量mDataChanged进行配合的,具体的过程在上面的源码分析中已经解释过了,诸位可以回过去看看。 基本思路是在给子view布局时,如果数据没有发生改变,就使用当前已经存在ActiveViews的view。 在obtainView时,如果发生了改变,就addScrapView.否则就fill with activeView..
再次说到addScrapView,由于这是一个比较重要的方法,这里小结时我们也来看看哪些地方调用了addScrapView. 我们可以在ListView源码中搜索addScrapView看看。
onMeasure
measureHeightOfChildren , measure listView指定范围的高度, 在onMeasure中调用
layoutChildren
scrollListItemsBy , 以一定child数目滑动List,需要将滑出的child删掉,在最后添加view
其实前三个中用到的addScrapView我们之前也已经都讲到了,addScrapView的实现过程也不算复杂,主要是和activeView以及有瞬态的view的配合使用。 第四个我们接下来讲一下。
既然了解了RecycleBin的缓存结构和基本方法后,我们来实战看看,在一个Listview滑动过程中,到底是怎么实现view的回收的吧。
现在考虑滑动一个ListView的情况,也就是scrollListItemsBy方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 private void scrollListItemsBy (int amount) { ... final AbsListView.RecycleBin recycleBin = mRecycler; if (amount < 0 ) { ... View last = getChildAt(numChildren - 1 ); while (last.getBottom() < listBottom) { final int lastVisiblePosition = mFirstPosition + numChildren - 1 ; if (lastVisiblePosition < mItemCount - 1 ) { last = addViewBelow(last, lastVisiblePosition); numChildren++; } ... } ... View first = getChildAt(0 ); while (first.getBottom() < listTop) { AbsListView.LayoutParams layoutParams = (LayoutParams) first.getLayoutParams(); if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) { recycleBin.addScrapView(first, mFirstPosition); } ... } ... }else { ... View last = getChildAt(lastIndex); ... if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) { recycleBin.addScrapView(last, mFirstPosition + lastIndex); } ... } }
可以看到,每次ListView的滑动事件要将一个view滑出屏幕时,会将头部的或者尾部的(视方向而定)childView通过addScrapView缓存起来, 缓存的流程就是我们一开始分析的了,先保存有瞬时态的view,然后视情况存到scrapViews中。
等到每次obtainView时再依序从缓存的view中取出来。
这样就完成了一个滑动的缓存与回收。
好了,关于ListView的回收机制到这里就讲的差不多了,本质上就是对AbsListView的内部类RecycleBin的操作。 当我们弄懂了这个机制,才能更好的思考更多的问题。
例如,最后我问大家一个问题:
根据我之前所讲的,当一个ListView的有若干个viewType, 当滑出的view和滑入添加的view的type不一样,比如说滑出了一个TextView的item, 滑入了一个ImageView的Item, 那这种情况下还能复用刚才的view吗?