ListView 缓存机制源码浅析
来源:互联网 发布:同和行知职业技术学校 编辑:程序博客网 时间:2024/06/08 07:33
ListView的缓存机制是通过RecycleBin实现对View的缓存实现的。缓存机制大大的提高了View的复用率,这也是为什么ListView可以加载大量的Item也不会OOM的原因。当然了,为了进一步提高性能我们都会自定义一个ViewHolder来避免不必要的findViewById操作,这也是RecycleView与ListView的区别之一<RecycleView缓存的是ViewHolder>。RecycleBin是ListView/GridView的父类AbsListView的内部类,这说明ListView/GridView的缓存机制大致是相同的。
一、概览
当item view 在屏幕之外,RecycleBin将其缓存起来,当需要新的itemView,除非RecycleBin中没有缓存,否则就直接从缓存中取,还记得在getView中有一个参数convertView就是从RecycleBin中取出来的。
二、RecycleBin
由上图我们可以看大,RecycleBin主要通过两个集合:View[] mActiveViews和ArrayList<View>[] mScrapViews来实现的。
下面看一下RecycleBin中需要重点关注的部分:
private View[] mActiveViews:缓存当前屏幕可以看到的item view;这个数组是为了防止多次layout带来的性能即数据重复问题而存在的。因为多次layout会导致ListView重复多次加载当前的屏幕显示的item views,为了避免数据重复以及多次实例化item views带来的资源浪费,有必要将其缓存起来。
private int mViewTypeCount:item view的种类;
private ArrayList<View>[] mScrapViews:注意到这是一个ArrayList<View>数组,之所以是一个数组,是因为item view的种类可能大于一,而mScrapViews:就是缓存每种item View的。还记得BaseAdapter里面有一个getViewTypeCount()方法吗?这个方法的返回值就是mViewTypeCount。
private ArrayList<View> mCurrentScrap:当前种类的item view的缓存。
public void setViewTypeCount(int viewTypeCount):设置列表item的种类;
void fillActiveViews(int childCount, int firstActivePosition):将当前屏幕可以看到的item view添加到mActiveViews中;
View getActiveView(int position):根据position获取mActiveViews中的item view;
void addScrapView(View scrap, int position):将废弃的(通常是滚出屏幕外的)item view缓存起来;简单的看一下addSrapView的源码:
void addScrapView(View scrap, int position){ ....... if (mViewTypeCount == 1) { mCurrentScrap.add(scrap); } else { mScrapViews[viewType].add(scrap); } if (mRecyclerListener != null) { mRecyclerListener.onMovedToScrapHeap(scrap); } }
这验证了我们对mScrapViews的分析;
View getScrapView(int position):根据position的值获取一个废弃的item view用于重用;
三、详细缓存流程
ListView是ViewGroup的子类,遵循自定义View的基本步骤,measure、layout、draw。缓存部分我们重点分析layout过程。下面看一下ListView的layoutChildren部分源码:
// Pull all children into the RecycleBin. // These views will be reused if possible final int firstPosition = mFirstPosition; final RecycleBin recycleBin = mRecycler; if (dataChanged) { for (int i = 0; i < childCount; i++) { recycleBin.addScrapView(getChildAt(i), firstPosition+i); } } else { recycleBin.fillActiveViews(childCount, firstPosition); } // Clear out old views detachAllViewsFromParent(); recycleBin.removeSkippedScrap(); switch (mLayoutMode) { case LAYOUT_SET_SELECTION: if (newSel != null) { sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom); } else { sel = fillFromMiddle(childrenTop, childrenBottom); } break; case LAYOUT_SYNC: sel = fillSpecific(mSyncPosition, mSpecificTop); break; case LAYOUT_FORCE_BOTTOM: sel = fillUp(mItemCount - 1, childrenBottom); adjustViewsUpOrDown(); break; case LAYOUT_FORCE_TOP: mFirstPosition = 0; sel = fillFromTop(childrenTop); adjustViewsUpOrDown(); break; case LAYOUT_SPECIFIC: sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop); break; case LAYOUT_MOVE_SELECTION: sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom); break; 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); } } 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; } // Flush any cached views that did not get reused above recycleBin.scrapActiveViews();第一次layout的时候,由于数据还没有显示,仍然有Adapter管理,并没有填充到ListView中,因此,childCount等于0。datasetchanged只有在数据源发生变化的情况下才会是true,其他的都是false。因此,会执行recycleBin.fillActiveViews(childCount, firstPosition);,但是此时childCount等于0,因此并不会向mActiveViews添加任何东西。
继续向下看,mLayoutMode代表了布局模式,默认情况下是LAYOUT_NORMAL,因此会走default语句中。因为childCount是0,并且默认的顺序从上到下的,因此会执行fillFromTop -- 执行该函数就可以将Adapter中的数据填充到ListView中了,填充完成之后,childCount就大于0了。下面看一下fillFromTop的源码:
private View fillFromTop(int nextTop) { mFirstPosition = Math.min(mFirstPosition, mSelectedPosition); mFirstPosition = Math.min(mFirstPosition, mItemCount - 1); if (mFirstPosition < 0) { mFirstPosition = 0; } return fillDown(mFirstPosition, nextTop); }可以看到fillFromTop调用了fillDown方法。
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) { // is this the selected item? 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; }可以看到View的产生是通过makeAndAddView进行的。
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) { View child; if (!mDataChanged) { // Try to use an existing view for this position child = mRecycler.getActiveView(position); if (child != null) { // Found it -- we're using an existing child // This just needs to be positioned setupChild(child, position, y, flow, childrenLeft, selected, true); return child; } } // Make a new view for this position, or convert an unused view if possible child = obtainView(position, mIsScrap); // This needs to be positioned and measured setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); return child; }由于mRecycler.getActiveView返回null,因此,最终makeAndAddView又调用了obtainView生成View的。下面看一下obtainView的部分源码:
...... final View scrapView = mRecycler.getScrapView(position); final View child = mAdapter.getView(position, scrapView, this); if (scrapView != null) { if (child != scrapView) { // Failed to re-bind the data, return scrap to the heap. mRecycler.addScrapView(scrapView, position); } else { isScrap[0] = true; // Finish the temporary detach started in addScrapView(). child.dispatchFinishTemporaryDetach(); } }......return child;看到这一句是不是很熟悉,final View child = mAdapter.getView(position, scrapView, this);没错,这就是我们自定义adapter的时候,重写的getView方法。由于第一次layout的时候,mRecycleView还没有缓存,因此scrapView是null,也就是说此时我们填充ListView都是通过getView产生的。
下面看一下第二次layout的时候,会发生什么
// Pull all children into the RecycleBin. // These views will be reused if possible final int firstPosition = mFirstPosition; final RecycleBin recycleBin = mRecycler; if (dataChanged) { for (int i = 0; i < childCount; i++) { recycleBin.addScrapView(getChildAt(i), firstPosition+i); } } else { recycleBin.fillActiveViews(childCount, firstPosition); } // Clear out old views detachAllViewsFromParent(); recycleBin.removeSkippedScrap(); switch (mLayoutMode) { case LAYOUT_SET_SELECTION: if (newSel != null) { sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom); } else { sel = fillFromMiddle(childrenTop, childrenBottom); } break; case LAYOUT_SYNC: sel = fillSpecific(mSyncPosition, mSpecificTop); break; case LAYOUT_FORCE_BOTTOM: sel = fillUp(mItemCount - 1, childrenBottom); adjustViewsUpOrDown(); break; case LAYOUT_FORCE_TOP: mFirstPosition = 0; sel = fillFromTop(childrenTop); adjustViewsUpOrDown(); break; case LAYOUT_SPECIFIC: sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop); break; case LAYOUT_MOVE_SELECTION: sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom); break; 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); } } 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; } // Flush any cached views that did not get reused above recycleBin.scrapActiveViews();第二次layout开始,会执行我们所说的fillActiveViews过程,因此此时childCount不再是0,所以当前屏幕显示的item view会被缓存到mActiveViews中。紧接着执行了detachAllViewsFromParent()方法,该方法清空了所有的view,这是为了避免向下执行填充之后出现数据重复。之后的执行逻辑与前面分析的相似了,mActiveViews中缓存的View会在makeAndAddView中使用。
滑动过程中,会产生大量的Item View,RecycleBin也发挥了重要的作用。这也是ListView不会OOM的原因。
- ListView 缓存机制源码浅析
- Glide缓存机制源码浅析
- ListView源码分析缓存机制
- <MyBatis缓存机制>一级缓存源码浅析
- Android 源码 listview 重用机制 浅析
- Android ListView 与 RecyclerView 对比浅析--缓存机制
- Android ListView 与 RecyclerView 对比浅析--缓存机制
- Android ListView 与 RecyclerView 对比浅析--缓存机制
- Android ListView与RecyclerView对比浅析--缓存机制
- Android ListView与RecyclerView对比浅析--缓存机制
- Android ListView 与 RecyclerView 对比浅析--缓存机制
- Android ListView与RecyclerView对比浅析--缓存机制
- IE缓存机制浅析
- Browser缓存机制浅析
- 浅析hibernate缓存机制
- iOS 缓存机制浅析
- 浏览器缓存机制浅析
- 浏览器缓存机制浅析
- Python学习之初始(四)
- POJ2299-Ultra-QuickSort(树状数组+离散化)
- LeetCode笔记:(Java) 9. Palindrome Number
- java中String、StringBuffer、StringBuilder的区别
- Android 手机卫士(4) 包名/版本号/版本名的获取
- ListView 缓存机制源码浅析
- HDU-5493---Queue (线段树)
- Binary Tree Tilt
- 20170914
- 高阶函数
- 卡特兰数(Catalan)
- MFC picture 控件error RC2108: expected numerical dialog constant
- 为何要用番茄工作法(笔记)
- 03 Hiernate常见错误