本文是阅读 http://www.cnblogs.com/qiengo/p/3628235.html http://blog.csdn.net/iisPRing/article/details/50967445 http://blog.csdn.net/guolin_blog/article/details/44996879 http://www.jianshu.com/p/9c603a11b0c9 https://my.oschina.net/lorcan/blog/539215 的读书笔记 代码是 android N 的,记录下方便记忆。
ListView 继承结构 如下图
AbsListView 的子类(ListView,GridView)都有RecyleBin机制 可以 防止 OOM,先来看看 RecyleBin,接着看 ListView 的原理(ListView 最终集成自 View,View的子类 主要看 onMeasure()、onLayout(), onDraw())
RecyleBin是view的回收站。
/** * 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,以便在下个layout中使用来避免创建新的 * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener) * @see android.widget.AbsListView.RecyclerListener */ class RecycleBin { private RecyclerListener mRecyclerListener; /** * The position of the first view stored in mActiveViews. */ private 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. */ private View[] mActiveViews = new View[0]; /** * Unsorted views that can be used by the adapter as a convert view. */ private ArrayList<View>[] mScrapViews; ... }ListView中有很多view,屏幕上能看到的view是onScreenView,是RecyleBin中的ActiveView,滑出屏幕的view是OffScreen的View,是RecyleBin中的ScrapView,scrap是废弃的意思。
ListView把scrapView全部删除,就不用绘制看不见的view了。ListView会把这些删除的ScrapView放入到RecycleBin中存起来,就像把暂时无用的资源放到回收站一样。
当ListView的底部需要显示新的View的时候,会从RecycleBin中取出一个ScrapView,将其作为convertView参数传递给Adapter的getView方法,从而达到View复用的目的,这样就不必在Adapter的getView方法中执行LayoutInflater.inflate()方法了。
RecyleBin 是防止 ListView 加载数据出现 OOM 的重要原因之一,RecyleBin 是 AbsListView 的内部类,RecycleBin的作用是帮助布局中的View的重用,它存储了两种类型的View:
mActiveViews 存储的是OnScreen的View,这些View很有可能被直接复用mScrapViews 存储的是OffScreen的View,这些View主要是用来间接复用的,这就是传回getView中covertView的来源RecyclerListener只有一个方法onMovedToScrapHeap(),addScrapView() 和 scrapActiveViews() 时,使用了该变量,如果注册了RecyclerListener,就调用onMovedToScrapHeap(),表示该view不再显示,这个view被回收到了scrap heap,该函数处理回收时view中的资源释放。
The position of the first view stored in mActiveViews. 存储在mActiveViews中的第一个view的位置,即AdapterView.getFirstVisiblePosition()返回值。
/** * Returns the position within the adapter's data set for the first item * displayed on screen. * * @return The position within the adapter's data set */ public int getFirstVisiblePosition() { return mFirstPosition; }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。
Unsorted views that can be used by the adapter as a convert view. 这个ArrayList就是adapter中getView方法中的参数convertView的来源。注意:这里是一个数组,因为如果adapter中数据有多种类型,那么就会有多个ScrapViews
view类型总数,列表中可能有多种数据类型,比如内容数据和分割符
private ArrayList mCurrentScrap; 默认情况下,mCurrentScrap = scrapViews[0];
If the data hasn’t changed, we can reuse the views at their old positions.
If the adapter has stable IDs,we can reuse the view forthe same data.
addScrapView()中,如果view不能添加到 mTransientStateViews,mTransientStateViewsById 中,就添加到 mSkippedScrap 中 Otherwise, we’ll have to remove the view and start over.
为每个子类调用forceLayout()。将mScrapView中回收回来的View设置一样标志,在下次被复用到ListView中时,告诉viewroot重新layout该view。forceLayout()方法只是设置标志,并不会通知其parent来重新layout。
public void markChildrenDirty() { if (mViewTypeCount == 1) { final ArrayList<View> 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<View> 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(); } } }判断给定的view的viewType指明是否可以回收回。viewType < 0可以回收。指定忽略的( ITEM_VIEW_TYPE_IGNORE = -1),或者是 HeaderView / FootView(ITEM_VIEW_TYPE_HEADER_OR_FOOTER = -2)是不被回收的。如有特殊需要可以将自己定义的viewType设置为-1,否则,将会浪费内存,导致OOM
public boolean shouldRecycleViewType(int viewType) { return viewType >= 0; }清空废弃view堆,并将这些View从窗口中Detach
/** * Clears the scrap heap. */ void clear() { if (mViewTypeCount == 1) { final ArrayList<View> scrap = mCurrentScrap; clearScrap(scrap); } else { final int typeCount = mViewTypeCount; for (int i = 0; i < typeCount; i++) { final ArrayList<View> scrap = mScrapViews[i]; clearScrap(scrap); } } clearTransientStateViews(); }用AbsListView.的所有子view填充ActiveViews。从代码看该方法的处理逻辑为将当前AbsListView的0-childCount个子类中的非header、footer的view添加到mActiveViews数组中。当Adapter中的数据个数未发生变化时(用户滚动,或点击等操作),ListView中item的个数会发生变化,因此,需要将可视的item加入到mActiveView中来管理
/** * Fill ActiveViews with all of the children of the AbsListView. * * @param childCount The minimum number of views mActiveViews should hold . mActiveViews应该保存的最少的view数 * @param firstActivePosition The position of the first view that will be stored in * mActiveViews. mActiveViews中存储的首个view的位置,第一个可见的view的位置 */ 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; } } }获取mActiveViews中指定位置的view,如果找到会将该view从mActiveViews中移除。position是adapter中的绝对下标值,mFirstActivePosition前面说过了,是当前可视区域的下标值,对应在adapter中的绝对值,如果找到,则返回找到的View,并将mActiveView对应的位置设置为null
/** * Get the view corresponding to the specified position. The view will be removed from * mActiveViews if it is found. * 根据position在mActiveViews中查找view * @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; // 找到了把该view从mActiveViews中移除,mActiveViews中的View不能重复利用 return match; } return null; }清掉当前处于transient(瞬时)状态的所有保存的view。内部为mTransientStateViews和mTransientStateViewsById的clear()调用
/** * Dumps and fully detaches any currently saved views with transient * state. */ void clearTransientStateViews() { final SparseArray<View> viewsByPos = mTransientStateViews; if (viewsByPos != null) { final int N = viewsByPos.size(); for (int i = 0; i < N; i++) { removeDetachedView(viewsByPos.valueAt(i), false); } viewsByPos.clear(); } final LongSparseArray<View> viewsById = mTransientStateViewsById; if (viewsById != null) { final int N = viewsById.size(); for (int i = 0; i < N; i++) { removeDetachedView(viewsById.valueAt(i), false); } viewsById.clear(); } }将view放入scrapview list中,有transient状态的view不会被scrap(废弃),会被加入mTransientStateViewsById,mTransientStateViews 或 调用getSkippedScrap() 加入mSkippedScrap中
/** * Puts a view into the list of scrap views. * <p> * If the list data hasn't changed or the adapter has stable IDs, views * with transient state will be preserved for later retrieval. * 缓存废弃的view,RecycleBin当中使用 mScrapViews 和 mCurrentScrap 这两个List来存储废弃View * @param scrap The view to add * scrap是要添加的view * * @param position The view's position within its parent * position是view在父类中的位置 */ 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; } // 设置它的scrappedFromPosition,然后从窗口中detach该view,并根据viewType加入到mScrapView中 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)) { // 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) { if (mAdapter != null && mAdapterHasStableIds) { // 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) { // 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 { // Otherwise, we'll have to remove the view and start over. getSkippedScrap().add(scrap); } } else { if (mViewTypeCount == 1) { mCurrentScrap.add(scrap); } else { mScrapViews[viewType].add(scrap); } if (mRecyclerListener != null) { // RecyclerListener可以通过AbsListView.setRecyclerListener()设置 mRecyclerListener.onMovedToScrapHeap(scrap); } } }返回null的情况举例: ListView第一次加载完整屏item,此时mScrapView中是没有缓存view的,新的view将会显示,此时listview会调用Adapter.getView,但是缓存中没有,因此convertView是null,所以,我们得分配一块内存来创建新的convertView;
返回scrapViews最后一个view的情况: 接着上边的情况,我们继续向上滚动,第一个view完全移出屏幕(假设还没有加载新的item),此时,第一个view就会被detach,并被加入到mScrapView中;然后,我们还继续向上滚动,直接后面又将要显示新的item view时,此时,系统会从mScrapView中找position对应的View,显然是找不到的,则将从mScrapView中,取最后一个缓存的view传递给convertView
根据position返回view: 接着上边的情况,第一个被完全移出 加入到mScrapView中,假设此时还没有新增的item到listview中,此时缓存中就只有第一个view;然后,现在向下滑动,则之前的第一个item,将被显示出来,此时,从缓存中查找position对应的view有没有,当然,肯定是找到了,就直接返回了
清空 mSkippedScrap
/** * Finish the removal of any views that skipped the scrap heap. */ void removeSkippedScrap() { if (mSkippedScrap == null) { return; } final int count = mSkippedScrap.size(); for (int i = 0; i < count; i++) { removeDetachedView(mSkippedScrap.get(i), false); } mSkippedScrap.clear(); }Move all views remaining in mActiveViews to mScrapViews. 将mActiveView中未使用的view回收(因为,此时已经移出可视区域了)。会调用mRecyclerListener.onMovedToScrapHeap(scrap);回收view的资源
scrapActiveViews()调用了该方法, pruneScrapViews() 确保mScrapViews 的数目不会超过mActiveViews的数目 (This can happen if an adapter does not recycle its views)。 mScrapView中每个ScrapView数组大小不应该超过mActiveView的大小,如果超过,系统认为程序并没有复用convertView,而是每次都是创建一个新的view,为了避免产生大量的闲置内存且增加OOM的风险,系统会在每次回收后,去检查一下,将超过的部分释放掉,节约内存降低OOM风险。
/** * Makes sure that the size of mScrapViews does not exceed the size of * mActiveViews, which can happen if an adapter does not recycle its * views. Removes cached transient state views that no longer have * transient state. */将mScrapView中所有的缓存view全部添加到指定的view list中,只看到有AbsListView.reclaimViews有调用到,但没有其它方法使用这个函数,可能在特殊情况下会使用到,但目前从framework中,看不出来。
/** * Puts all views in the scrap heap into the supplied list. */ void reclaimScrapViews(List<View> views) { if (mViewTypeCount == 1) { views.addAll(mCurrentScrap); } else { final int viewTypeCount = mViewTypeCount; final ArrayList<View>[] scrapViews = mScrapViews; for (int i = 0; i < viewTypeCount; ++i) { final ArrayList<View> scrapPile = scrapViews[i]; views.addAll(scrapPile); } } }Updates the cache color hint of all known views. 更新view的缓存颜色提示setDrawingCacheBackgroundColor。为所有的view绘置它们的背景色。
界面显示一个view,经过三个阶段:onMeasure()->onLayout()->onDraw()
onMeasure()用于测量View的大小,占用的大小通常是整个屏幕onDraw()用于将View绘制到界面上onLayout()用于确定View的布局AdapterView继承自ViewGroup,ViewGroup通过addView()添加View,AdapterView重写了addView()方法,禁用了该方法: android/widget/AdapterView.java中
/** * This method is not supported and throws an UnsupportedOperationException when called. * * @param child Ignored. * * @throws UnsupportedOperationException Every time this method is invoked. */ @Override public void addView(View child) { throw new UnsupportedOperationException("addView(View) is not supported in AdapterView"); }AdapterView把addView方法给禁用了,那么ListView怎么向其中添加child呢?通过onLayout中调用layoutChildren(),AbsListView的主要实现也在 onLayout中,
layoutChildren()关于RecyleBin主要干了3件事:
ListView的children放到RecycleBin中ListView清空childrenRecycleBin中缓存的view复用,变成ListView的children下面来看 onLayout 流程
onLayout() 代码不多,判断 changed,如果ListView的大小或者位置发生了变化,子布局重绘,调用子类的 layoutChildren()
layoutChildren() 调用 fillFromTop() 加载 item 的布局
fillFromTop() 确定 mFirstPosition,调用 fillDown(),没做具体加载 子布局的工作,来看 fillDown()
fillDown用子View从指定的position自上而下填充ListView,,fillUp则是自下而上填充
/** * Fills the list from pos down to the end of the list view. * * @param pos The first position to put in the list * pos 表示列表中第一个要绘制的item的position,其对应着Adapter中的索引 * * @param nextTop The location where the top of the item associated with pos * should be drawn * nextTop表示第一个要绘制的item在ListView中实际的位置, * * @return The view that is currently selected, if it happens to be in the * range that we draw. */ private View fillDown(int pos, int nextTop) { View selectedView = null; int end = (mBottom - mTop); // end是ListView底部减去顶部所得的像素值,是ListView的高度 if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { end -= mListPadding.bottom; } // nextTop 是下一个子item的头部,end是listveiw的底部的高度, // 比较nextTop 和 end,判断是否继续填充下一个item,nextTop < end确保了我们只要将新增的子View能够覆盖ListView的界面,nextTop >= end 时,子元素超出屏幕 // mItemCount 是Adapter 元素的数量,pos < mItemCount确保了我们新增的子View在Adapter中都有对应的数据源item,pos >= mItemCount 时,adapter元素都被遍历完了 while (nextTop < end && pos < mItemCount) { // is this the selected item? boolean selected = pos == mSelectedPosition; // 将pos和nextTop传递给makeAndAddView方法 View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected); // child的bottom值表示的是该child的底部到ListView顶部的距离,将该child的bottom作为下一个child的top,nextTop保存着下一个child的top值 nextTop = child.getBottom() + mDividerHeight; if (selected) { selectedView = child; } pos++; // 循环一次,pos加1,position指针下移 } setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1); return selectedView; }fillDown() 是加载item 布局的主要实现,while循环 保证了 只加载一屏,超出的数据不会加载
nextTop < end确保了我们只要将新增的子View能够覆盖ListView的界面就可以了,比如ListView的高度最多显示10个子View,我们没必要向ListView中加入11个子View。pos < mItemCount确保了我们新增的子View在Adapter中都有对应的数据源item,比如ListView的高度最多显示10个子View,但是我们Adapter中一共才有5条数据,这种情况下只能向ListView中加入5个子View,从而不能填充满ListView的全部高度。再看看 每个item的 布局 是怎么加载的
该方法会创建View, 返回这个View作为child,,并把该子View添加到ListView的children中
/** * 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. * * @param position Logical position in the list * position表示的是数据源item在Adapter中的索引 * * @param y Top or bottom edge of the view to add * y表示要生成的View的top值或bottom值 * * @param flow If flow is true, align top edge to y. If false, align bottom * edge to y. * flow是true,那么y表示top值,否则表示bottom值 * * @param childrenLeft Left edge where children should be positioned * @param selected Is this position selected? * @return View that was added */ private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) { View child; if (!mDataChanged) { // 如果数据没有变化,尝试用该position从RecycleBin的mActiveViews中获取可复用的View // Try to use an existing view for this position child = mRecycler.getActiveView(position); // 第一次进入时 ,child 是null if (child != null) { // 如果child 不为空,说明我们找到了一个已经存在的child, // 这样mActiveViews中存储的View就被直接复用了 // 调用setupChild,对child进行定位 // Found it -- we're using an existing child // This just needs to be positioned setupChild(child, position, y, flow, childrenLeft, selected, true); return child; } } // 如果数据变化了,新建一个View,或者如果可能的话去ScrapView中拿一个缓存的view // Make a new view for this position, or convert an unused view if possible child = obtainView(position, mIsScrap); // 这个方法负责进行定位和量算,把View放到ListView中合适的位置 // This needs to be positioned and measured setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); return child; }makeAndAddView() 有两种方式加载一个view,一个是 mRecycler.getActiveView(),一个是 obtainView() 第一次加载时,activeview仅仅在第一次layoutChildren中赋值且为空值,所以走 obtainView() 加载了 一个View,加载完的View传入 setupChild() 来显示
如果没能够从mActivieViews中直接复用View,那么就要调用obtainView方法获取View,该方法尝试间接复用RecycleBin中的mScrapViews中的View,如果不能间接复用,则创建新的View。
/** * Get a view and have it show the data associated with the specified * position. This is called when we have already discovered that the view is * not available for reuse in the recycle bin. The only choices left are * converting an old view or making a new one. * * @param position The position to display * @param isScrap Array of at least 1 boolean, the first entry will become true if * the returned view was taken from the "temporary detached" scrap heap, false if * otherwise. * * @return A view displaying the data associated with the specified position */ View obtainView(int position, boolean[] isScrap) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView"); isScrap[0] = false; // Check whether we have a transient state view. Attempt to re-bind the // data and discard the view if we fail. final View transientView = mRecycler.getTransientStateView(position); if (transientView != null) { ... return transientView; } final View scrapView = mRecycler.getScrapView(position); // 尝试获取一个废弃缓存中的View final View child = mAdapter.getView(position, scrapView, this); // 调用了getView方法,去初始化我们的covertView // 第一次加载时,scrapView = null ... }obtainView() 要返回一个View,这个view可能是从废弃缓存或 Adapter的getView()中获取的,第一次加载时 mRecycler.getTransientStateView()返回值是null,所以obtainView() 返回的是view是 mAdapter.getView()
获取到view,makeAndAddView() 接着往下执行,调用 setupChild(),obtainView() 返回的View也传入 setupChild() 中
setupChild() 调用 ViewGroup.addViewInLayout() 显示 这个item的View
加载完view,ListView.layoutChildren() 都到了 RecyleBin.scrapActiveViews()
/** * Move all views remaining in mActiveViews to mScrapViews. * 将mActiveViews废弃到mScrapViews中 */ void scrapActiveViews() { final View[] activeViews = mActiveViews; final boolean hasListener = mRecyclerListener != null; final boolean multipleScraps = mViewTypeCount > 1; ArrayList<View> 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<View>(); } long id = mAdapter.getItemId(mFirstActivePosition + i); mTransientStateViewsById.put(id, victim);// 将 victim 添加到 mTransientStateViewsById 中 } else if (!mDataChanged) { if (mTransientStateViews == null) { mTransientStateViews = new SparseArray<View>(); } mTransientStateViews.put(mFirstActivePosition + i, victim); // 将 victim 添加到 mTransientStateViews 中 } ... }RecyleBin.scrapActiveViews()中有2个重要的赋值操作,是将view添加到 瞬间状态的数组中,瞬态的来源是view中hasTransientState方法,View.hasTransientState() 如下,其方法作用看源码解释就行,item中如果有view正在进行动画之类的动态改变发生。
/** * Indicates whether the view is currently tracking transient state that the * app should not need to concern itself with saving and restoring, but that * the framework should take special note to preserve when possible. * * <p>A view with transient state cannot be trivially rebound from an external * data source, such as an adapter binding item views in a list. This may be * because the view is performing an animation, tracking user selection * of content, or similar.</p> * * @return true if the view has transient state */ @ViewDebug.ExportedProperty(category = "layout") public boolean hasTransientState() { return (mPrivateFlags2 & PFLAG2_HAS_TRANSIENT_STATE) == PFLAG2_HAS_TRANSIENT_STATE; }第一次layoutchildern 结束了
第二次加载时 recycleBin.fillActiveViews() 给activeview添加数据。将第一次加载的view放在了activeViews中 detachAllViewsFromParent() 清除掉 ListView中的子View,防止数据重复,ListView中所有的子View都是处于detach状态,后续从RecyleBin中获取缓存的view 进入switch (mLayoutMode),LayoutMode还是LAYOUT_NORMAL,进入default分支,getChildCount() 不再是0了,是 一屏显示的item的个数 调用 fillSpecific() 加载view
将所有的子View从ListView中分离,也就是清空了children
/** * Detaches all views from the parent. Detaching a view should be followed * either by a call to * {@link #attachViewToParent(View, int, android.view.ViewGroup.LayoutParams)} * or a call to {@link #removeDetachedView(View, boolean)}. Detachment should only be * temporary; reattachment or removal should happen within the same drawing cycle as * detachment. When a view is detached, its parent is null and cannot be retrieved by a * call to {@link #getChildAt(int)}. * * @see #detachViewFromParent(View) * @see #detachViewFromParent(int) * @see #detachViewsFromParent(int, int) * @see #attachViewToParent(View, int, android.view.ViewGroup.LayoutParams) * @see #removeDetachedView(View, boolean) */ protected void detachAllViewsFromParent() { final int count = mChildrenCount; if (count <= 0) { return; } final View[] children = mChildren; mChildrenCount = 0; for (int i = count - 1; i >= 0; i--) { children[i].mParent = null; children[i] = null; } }fillSpecific()方法会优先将指定位置的子View先加载到屏幕上,然后再加载该子View往上以及往下的其它子View。那么由于这里我们传入的position就是第一个子View的位置,于是fillSpecific()方法的作用就基本上和fillDown()方法是差不多的了,接着看 makeAndAddView()
layoutChildren() 中调用 recycleBin.fillActiveViews 缓存了子View makeAndAddView() 中 调用 mRecycler.getActiveView() 取出缓存的子View 调用 setupChild(),最后一个参数 是true,表示是从RecyleBin中取出的缓存的view 第二次 layout时,不走obtainView(),会节省一些时间
举一个例子,假设在某一时刻ListView中显示了10个子View,position依次为从0到9。然后我们手指向上滑动,且向上滑动了一个子View的高度,ListView需要绘制下一帧。这时候ListView在layoutChildren方法中把这10个子View都放入到了RecycleBin的mActiveViews数组中了,然后清空了children数组,然后调用fillDown方法,向ListView中依次添加position1到10的子View,在添加position为1的子View的时候,由于在上一帧中position为1的子View已经被放到mActiveViews数组中了,这次直接可以将其从mActiveViews数组中取出来,这样就是直接复用子View,所以说RecycleBin的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; // 将activeview的对应item给置空,说明activeview仅仅只能使用一次 return match; } return null; }setupChild() 调用 attachViewToParent() ,将view添加到 viewGroup中
getTransientStateView() 从 mTransientStateViewsById 或 mTransientStateViews 返回一个view
mTransientStateViewsById , mTransientStateViews 在哪赋值的 scrapActiveViews() 和 addScrapView() 中
AbsListView.RecyleBin.scrapActiveViews()
/** * Move all views remaining in mActiveViews to mScrapViews. * 将mActiveViews废弃到mScrapViews中 */ void scrapActiveViews() { ... if (victim.hasTransientState()) { // Store views with transient state for later use. victim.dispatchStartTemporaryDetach(); if (mAdapter != null && mAdapterHasStableIds) { if (mTransientStateViewsById == null) { mTransientStateViewsById = new LongSparseArray<View>(); } long id = mAdapter.getItemId(mFirstActivePosition + i); mTransientStateViewsById.put(id, victim);// 将 victim 添加到 mTransientStateViewsById 中 } else if (!mDataChanged) { if (mTransientStateViews == null) { mTransientStateViews = new SparseArray<View>(); } mTransientStateViews.put(mFirstActivePosition + i, victim); // 将 victim 添加到 mTransientStateViews 中 } else if (whichScrap != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { // The data has changed, we can't keep this view. removeDetachedView(victim, false); } } ... }AbsListView.RecyleBin.addScrapView()
/** * Puts a view into the list of scrap views. * <p> * 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) { ... // Don't scrap views that have transient state. final boolean scrapHasTransientState = scrap.hasTransientState(); if (scrapHasTransientState) { if (mAdapter != null && mAdapterHasStableIds) { // 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);//将 scrap添加到 mTransientStateViewsById 中 } else if (!mDataChanged) { // 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);// 将 scrap 添加到 mTransientStateViews 中 } else { // Otherwise, we'll have to remove the view and start over. getSkippedScrap().add(scrap); } } ... }transient state类似 view 的一个属性,该属性的作用就是标注当前view是否正在变化中。 ListView的item 在加载数据时或者 播放动画时,view处于 瞬态 transient state。
obtainView()中final View updatedView = mAdapter.getView(position, transientView, this); mAdapter.getView() 获得的view是静态的view,没有发生任何动态改变和展现任何正在发生中的动画的item。 接着下边 mRecycler.addScrapView(updatedView, position);添加到RecyleBin中的是这个静态的view
/** * Get a view and have it show the data associated with the specified * position. This is called when we have already discovered that the view is * not available for reuse in the recycle bin. The only choices left are * converting an old view or making a new one. * * @param position The position to display * @param isScrap Array of at least 1 boolean, the first entry will become true if * the returned view was taken from the "temporary detached" scrap heap, false if * otherwise. * * @return A view displaying the data associated with the specified position */ View obtainView(int position, boolean[] isScrap) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView"); isScrap[0] = false; // Check whether we have a transient state view. Attempt to re-bind the // data and discard the view if we fail. final View transientView = mRecycler.getTransientStateView(position); if (transientView != null) { final LayoutParams params = (LayoutParams) transientView.getLayoutParams(); // If the view type hasn't changed, attempt to re-bind the data. if (params.viewType == mAdapter.getItemViewType(position)) { final View updatedView = mAdapter.getView(position, transientView, this);// 获取一个静态的view // If we failed to re-bind the data, scrap the obtained view. if (updatedView != transientView) { setItemViewLayoutParams(updatedView, position); mRecycler.addScrapView(updatedView, position);// 将这个静态的view添加到RecyleBin中 } } isScrap[0] = true; // Finish the temporary detach started in addScrapView(). transientView.dispatchFinishTemporaryDetach(); return transientView; } ... }上边介绍了 怎么加载数据,下边将是 ListView最神奇的部分,滑动加载更多, 比如说我们的Adapter当中有1000条数据,但是第一屏只显示了10条,ListView中也只有10个子View而已,那么剩下的990是怎样工作并显示到界面上的呢? ListView 和 GridView 都支持 滑动加载数据,所以滑动的代码在 AbsListView 中 onTouchEvent() 中代码非常多,逻辑也复杂 滑动部分对应的 ACTION_MOVE,我们看 ACTION_MOVE 的逻辑
ACTION_MOVE 调用了 onTouchMove()
手指上滑时,TouchMode是TOUCH_MODE_SCROLL,onTouchMove() 调用了 scrollIfNeeded()
scrollIfNeeded() 中 屏幕有移动就会调用 trackMotionScroll(),滑动屏幕时,trackMotionScroll() 会被多次调用
通过 down 来判断 上滑 或下滑,遍历子View,移出屏幕的子View 通过 mRecycler.addScrapView() 添加到 RecyleBin的废弃缓存中 detachViewsFromParent() ListView 中移出屏幕的子view全detach掉 offsetChildrenTopAndBottom() 使view随着 incrementalDeltaY 偏移,实现ListView随着手指动的效果 listView滑到头了,调用 fillGap(),fillGap() 是一个抽象方法,由子类实现
/** * Fills the gap left open by a touch-scroll. During a touch scroll, children that * remain on screen are shifted and the other ones are discarded. The role of this * method is to fill the gap thus created by performing a partial layout in the * empty space. * * @param down true if the scroll is going down, false if it is going up */ abstract void fillGap(boolean down);fillGap() 中调用 fillDown() 或 fillUp()
fillDown() 和 fillUp() 都调用了 makeAndAddView()
makeAndAddView() 中 调用 mRecycler.getActiveView() 返回的是 null (因为第二次加载时,已经调用了 getActiveView(), mActiveViews是不能够重复利用的) 调用 obtainView(),setupChild()
mRecycler.getScrapView() 从废弃缓存中获取一个view 从缓存中拿到子View之后再调用setupChild()方法将它重新attach到ListView当中
getView()例子
public View getView(int position, View convertView, ViewGroup parent){ ViewHolder holder; if (convertView == null) { convertView = mInflater.inflate(R.layout.list_item, parent, false); holder = new ViewHolder(); holder.text = (TextView)convertView.findViewById(R.id.text); holder.icon = (ImageView)convertView.findViewById(R.id.icon); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } holder.text.setText(DATA[position]); holder.icon.setImageBitmap(Icons[position]); return convertView;}public static class ViewHolder{ TextView text; ImageView icon;}第二个参数就是我们最熟悉的convertView呀,难怪平时我们在写getView()方法是要判断一下convertView是不是等于null,如果等于null才调用inflate()方法来加载布局,不等于null就可以直接利用convertView,因为convertView就是我们之间利用过的View,只不过被移出屏幕后进入到了废弃缓存中,现在又重新拿出来使用而已。然后我们只需要把convertView中的数据更新成当前位置上应该显示的数据,那么看起来就好像是全新加载出来的一个布局一样,这背后的道理你是不是已经完全搞明白了?
setupChild() 调用 ViewGroup.attachViewToParent() 加载子view
最后再来看看 RecyleBin 是怎么用的,上边涉及到RecyleBin的操作如下图
下边介绍下 ListView、AbsListView中是怎么使用RecyleBin的
如果数据没变,调 mRecycler.getActiveView() 获取一个active view
/** * 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. * * @param position Logical position in the list * @param y Top or bottom edge of the view to add * @param flow If flow is true, align top edge to y. If false, align bottom * edge to y. * @param childrenLeft Left edge where children should be positioned * @param selected Is this position selected? * @return View that was added */ 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);// 从 RecyleBin 中获取一个active view // 第一次进入时 ,child 是null // 第二次进入时,getActiveView()会返回一个View,child != null // 根据RecycleBin的机制,mActiveViews是不能够重复利用的, 第二次加载时 获取过,再调用 getActiveView 返回的是 null 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; }Scroll the children by amount, adding a view at the end and removing views that fall off as necessary. 对子view滑动一定距离,添加view到底部或者移除顶部的不可见view。从注释看,不可见的item 的自动移除是在scrollListItemsBy中进行的。
Android中view回收的计算是其父view中不再显示的,如果scrollview中包含了一个wrap_content属性的listview,里面的内容并不会有任何回收,引起listview 的getheight函数获取的是一个足以显示所有内容的高度。
当这个方法被调用时,说明Recycle bin中的view已经不可用了,那么,现在唯一的方法就是,convert一个老的view,或者构造一个新的view
/** * Get a view and have it show the data associated with the specified * position. This is called when we have already discovered that the view is * not available for reuse in the recycle bin. The only choices left are * converting an old view or making a new one. * * @param position The position to display * @param isScrap Array of at least 1 boolean, the first entry will become true if * the returned view was taken from the "temporary detached" scrap heap, false if * otherwise. * * @return A view displaying the data associated with the specified position */ View obtainView(int position, boolean[] isScrap) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView"); isScrap[0] = false; // Check whether we have a transient state view. Attempt to re-bind the // data and discard the view if we fail. final View transientView = mRecycler.getTransientStateView(position); if (transientView != null) { final LayoutParams params = (LayoutParams) transientView.getLayoutParams(); // If the view type hasn't changed, attempt to re-bind the data. if (params.viewType == mAdapter.getItemViewType(position)) { final View updatedView = mAdapter.getView(position, transientView, this); // If we failed to re-bind the data, scrap the obtained view. if (updatedView != transientView) { setItemViewLayoutParams(updatedView, position); mRecycler.addScrapView(updatedView, position); } } isScrap[0] = true; // Finish the temporary detach started in addScrapView(). transientView.dispatchFinishTemporaryDetach(); return transientView; } // trackMotionScroll()中,一旦有子view移除屏幕,就加入到废弃缓存中 final View scrapView = mRecycler.getScrapView(position); // 尝试获取一个废弃缓存中的View final View child = mAdapter.getView(position, scrapView, this); // 通过 Adapter.getView() 获取一个 View // 第一次加载时,scrapView = null if (scrapView != null) { if (child != scrapView) { // 如果重用的scrapView和adapter获得的view是不一样的,将scrapView进行回收 // Failed to re-bind the data, return scrap to the heap. mRecycler.addScrapView(scrapView, position);// 将 scrapView 回收 } else { // 如果重用的view和adapter获得的view是一样的,将isScrap[0]值为true,否则默认为false if (child.isTemporarilyDetached()) { isScrap[0] = true; // Finish the temporary detach started in addScrapView(). child.dispatchFinishTemporaryDetach(); } else { // we set isScrap to "true" only if the view is temporarily detached. // if the view is fully detached, it is as good as a view created by the // adapter isScrap[0] = false; } } } if (mCacheColorHint != 0) { child.setDrawingCacheBackgroundColor(mCacheColorHint); } if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); } setItemViewLayoutParams(child, position); if (AccessibilityManager.getInstance(mContext).isEnabled()) { if (mAccessibilityDelegate == null) { mAccessibilityDelegate = new ListItemAccessibilityDelegate(); } if (child.getAccessibilityDelegate() == null) { child.setAccessibilityDelegate(mAccessibilityDelegate); } } Trace.traceEnd(Trace.TRACE_TAG_VIEW); return child; }新闻热点
疑难解答