AbsListView 继承关系
从继承关系中可以看出,GridView 和 ListView 都是继承自 AbsListView,因此两者在工作原理和实现上都是有很多共同点的,而缓存逻辑应为 GridView 和 ListView 共有的,因此缓存应该是在 AbsListView 中实现的。
Adapter 的作用 顾名思义,Adapter 是适配器的意思,它的作用是在 ListView 和数据源中起到一个桥梁的作用,ListView 并不会直接和数据源打交道,而是借助 Adapter 这个桥梁去访问真正的数据源。
Adapter 提供统一的接口让 ListView 不必担心任何适配问题,用户可以继承 Adapter 去实现与特定数据的适配功能。
RecycleBin ListView 能够实现成百上千条数据都不会出现 OOM ,最重要的一个原因就是 RecycleBin 机制,它是 AsbListView 的一个内部类,GridView 和 ListView 都可以使用它。
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 class RecycleBin { private RecyclerListener mRecyclerListener; private int mFirstActivePosition; private View[] mActiveViews = new View[0 ]; private ArrayList<View>[] mScrapViews; private int mViewTypeCount; private ArrayList<View> mCurrentScrap; void fillActiveViews (int childCount, int firstActivePosition) { } View getActiveView (int position) { return null ; } void addScrapView (View scrap, int position) { } View getScrapView (int position) { return null ; } private View retrieveFromScrap (ArrayList<View> scrapViews, int position) { if (...){ return scrap; } else { return null ; } } public void setViewTypeCount (int viewTypeCount) { } }
重要的成员变量:
1 2 3 4 5 6 7 8 9 10 private int mFirstActivePosition;private View[] mActiveViews = new View[0 ];private ArrayList<View>[] mScrapViews;private int mViewTypeCount;private ArrayList<View> mCurrentScrap;
重要方法:
fillActiveViews() : 这个方法接收两个参数,第一个参数表示要存储的 View 的数量,第二个参数表示 ListView 中第一个可见元素的 position 值。RecycleBin 当中使用 mActiveViews 这个数组来存储 View,调用这个方法后就会根据传入的参数来将 ListView 中的指定元素存储到mActiveViews 数组当中。
getActiveView() : 这个方法和 fillActiveViews()
是对应的,用于从 mActiveViews 数组当中获取数据。该方法接收一个 position 参数,表示元素在 ListView 当中的位置,方法内部会自动将position 值转换成 mActiveViews 数组对应的下标值。需要注意的是,mActiveViews 当中所存储的 View,一旦被获取了之后就会从 mActiveViews 当中移除,下次获取同样位置的 View 将会返回 null,也就是说 mActiveViews 不能被重复利用。
addScrapView() :用于将一个废弃的 View 进行缓存,该方法接收一个 View 参数,当有某个View 确定要废弃掉的时候(比如滚动出了屏幕),就应该调用这个方法来对 View 进行缓存,RecycleBin 当中使用 mScrapViews 和 mCurrentScrap 这两个 List 来存储废弃 View。
getScrapView() : 用于从废弃缓存中取出一个 View,这些废弃缓存中的 View 是没有顺序可言的,因此 getScrapView()
方法中的算法也非常简单,就是直接从 mCurrentScrap 当中获取尾部的一个 scrap view 进行返回。
setViewTypeCount() : 我们都知道 Adapter 当中可以重写一个 getViewTypeCount()
来表示ListView 中有几种类型的数据项,而 setViewTypeCount()
方法的作用就是为每种类型的数据项都单独启用一个 RecycleBin 缓存机制。实际上,getViewTypeCount()
方法通常情况下使用的并不是很多,所以我们只要知道 RecycleBin 当中有这样一个功能就行了。
Layout 过程
对于任意一个 View,先要显示到界面上,至少需要经过两次 onMeasure() 和 onLayout() 的调用。
对于其他 View ,我们并不关心它测量和布局了几次,因为逻辑都是一样,但是对于 ListView 来说,它是在 layout 过程中添加数据的,如果多次 layout,那么不就重复添加数据了嘛?这里 ListView 做了很好的处理来避免重复添加数据的问题。
第一次 Layout ListView 中并没有 onLayout()
这个方法,那肯定就是在父类 AbsListView 定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Override protected void onLayout (boolean changed, int l, int t, int r, int b) { super .onLayout(changed, l, t, r, b); mInLayout = true ; final int childCount = getChildCount(); if (changed) { for (int i = 0 ; i < childCount; i++) { getChildAt(i).forceLayout(); } mRecycler.markChildrenDirty(); } layoutChildren(); mInLayout = false ; mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR; if (mFastScroll != null ) { mFastScroll.onItemCountChanged(getChildCount(), mItemCount); } }
复杂逻辑被封装到了 layoutChildren()
中,在 AbsListView 中它是一个空方法,这很正常,因为子元素的布局应该由具体的子类来负责完成,而不是由父类来完成。
ListView 的 layoutChildren()
:
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 @Override protected void layoutChildren () { try { final int childrenTop = mListPadding.top; final int childrenBottom = mBottom - mTop - mListPadding.bottom; final int childCount = getChildCount(); int index = 0 ; int delta = 0 ; ...... boolean dataChanged = mDataChanged; if (dataChanged) { handleDataChanged(); } ...... } else { recycleBin.fillActiveViews(childCount, firstPosition); } detachAllViewsFromParent(); recycleBin.removeSkippedScrap(); switch (mLayoutMode) { default : if (childCount == 0 ) { if (!mStackFromBottom) { final int position = lookForSelectablePosition(0 , true ); setSelectedPositionInt(position); sel = fillFromTop(childrenTop); } else { final int position = lookForSelectablePosition(mItemCount - 1 , false ); setSelectedPositionInt(position); sel = fillUp(mItemCount - 1 , childrenBottom); } } break ; } recycleBin.scrapActiveViews(); ...... } }
第一次布局时,layoutChildrent()
主要要从第 15 行开始看起,调用 fillFromTop()
去从顶部填充 ListView。
跟进 fillFromTop()
:
1 2 3 4 5 6 7 8 9 10 private View fillFromTop (int nextTop) { ... return fillDown(mFirstPosition, nextTop); }
跟进 fillDown()
:
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 private View fillDown (int pos, int nextTop) { View selectedView = null ; int end = (mBottom - mTop); if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { end -= mListPadding.bottom; } while (nextTop < end && pos < mItemCount) { boolean selected = pos == mSelectedPosition; View child = makeAndAddView(pos, nextTop, true , mListPadding.left, selected); nextTop = child.getBottom() + mDividerHeight; if (selected) { selectedView = child; } pos++; } setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1 ); return selectedView; }
主要看 makeAndAddView()
,它的作用是获取一个 View,并添加到 ListView 中。
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 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; }
我们第1次 layout 时,尝试从 mActiveViews 中获取 View ,这里 child 肯定为 null,那么我们跟进obtainView()
,在ListView中没有找到该方法,那么应该在其父类中..跟进
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 View obtainView (int position, boolean [] isScrap) { isScrap[0 ] = false ; View scrapView; scrapView = mRecycler.getScrapView(position); View child; if (scrapView != null ) { child = mAdapter.getView(position, scrapView, this ); if (child != scrapView) { mRecycler.addScrapView(scrapView); if (mCacheColorHint != 0 ) { child.setDrawingCacheBackgroundColor(mCacheColorHint); } } else { isScrap[0 ] = true ; dispatchFinishTemporaryDetach(child); } } else { child = mAdapter.getView(position, null , this ); if (mCacheColorHint != 0 ) { child.setDrawingCacheBackgroundColor(mCacheColorHint); } } return child; }
主要的逻辑是:尝试从scrapViews复用View,如果没有,则通过 getView()
创建一个新的 View(也就 Adapter 中的那个 getView()
。
第一次 layout ,在 scrapViews 中肯定是没有缓存的,因此会创建一个新 View。
从这里可以看出,在 Adapter 的 getView(int position, View convertView, ViewGroup parent)
中,需要判断 convertView 是否为 null,如果为 null,则需要通过 LayoutInflater 加载 View,否则可以复用 View。
回到 makeAndAddView()
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 private View makeAndAddView (int position, int y, boolean flow, int childrenLeft, boolean selected) { View child; ...... child = obtainView(position, mIsScrap); setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0 ]); return child; }
跟进 setupChild()
:
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 private void setupChild (View child, int position, int y, boolean flowDown, int childrenLeft,boolean selected, boolean recycled) { ... if (recycled && ...){ attachViewToParent(child, flowDown ? -1 : 0 , p); }else { addViewInLayout(child, flowDown ? -1 : 0 , p, true ); } ... }
第一次 layout ,View 是通过 LayoutInflater新创建的,因此不可能进行过 recycled,所以会执行 addViewInLayout()
添加 View 到 ListView 中。那么根据 fillDown()
方法中的while循环,会让子元素 View 将整个 ListView 控件填满然后就跳出,也就是说即使我们的 Adapter 中有一千条数据,ListView 也只会加载第一屏的数据,剩下的数据反正目前在屏幕上也看不到,所以不会去做多余的加载工作,这样就可以保证 ListView 中的内容能够迅速展示到屏幕上。
至此,第一次 layout 结束。
ListView 的第一次 Layout 中,即使 Adapter 中有几千条数据,也只会填充一个屏幕的 ListView 子 View,而且没有涉及到 RecycleBin 的运作。
第二次 Layout 还是从 layoutChildren()
开始看:
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 @Override protected void layoutChildren () { invalidate(); ... recycleBin.fillActiveViews(childCount, firstPosition); detachAllViewsFromParent(); recycleBin.removeSkippedScrap(); switch (mLayoutMode) { ... default : if (childCount == 0 ) { } else { if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) { sel = fillSpecific(mSelectedPosition, oldSel == null ? childrenTop : oldSel.getTop()); } else if (mFirstPosition < mItemCount) { sel = fillSpecific(mFirstPosition, oldFirst == null ? childrenTop : oldFirst.getTop()); } else { sel = fillSpecific(0 , childrenTop); } } break ; } recycleBin.scrapActiveViews(); ...... } }
主要看 2 个操作:
detachAllViewsFromParent()
:把 ListView 中所有的子 View 清除掉,从而保证了第二次 Layout 过程不会添加重复数据。因为之前已经将 View 缓存到 ActiveViews 中了,因此待会儿用缓存来加载 Item View 会快很多,不会太影响效率。
fillSpecific()
:其实第二次 Layout 和第一次 Layout 的基本流程是差不多的,第 2 次 Layout 时 childCount 不为0,我们进入了:
1 2 3 4 5 6 7 8 9 if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) { sel = fillSpecific(mSelectedPosition, oldSel == null ? childrenTop : oldSel.getTop()); } else if (mFirstPosition < mItemCount) { sel = fillSpecific(mFirstPosition, oldFirst == null ? childrenTop : oldFirst.getTop()); } else { sel = fillSpecific(0 , childrenTop); }
因为我们还没有选中任何一个 View,所以 mSelectedPosition = -1,因此会进入第二个逻辑判断。
跟进 fillSpecific()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 / ** * Put a specific item at a specific location on the screen and then build * up and down from there. * @param position The reference view to use as the starting point * @param top Pixel offset from the top of this view to the top of the * reference view. * * @return The selected view, or null if the selected view is outside the * visible area. */ private View fillSpecific (int position, int top) { boolean tempIsSelected = position == mSelectedPosition; View temp = makeAndAddView(position, top, true , mListPadding.left, tempIsSelected); mFirstPosition = position; ... }
它和 fillUp()
、fillDown()
方法功能也是差不多的,主要的区别在于,fillSpecific()
方法会优先将指定位置的子View 先加载到屏幕上,然后再加载该子 View 往上以及往下的其它子 View。那么由于这里我们传入的 position 就是第一个子View 的位置,于是 fillSpecific()
方法的作用就基本上和 fillDown()
方法是差不多的了。
继续跟进 makeAndAddView()
:
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 / ** * 获取一个View并添加进ListView。这个View呢可能是新创建的,也有可能是来自mActiveViews,或者是来自mScrapViews * @param position * 列表中的逻辑位置 * @param y * 被添加View的上 边位置或者下 边位置 * @param flow * 如果flow是true ,那么y是View的上 边位置,否则那么y是View的下 边位置 * @param childrenLeft Left edge where children should be positioned * @param selected Is this position selected? * @return * 返回被添加的View */ 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; }
因为在 layoutChildren()
时已经将 ListView 中的 View 缓存到了 mActiveViews 中,因此这里会执行 21 行从缓存中获取 View,然后执行 setupChild()
,方法的最后传了一个 true 参数,表示当前 View 是被回收过的。
跟进 setupChild()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private void setupChild (View child, int position, int y, boolean flowDown, int childrenLeft, boolean selected, boolean recycled) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupListItem" ); ... if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) { attachViewToParent(child, flowDown ? -1 : 0 , p); } else { p.forceAdd = false ; if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { p.recycledHeaderFooter = true ; } } addViewInLayout(child, flowDown ? -1 : 0 , p, true ); } .... }
这里的参数 recycled = true,因此会执行 attachViewParent()
,而第一次 Layout 时会执行 addViewInLayout()
。两者的区别是,如果我们需要向 ViewGroup 中添加一个新的 View,应该调用 addViewInLayout()
,而如果想要将之前 detach 的 View 重新 attach 到 ViewGroup 上,就应该调用 attachToParent()
。那么由于前面在 layoutChildren()
方法当中调用了 detachAllViewsFromParent()
方法,这样 ListView 中所有的子 View 都是处于 detach 状态的,所以这里attachViewToParent()
方法是正确的选择。
经历这样一个 detach 又 attach 的过程,ListView 中所有的子 View 又都可以显示出来,第二次 Layout 结束。
也就是说,ListView 的第二次 Layout 过程中,把 ListView 中所有子 View 缓存到 RecycleBin 中的 mActivieViews,然后再 detach 掉 ListView 中所有的子 View,接着又 attach 回来(这时使用的是 mActivieViews 中的缓存,没有重新 Inflate),然后再把 mActivieView 缓存到 mScrapViews 中(getActiveView() 中获取到缓存 View 后,就会把它移除,也就是说 mActiveViews 中的同一个缓存不能获得两次)。
滑动加载更多数据 经过两次 Layout ,虽然我们在 ListView 中已经可以看到内容了,然而关于 ListView 最神奇的部分我们却还没有接触到,因为目前 ListView 中只是加载并显示了第一屏的数据而已。比如说我们的 Adapter 当中有 1000 条数据,但是第一屏只显示了 10 条,ListView 中也只有10个子 View 而已,那么剩下的 990 是怎样工作并显示到界面上的呢?这就要看一下 ListView 滑动部分的源码了,因为我们是通过手指滑动来显示更多数据的。
由于滑动部分的机制是属于通用型的,即 ListView 和 GridView 都会使用同样的机制,因此这部分代码就肯定是写在 AbsListView当中的了。那么监听触控事件是在 onTouchEvent()
方法当中进行的,我们就来看一下 AbsListView 中的这个方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Override public boolean onTouchEvent (MotionEvent ev) { ...... switch (actionMasked) { ...... case MotionEvent.ACTION_MOVE: { ... trackMotionScroll(deltaY, incrementalDeltaY); break ; } ...... } ...... return true ; }
手指在 ListView 中滑动时,TouchMode 一直等于 TOUCH_MODE_SCROLL 这个值,相应 case 中我们主要要看trackMotionScroll()
,因为滑动过程中的复杂过程都是在这个方法中处理的。
跟进 trackMotionScroll()
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 51 52 53 54 55 / ** * @param incrementalDeltaY 代表滑动方向,大于 0 代表用户向上滑动,反之则反之 */ boolean trackMotionScroll (int deltaY, int incrementalDeltaY) { ... final int headerViewsCount = getHeaderViewsCount(); final int footerViewsStart = mItemCount - getFooterViewsCount(); int start = 0 ; int count = 0 ; if (down) { final int top = listPadding.top - incrementalDeltaY; for (int i = 0 ; i < childCount; i++) { final View child = getChildAt(i); if (child.getBottom() >= top) { break ; } else { count++; int position = firstPosition + i; if (position >= headerViewsCount && position < footerViewsStart) { mRecycler.addScrapView(child); } } } } else { final int bottom = getHeight() - listPadding.bottom - incrementalDeltaY; for (int i = childCount - 1 ; i >= 0 ; i--) { final View child = getChildAt(i); if (child.getTop() <= bottom) { break ; } else { start = i; count++; int position = firstPosition + i; if (position >= headerViewsCount && position < footerViewsStart) { mRecycler.addScrapView(child); } } } } ... if (count > 0 ) { detachViewsFromParent(start, count); } offsetChildrenTopAndBottom(incrementalDeltaY); ... if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) { fillGap(down); } return false ; }
可以看到,从第 10 行开始,当 ListView 向下滑动的时候,就会进入一个 for 循环当中,从上往下依次获取子 View,第 29 行当中,如果该子 View 的 bottom 值已经小于 top 值了,就说明这个子 View 已经移出屏幕了,所以会调用 RecycleBin 的addScrapView()
方法将这个 View 加入到废弃缓存当中,并将 count 计数器加1,计数器用于记录有多少个子 View 被移出了屏幕。那么如果是 ListView 向上滑动的话,其实过程是基本相同的,只不过变成了从下往上依次获取子 View,然后判断该子 View的 top 值是不是大于 bottom 值了,如果大于的话说明子 View 已经移出了屏幕,同样把它加入到废弃缓存中,并将计数器加1。
在 44 行调用detachViewsFromParent()
,会把所有移出屏幕的子 View 全部 detach 掉 ,在 ListView 的概念中,所有看不到的 View 就没有必要为它进行保存,因为屏幕外还有成百上千条数据等着显示呢,一个好的回收策略才能保证 ListView 的高性能和高效率。
紧接着调用了offsetChildrenTopAndBottom()
方法,并将 incrementalDeltaY 作为参数传入,这个方法的作用是让 ListView 中所有的子 View 都按照传入的参数值进行相应的偏移,这样就实现了随着手指的拖动 ,ListView 的内容也会随着滚动的效果 。
第 51 行调用了 fillGap()
,用来加载屏幕外的数据。
fillGap()
是一个抽象类,因此我们到 ListView 中找到这个类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void fillGap (boolean down) { final int count = getChildCount(); if (down) { final int startOffset = count > 0 ? getChildAt(count - 1 ).getBottom() + mDividerHeight : getListPaddingTop(); fillDown(mFirstPosition + count, startOffset); correctTooHigh(getChildCount()); } else { final int startOffset = count > 0 ? getChildAt(0 ).getTop() - mDividerHeight : getHeight() - getListPaddingBottom(); fillUp(mFirstPosition - 1 , startOffset); correctTooLow(getChildCount()); } }
down 参数用于表示 ListView 是向下滑动还是向上滑动的,可以看到,如果是向下滑动的话就会调用 fillDown()
方法,而如果是向上滑动的话就会调用 fillUp()
方法。那么这两个方法我们都已经非常熟悉了,内部都是通过一个循环来去对 ListView 进行填充,所以这两个方法我们就不看了,但是填充 ListView 会通过调用 makeAndAddView()
方法来完成,又是makeAndAddView()
方法,但这次的逻辑再次不同了,所以我们还是回到这个方法瞧一瞧:
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 / ** * 获取一个View并添加进ListView。这个View呢可能是新创建的,也有可能是来自mActiveViews,或者是来自mScrapViews * @param position * 列表中的逻辑位置 * @param y * 被添加View的上 边位置或者下 边位置 * @param flow * 如果flow是true ,那么y是View的上 边位置,否则那么y是View的下 边位置 * @param childrenLeft Left edge where children should be positioned * @param selected Is this position selected? * @return * 返回被添加的View */ 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; }
不管怎么说,这里首先仍然是会尝试调用 RecycleBin 的 getActiveView()
方法来获取子布局,只不过肯定是获取不到的了,因为在第二次 Layout 过程中我们已经从 mActiveViews 中获取过了数据,而根据 RecycleBin 的机制,mActiveViews 是不能够重复利用的,因此这里返回的值肯定是 null。
既然 getActiveView()
方法返回的值是 null,那么就还是会走到第 29 行的 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 View obtainView (int position, boolean [] isScrap) { isScrap[0 ] = false ; View scrapView; scrapView = mRecycler.getScrapView(position); View child; if (scrapView != null ) { child = mAdapter.getView(position, scrapView, this ); if (child != scrapView) { mRecycler.addScrapView(scrapView); if (mCacheColorHint != 0 ) { child.setDrawingCacheBackgroundColor(mCacheColorHint); } } else { isScrap[0 ] = true ; dispatchFinishTemporaryDetach(child); } } else { child = mAdapter.getView(position, null , this ); if (mCacheColorHint != 0 ) { child.setDrawingCacheBackgroundColor(mCacheColorHint); } } return child; }
在 1 处会调用 RecyleBin 的 getScrapView()
方法来尝试从废弃缓存中获取一个 View,那么废弃缓存有没有 View 呢?
这里要分两种情况:
屏幕中有一个 View 的部分被移出屏幕,而有一个新 View 部分进入屏幕 。此时废弃缓存为空,因此这个新的 View 是 inflate 出来的。
另一种情况是,有某些 View 已经被移出屏幕,刚才在 trackMotionScroll()
方法中我们就已经看到了,一旦有任何子 View 被移出了屏幕,就会将它加入到废弃缓存 mScrapViews 中,而从 obtainView()
方法中的逻辑来看,一旦有新的数据需要显示到屏幕上,就会尝试从废弃缓存中获取View。所以它们之间就形成了一个生产者和消费者的模式,那么 ListView 神奇的地方也就在这里体现出来了,不管你有任意多条数据需要显示,ListView 中的子 View 其实来来回回就那么几个,移出屏幕的子 View 会很快被移入屏幕的数据重新利用起来,因而不管我们加载多少数据都不会出现 OOM 的情况,甚至内存都不会有所增加。
在 2 处会调用 getView()
,也就是将从 mScrapViews 中获取的 View 传入 Adapter 的 getView()
中:
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 public class ListViewAdapter extends BaseAdapter { @Override public View getView (int position, View convertView, ViewGroup parent) { if (convertView == null ){ viewHolder = new ViewHolder(); convertView = inflater.inflate(R.layout.list_view_item_simple, null ); viewHolder.mTextView=(TextView) convertView.findViewById(R.id.text_view); convertView.setTag(viewHolder); Log.d(TAG,"convertView == null" ); }else { Log.d(TAG,"convertView != null" ); viewHolder = (ViewHolder) convertView.getTag(); } viewHolder.mTextView.setText(mList.get(position)); return convertView; } static class ViewHolder { private TextView mTextView; } }
难怪平时我们在写 getView()
方法是要判断一下 convertView 是不是等于 null,如果等于 null 才调用 inflate()
方法来加载布局,不等于 null 就可以直接利用 convertView,因为 convertView 就是我们之间利用过的 View,只不过被移出屏幕后进入到了废弃缓存中,现在又重新拿出来使用而已。然后我们只需要把 convertView 中的数据更新成当前位置上应该显示的数据,那么看起来就好像是全新加载出来的一个布局一样。