最初的分析文档为word,该文档是直接从word文档发布,布局未做详细调整,凑合看吧。
所用源码版本为最新的Android 4.4.2(API 19)。更新中……
ListView结构关系
首先理清listview的层级关系,
使用Google Online Draw 画出继承关系图如下:
图中单独画出Scrollview是为了说明该ViewGroup并没有自带回收机制,如果要是Scrollview显示大量view,需要手动做处理。
重要的类有三个:Listview、AbsListView、AdapterView。各个类的大小如下:
- Listview 3800
- AbsListView 6920
- AdapterView 1208
从Listview开始, ListView的初始化ListVIew.onLayout过程与普通视图的layout过程不同,流程图如下。从左向右,从上向下。
视图的创建过程的都会执行的三个步骤: onMeasure, onLayout, onDraw
图中可以看出重要的类有三个:Listview、AbsListView、AdapterView。主要的回收类RecycleBin位于AbsListView中。
RecycleBin类解析
位于AbsListView中,6466-6900行。
AbsListView的源码中可以看到有个RecycleBin 对象mRecycler。(317行, The data set used to store unused views that should be reused during the next layout to avoid creating new ones. 用于存储不用的view,以便在下个layout中使用来避免创建新的。)注释说明:
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.
大意是使用两级view来进行回收:
ActiveView
:激活view,当期显示在屏幕上的激活的view。
ScrapView
:废弃view,被删除的ActiveView会被自动加入ScrapView。
然后看看RecycleBin内部的重要的的变量和方法:
2.1 RecycleBin变量
mRecyclerListener
: 当发生View回收时,mRecyclerListener若有注册,则会通知给注册者.RecyclerListener接口只有一个函数onMovedToScrapHeap,指明某个view被回收到了scrap heap. 该view不再被显示,任何相关的昂贵资源应该被丢弃。该函数是处理回收时view中的资源释放。
mFirstActivePosition
:The position of the first view stored in mActiveViews.存储在mActiveViews中的第一个view的位置,即getFirstVisiblePosition。
mActiveViews
: 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,这个数组会在布局开始时填充,布局结束后所有view被移至mScrapViews。
mScrapViews
:ArrayList<View>[] Unsorted views that can be used by the adapter as a convert view.可以被适配器用作convert view的无序view数组。 这个ArrayList就是adapter中getView方法中的参数convertView的来源。注意:这里是一个数组,因为如果adapter中数据有多种类型,那么就会有多个ScrapViews。
mViewTypeCount
:view类型总数,列表中可能有多种数据类型,比如内容数据和分割符。
mCurrentScrap
:跟mScrapViews的却别是,mScrapViews是个队列数组,ArrayList<View>[]类型,数组长度为mViewTypeCount,而默认ViewTypeCount = 1的情况下mCurrentScrap=mScrapViews[0]。
下面三个参数分别对应addScrapView中scrapHasTransientState的三个情况
- mTransientStateViews: If the data hasn't changed, we can reuse the views at their old positions.
- mTransientStateViewsById: If the adapter has stable IDs,we can reuse the view forthe same data.
- mSkippedScrap:Otherwise, we'll have to remove the view and start over.
2.2 RecycleBin方法
markChildrenDirty
():为每个子类调用forceLayout()。将mScrapView中回收回来的View设置一样标志,在下次被复用到ListView中时,告诉viewroot重新layout该view。forceLayout()方法只是设置标志,并不会通知其parent来重新layout。
shouldRecycleViewType
():判断给定的view的viewType指明是否可以回收回。viewType < 0可以回收。指定忽略的( ITEM_VIEW_TYPE_IGNORE = -1),或者是 HeaderView / (FootViewITEM_VIEW_TYPE_HEADER_OR_FOOTER = -2)是不被回收的。如有特殊需要可以将自己定义的viewType设置为-1,否则,将会浪费内存,导致OOM。
clear
() :Clears the scrap heap.清空废弃view堆,并将这些View从窗口中Detach。
fillActiveViews
(int childCount, int firstActivePosition):Fill ActiveViews with all of the children of the AbsListView. childCount:The minimum number of views mActiveViews should hold. firstActivePosition:The position of the first view that will be stored in mActiveViews.用AbsListView.的所有子view填充ActiveViews,其中childCount是mActiveViews应该保存的最少的view数,firstActivePosition是mActiveViews中存储的首个view的位置。从代码看该方法的处理逻辑为将当前AbsListView的0-childCount个子类中的非header、footer类添加到mActiveViews数组中。当Adapter中的数据个数未发生变化时,此时用户可能只是滚动,或点击等操作,ListView中item的个数会发生变化,因此,需要将可视的item加入到mActiveView中来管理。
getActiveView
(int position):Get the view corresponding to the specified position. The view will be removed from mActiveViews if it is found. 获取mActiveViews中指定位置的view,如果找到会将该view从mActiveViews中移除。position是adapter中的绝对下标值,mFirstActivePosition前面说过了,是当前可视区域的下标值,对应在adapter中的绝对值,如果找到,则返回找到的View,并将mActiveView对应的位置设置为null。
clearTransientStateViews()
:Dump any currently saved views with transient state.清掉当前处于transient(转换)状态的所有保存的view。内部为mTransientStateViews和mTransientStateViewsById的clear()调用。
addScrapView
(View scrap, int position):将view放入scrapview list中。If the list data hasn't changed or the adapter has stable IDs, views with transient state will be preserved for later retrieval. scrap:要添加的view。Position:view在父类中的位置。放入时位置赋给scrappedFromPosition 。有transient状态的view不会被scrap(废弃),会被加入mSkippedScrap。
就是将移出可视区域的view,设置它的scrappedFromPosition,然后从窗口中detach该view,并根据viewType加入到mScrapView中。
该方法会调用mRecyclerListener 接口的函数onMovedToScrapHeap(6734)
if (mRecyclerListener != null) { mRecyclerListener.onMovedToScrapHeap(scrap); } |
mRecyclerListener的设置可通过AbsListView的setRecyclerListener方法。
当view被回收准备再利用的时候设置要通知的监听器, 可以用来释放跟view有关的资源。这点似乎很有用。
public void setRecyclerListener(RecyclerListener listener) { mRecycler.mRecyclerListener = listener; } |
getScrapView
(int position) :A view from the ScrapViews collection. These are unordered.该方法中调用了retrieveFromScrap(ArrayList<View> scrapViews, int position)。
retrieveFromScrap
(ArrayList<View> scrapViews, int position):无注释。(6902)该方法属于AbsListView。
int size = scrapViews.size(); if (size > 0) { // See if we still have a view for this position. for (int i=0; i<size; i++) { View view = scrapViews.get(i); if (((AbsListView.LayoutParams)view.getLayoutParams()) .scrappedFromPosition == position) { scrapViews.remove(i); return view; } } return scrapViews.remove(size - 1); } else { return null; } |
其中scrappedFromPosition :The position the view was removed from when pulled out of the scrap heap.(6412)根据position,从mScrapView中找:
1. 如果有view.scrappedFromPosition = position的,直接返回该view;
2. 否则返回mScrapView中最后一个;
3. 如果缓存中没有view,则返回null;
a. 第三种情况,这个最简单:
一开始,listview稳定后,显示N个,此时mScrapView中是没有缓存view的,当我们向上滚动一小段距离(第一个此时仍显示部分),新的view将会显示,此时listview会调用Adapter.getView,但是缓存中没有,因此convertView是null,所以,我们得分配一块内存来创建新的convertView;
b. 第二种情况:
在a中,我们继续向上滚动,直接第一个view完全移出屏幕(假设没有新的item),此时,第一个view就会被detach,并被加入到mScrapView中;然后,我们还继续向上滚动,直接后面又将要显示新的item view时,此时,系统会从mScrapView中找position对应的View,显然,是找不到的,则将从mScrapView中,取最后一个缓存的view传递给convertView;
c. 第一种情况:
紧接着在b中,第一个被完全移出,加入到mScrapView中,且没有新增的item到listview中,此时,缓存中就只有第一个view;然后,我此时向下滑动,则之前的第一个item,将被显示出来,此时,从缓存中查找position对应的view有没有,当然,肯定是找到了,就直接返回了。
removeSkippedScrap()
:Finish the removal of any views that skipped the scrap heap.清空mSkippedScrap。
scrapActiveViews
():Move all views remaining in mActiveViews to mScrapViews.将mActiveViews 中剩余的view放入mScrapViews。实际上就是将mActiveView中未使用的view回收(因为,此时已经移出可视区域了)。会调用mRecyclerListener.onMovedToScrapHeap(scrap);
pruneScrapViews
():确保mScrapViews 的数目不会超过mActiveViews的数目 (This can happen if an adapter does not recycle its views)。mScrapView中每个ScrapView数组大小不应该超过mActiveView的大小,如果超过,系统认为程序并没有复用convertView,而是每次都是创建一个新的view,为了避免产生大量的闲置内存且增加OOM的风险,系统会在每次回收后,去检查一下,将超过的部分释放掉,节约内存降低OOM风险。
reclaimScrapViews
(List<View> views):Puts all views in the scrap heap into the supplied list.将mScrapView中所有的缓存view全部添加到指定的view list中,只看到有AbsListView.reclaimViews有调用到,但没有其它方法使用这个函数,可能在特殊情况下会使用到,但目前从framework中,看不出来。
setCacheColorHint
(int color):Updates the cache color hint of all known views.更新view的缓存颜色提示setDrawingCacheBackgroundColor。为所有的view绘置它们的背景色。
RecycleBin的调用和关键方法
3.1 ListView
3.1.1 layoutChildren
1479-1729
1583-当数据发生改变的时候,把当前的view放到scrapviews里面,否则标记为activeViews。
// 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();//移除所有old views ...... // Flush any cached views that did not get reused above recycleBin.scrapActiveViews();//刷新缓存,将当前的ActiveVies 移动到 ScrapViews。 |
dataChanged,从单词的意思我们就可以,这里的优化规则就是基于数据是否有变化,mDataChanged在makeAndAddView(见下文)中有使用。
step1:如果数据发生变化,就将所有view加入到mScrapView中,否则,将所有view放到mActiveView中;
step2:添加view到listview中;
step3:回收mActiveView中的未使用的view到mScrapView中;
注:在step1中,如果是addScrapView,则所有的view将会detach,如果是fillActiveViews,则不会detach,只有在step3中,未用到的view才会detach。
3.1.2 makeAndAddView
(int position, int y, boolean flow, int childrenLeft,boolean selected)(1772)
Obtain the view and add it to our list of children. The view can be made fresh, converted from an unused view, or used as is if it was in the recycle bin.
View child; if (!mDataChanged) {// 数据没有更新时,使用以前的view // 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 // 对复用的View针对当前需要进行配置。定位并且添加这个view到ViewGrop中的children列表,从回收站获取的视图不需要measure,所以最后一个参数为true 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; |
3.1.3 setupChild
(View child, int position, int y, boolean flowDown, int childrenLeft, boolean selected, boolean recycled)(1812)
Add a view as a child and make sure it is measured (if necessary) and positioned properly.
3.1.4 setAdapter
(ListAdapter adapter) (457)
Sets the data behind this ListView. The adapter passed to this method may be wrapped by a WrapperListAdapter, depending on the ListView features currently in use. For instance, adding headers and/or footers will cause the adapter to be wrapped.
if (mAdapter != null && mDataSetObserver != null) { mAdapter.unregisterDataSetObserver(mDataSetObserver);//移除了与当前listview的adapter绑定数据集观察者DataSetObserver } resetList();//重置listview,主要是清除所有的view,改变header、footer的状态 mRecycler.clear();//清除掉RecycleBin对象mRecycler中所有缓存的view,RecycleBin后面着重介绍,主要是关系到Listview中item的重用机制,它是AbsListview的一个内部类 if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {//判断是否有headerview和footview mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter); } else { mAdapter = adapter; } mOldSelectedPosition = INVALID_POSITION; mOldSelectedRowId = INVALID_ROW_ID; // AbsListView#setAdapter will update choice mode states. super.setAdapter(adapter); if (mAdapter != null) { mAreAllItemsSelectable = mAdapter.areAllItemsEnabled(); mOldItemCount = mItemCount; mItemCount = mAdapter.getCount(); checkFocus(); mDataSetObserver = new AdapterDataSetObserver();//注册headerview的观察者 mAdapter.registerDataSetObserver(mDataSetObserver);//在RecycleBin对象mRecycler记录下item类型的数量 mRecycler.setViewTypeCount(mAdapter.getViewTypeCount()); int position; if (mStackFromBottom) { position = lookForSelectablePosition(mItemCount - 1, false); } else { position = lookForSelectablePosition(0, true); } setSelectedPositionInt(position);//AdapterView中的方法,记录当前的position setNextSelectedPositionInt(position);//AdapterView中的方法,记录下一个position if (mItemCount == 0) { // Nothing selected checkSelectionChanged(); } } else { mAreAllItemsSelectable = true; checkFocus(); // Nothing selected checkSelectionChanged(); } requestLayout(); |
3.1.5 scrollListItemsBy
(int amount)(3012-3082)
对子view滑动一定距离,添加view到底部或者移除顶部的不可见view。从注释看,不可见的item 的自动移除是在scrollListItemsBy中进行的。
private void scrollListItemsBy(int amount) { offsetChildrenTopAndBottom(amount); final int listBottom = getHeight() - mListPadding.bottom;//获取listview最底部位置 final int listTop = mListPadding.top; //获取listview最顶部位置 final AbsListView.RecycleBin recycleBin = mRecycler; if (amount < 0) { // shifted items up // may need to pan views into the bottom space int numChildren = getChildCount(); View last = getChildAt(numChildren - 1); while (last.getBottom() < listBottom) {//最后的view高于底部时添加下一个view final int lastVisiblePosition = mFirstPosition + numChildren - 1; if (lastVisiblePosition < mItemCount - 1) { last = addViewBelow(last, lastVisiblePosition); numChildren++; } else { break; } } // may have brought in the last child of the list that is skinnier // than the fading edge, thereby leaving space at the end. need // to shift back if (last.getBottom() < listBottom) {//到达最后一个view offsetChildrenTopAndBottom(listBottom - last.getBottom()); } // top views may be panned off screen View first = getChildAt(0); while (first.getBottom() < listTop) {//顶部view移除屏幕时 AbsListView.LayoutParams layoutParams = (LayoutParams) first.getLayoutParams(); if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) { recycleBin.addScrapView(first, mFirstPosition); //回收view } detachViewFromParent(first); //从父类中移除 first = getChildAt(0); //这行好像没用啊。。。。 mFirstPosition++; } } else { // shifted items down View first = getChildAt(0); // may need to pan views into top while ((first.getTop() > listTop) && (mFirstPosition > 0)) {//顶部view上部有空间时添加view。 first = addViewAbove(first, mFirstPosition); mFirstPosition--; } // may have brought the very first child of the list in too far and // need to shift it back if (first.getTop() > listTop) {//到达第一个view offsetChildrenTopAndBottom(listTop - first.getTop()); } int lastIndex = getChildCount() - 1; View last = getChildAt(lastIndex); // bottom view may be panned off screen while (last.getTop() > listBottom) {//底部view移除屏幕的情况 AbsListView.LayoutParams layoutParams = (LayoutParams) last.getLayoutParams(); if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) { recycleBin.addScrapView(last, mFirstPosition+lastIndex); } detachViewFromParent(last); last = getChildAt(--lastIndex); } } } |
从以上代码可以看出,Android中view回收的计算是其父view中不再显示的,如果scrollview中包含了一个wrap_content属性的listview,里面的内容并不会有任何回收,引起listview 的getheight函数获取的是一个足以显示所有内容的高度。
3.2 AbsListView
3.2.1 obtainView
(int position, boolean[] isScrap)(2227)
Get a view and have it show the data associated with the specified position. 当这个方法被调用时,说明Recycle bin中的view已经不可用了,那么,现在唯一的方法就是,convert一个老的view,或者构造一个新的view。
position: 要显示的位置
isScrap: 是个boolean数组, 如果view从scrap heap获取,isScrap [0]为true,否则为false。
isScrap[0] = false; View scrapView; scrapView = mRecycler.getTransientStateView(position); if (scrapView == null) { // 查看回收站中是否有废弃无用的View,如果有,则使用它,无需New View。 scrapView = mRecycler.getScrapView(position); } View child; if (scrapView != null) { //此时说明可以从回收站中重新使用scrapView。 child = mAdapter.getView(position, scrapView, this); if(child.getImportantForAccessibility()== IMPORTANT_FOR_ACCESSIBILITY_AUTO) { child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); } if (child != scrapView) { //如果重用的scrapView和adapter获得的view是不一样的,将scrapView进行回收 mRecycler.addScrapView(scrapView, position);// scrapView 仍然放入回收站 if (mCacheColorHint != 0) { child.setDrawingCacheBackgroundColor(mCacheColorHint); } } else { //如果重用的view和adapter获得的view是一样的,将isScrap[0]值为true,否则默认为false isScrap[0] = true; // Clear any system-managed transient state so that we can // recycle this view and bind it to different data. if (child.isAccessibilityFocused()) { child.clearAccessibilityFocus(); } child.dispatchFinishTemporaryDetach(); } }else {//回收站中没有拿到数据,就只能够自己去inflate一个xml布局文件,或者new一个view child = mAdapter.getView(position, null, this); //当getview中传入的 converView=null的时候会在getView的方法中进行新建这个view if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); } if (mCacheColorHint != 0) { child.setDrawingCacheBackgroundColor(mCacheColorHint); } } |
3.2.2 trackMotionScroll
(int deltaY, int incrementalDeltaY)(4991)
监视滑动动作
deltaY: Amount to offset mMotionView. This is the accumulated delta since the motion began. 正数表示向下滑动。
incrementalDeltaY :Change in deltaY from the previous event.
....... // 滚动时,不在可见范围内的item放入回收站。。。。。。。 if (down) { int top = -incrementalDeltaY; if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { top += listPadding.top; } 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) { // The view will be rebound to new data, clear any // system-managed transient state. if (child.isAccessibilityFocused()) { child.clearAccessibilityFocus(); } mRecycler.addScrapView(child, position);//放入回收站 } } } } else { int bottom = getHeight() - incrementalDeltaY; if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { bottom -= listPadding.bottom; } 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) { // The view will be rebound to new data, clear any // system-managed transient state. if (child.isAccessibilityFocused()) { child.clearAccessibilityFocus(); } mRecycler.addScrapView(child, position);//放入回收站 } } } } |
4. 用到的关键类
4.1 ViewType的使用
在listview中当有多种viewtype的时候,在adapter中继承设置getItemViewType方法可以更有效率 。示例如下:
....... private static final int TYPE_ITEM = 0; private static final int TYPE_SEPARATOR = 1; private static final int TYPE_MAX_COUNT = TYPE_SEPARATOR + 1; @Override public int getItemViewType(int position) { return mSeparatorsSet.contains(position) ? TYPE_SEPARATOR : TYPE_ITEM; } @Override public int getViewTypeCount() { return TYPE_MAX_COUNT; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder = null; int type = getItemViewType(position); if (convertView == null) { holder = new ViewHolder(); switch (type) { case TYPE_ITEM: convertView = mInflater.inflate(R.layout.item1, null); holder.textView = (TextView)convertView.findViewById......; break; case TYPE_SEPARATOR: convertView = mInflater.inflate(R.layout.item2, null); holder.textView = (TextView)convertView.findViewById......; break; } convertView.setTag(holder); } else { holder = (ViewHolder)convertView.getTag(); } ........ } |
如果实现了RecyclerListener接口,当一个View由于ListView的滑动被系统回收到RecycleBin的mScrapViews数组时,会调用RecyclerListener中的onMovedToScrapHeap(View view)方法。RecycleBin相当于一个临时存储不需要显示的那部分Views的对象,随着列表滑动,这些Views需要显示出来的时候,他们就被从RecycleBin中拿了出来,RecycleBin本身并不对mScrapViews中的对象做回收操作。
于是在工程里,为ListView添加RecyclerListener接口,并在onMovedToScrapHeap方法中释放ListItem包含的Bitmap资源,这样可以极大的减少内存占用。
4.2 TransientStateView
用来标记这个view的瞬时状态,用来告诉app无需关心其保存和恢复。从注释看,这种具有瞬时状态的view,用于在view动画播放等情况中。