RecycleBin原理解析,带你领会ListView的View重用机制

来源:互联网 发布:单片机做扫地机器人 编辑:程序博客网 时间:2024/05/17 13:45

ListView无疑是Android开发中使用最多的组件之一了,可以肯定是99%以上的应用中都是用了ListView,不过ListView也不是万能的,很多时候你会觉得ListView提供给我们的功能并不够,我们需要扩展ListView,或者重新自定义一个支持滚动,view重用特性的组件。如果我们能够了解ListView的内部实现原理,相信对于更好地利用ListView以及对其进行扩展都是不错的。

转载请注明出处: http://blog.csdn.net/qinjunni2014/article/details/45653747

1. view的重用

ListView最重要的特性就是对view进行了重用,我们只要理解了view的重用原理,就对listview有了很大程度的了解。我们来看看它是如何做的呢?在将这个之前,让我们简要看看ListView的继承结构,ListView继承自AbsListView,而AbsListView继承自AdapterView,AdapterView继承自ViewGroup,因此我们从AdapterView说起就比较明了了。

AdapterView是一个抽象类,里面定义很多接口方法,有待子类去实现,不过也实现了一部分的方法。如下图所示

这里写图片描述

这里写图片描述

其中有一部分是继承自ViewGroup的,比如AddView系列以及removeView系列,Accessibility系列。这里我们就不多讲了,相信大家也很熟悉,除此之外就是AdapterView自己的函数,AdapterView顾名思义肯定是根据一个Adapter来生成它内部的view。说到这,我们又不得不引出Adapter这个接口,注意这是个接口,它只是定义了一些接口函数。

这里写图片描述

这些函数规定了适配器内部每个item的类型,id,每个item对应的view,以及总公的item数量。此外有一个函数需要注意的是hasStableIds(), 这个函数返回一个bool值,如果为true,代表同一个id总是对应同一个item。这个函数在稍后我们将view重用时会用到。AdapterView接口规定了使用Adapter来生成view的函数。

2. RecycheBin

关于view重用的代码大部分都在AbsListView里面,那是因为不光是ListView, 还有GridView也继承了AbsListView,他们都是使用了相同的view重用的原理。我们直接看AbsListView,这个类比较复杂,不过不要担心,我们就从view重用这一点慢慢展开,我们可以看到它有一个内部类叫RecycleBin,正是这个类实现了view的重用,看名字也可以看出。它的成员比较简单,就这些:

 private int mFirstActivePosition; private View[] mActiveViews = new View[0]; private ArrayList<View>[] mScrapViews; private int mViewTypeCount; private ArrayList<View> mCurrentScrap; private ArrayList<View> mSkippedScrap; private SparseArray<View> mTransientStateViews; private LongSparseArray<View> mTransientStateViewsById;

mActiveViews代表了在每一次layout开始的时候,位于屏幕上的view, mFirstActivePosition指定了第一个active的view的位置。对于mActiveViews通常的操作为,在每次layout开始,AbsListView会将位于屏幕上的view全部填充到RecycleBin的mActiveViews中去。layout过程中,将下一轮即将显示在屏幕上得view从RecycleBin中取出来,最后如果mActiveViews中还有元素,就在layout结束时将它们统统转移到mScrapView中去。这个流程可以从ListView中的layoutChildren看出,每次layout时,onlayout最终会调用这个函数

 @Override protected void layoutChildren() {     //....忽略一大坨代码    // layout开始前将所有已经存在的子view放入recycleBin中   // 如果dataChanged为true,就放入mActiveViews中,否则放入mScrapViews中去   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();    //重新填充子view...    // 将所有没有被重用到的view从mActiveViews转移到mScrapViews中去    recycleBin.scrapActiveViews();}

从layout的过程就可以看出来,scrapview实际上是一个存放备用view的回收池,每次layout完,有多余的view会存储到池子里,以后可能会用到。那这是layout时候做的事情,如果是scroll的情况呢,情况其实类似。我们来看看scroll的流程图

这里写图片描述

scroll事件从onTouchEvent函数发起,大家肯定知道的,中间经过一些判断,最终带着deltaY和incrementalDetalY到达trackMotionScroll函数,我们的分析从这个函数开始.

boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {    //重新计算deltay和 incrementalDeltaY,判断时候还能继续向下滚动或者向上滚动    //.....一波代码    final boolean down = incrementalDeltaY < 0;//判断是否向下滚动    if (down) {       int top = -incrementalDeltaY;       if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {           top += listPadding.top;       }       //如果向下滚动,则有些view会从上方滚出       for (int i = 0; i < childCount; i++) {           final View child = getChildAt(i);           //判断子view是否已经滚出           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);//加入scrap集合备用               }           }       }   } 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);               }           }       }   }    //....    if (count > 0) {       detachViewsFromParent(start, count);//将滚出的view进行detach        mRecycler.removeSkippedScrap();    }    //....    final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);    if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {        fillGap(down);//填充滚动引起的view间隙    }}

从我的注释,大家可以看到在滚动过程中,RecycleBin所起的作用,当然函数在走到fillGap前,只是完成了一部分滚出view的回收,接下来,是利用这些view进行重用还是生成新的view就要看fillGap函数所做的操作了。

@Overridevoid fillGap(boolean down) {    final int count = getChildCount();    if (down) {//如果是向下滚动,则调用fillDown        int paddingTop = 0;        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {            paddingTop = getListPaddingTop();        }        final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :                paddingTop;        fillDown(mFirstPosition + count, startOffset);        correctTooHigh(getChildCount());    } else {//如果是向上滚动,则调用fillUp        int paddingBottom = 0;        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {            paddingBottom = getListPaddingBottom();        }        final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :                getHeight() - paddingBottom;        fillUp(mFirstPosition - 1, startOffset);        correctTooLow(getChildCount());    }}

fillGap的实现在ListView中,它只是做了个分派操作,内部分向上滚动和向下滚动分别调用fillUp和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;    }    //开启循环,持续填充view直到已经填满    while (nextTop < end && pos < mItemCount) {        // is this the selected item?        boolean selected = pos == mSelectedPosition;        View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);        //更新下次填充view的top位置        nextTop = child.getBottom() + mDividerHeight;        if (selected) {            selectedView = child;        }        pos++;//更新被填充位置的position    }    setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);    return selectedView;}

fillDown的两个参数分别为即将开始填充的view的item 位置 和 top坐标位置。

makeAndAddView所做的操作就是获得一个view并将其添加到viewHierarchy上。

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,            boolean selected) {    View child;    //判断数据时候已经发生变化,如果没有,就尝试从ActiveView中去获取view,    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;        }    }    // 尝试从scrap中获取可重用的view,如果没有,就创建新的view    child = obtainView(position, mIsScrap);    // 设置子view的位置,如果有必要,会去重新measure子view,并添加到父view上    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);    return child;}

makeAndAddView在数据没有改变的情况下,会首先尝试从mActiveViews中去获取。不过需要注意的是,在scroll操作中,我们一开始并没有把屏幕上的view填充到mActiveViews中,因此scroll逻辑走到这里的时候,从mActiveViews中是拿不到view的,为什么还有这一段呢?那是因为这段代码在layout时也会调用的,从layoutChildren函数的重新填充子view那一步中,会调用一系列以fill开头的函数,最终这些函数都会走到这里。现在我们将注意力集中到obtainView上去。

View obtainView(int position, boolean[] isScrap) {   Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");   isScrap[0] = false;   // 检查是否有一个对应的处于 transient state 的view. 如果有尝试重新绑定Data   //    final View transientView = mRecycler.getTransientStateView(position);   if (transientView != null) {       final LayoutParams params = (LayoutParams) transientView.getLayoutParams();       // 如果view type没有变化,尝试重新绑定数据       if (params.viewType == mAdapter.getItemViewType(position)) {           final View updatedView = mAdapter.getView(position, transientView, this);           // 如果两者不相等,表示重绑定数据失败,生成了新的view,           //但是我们依然使用transientView,将updatedView入回收池           if (updatedView != transientView) {               setItemViewLayoutParams(updatedView, position);               mRecycler.addScrapView(updatedView, position);           }       }       // Scrap view implies temporary detachment.       isScrap[0] = true;       return transientView;   }    //找不到transientview的情况下,就从回收池中去取,   final View scrapView = mRecycler.getScrapView(position);   //重新绑定数据,   final View child = mAdapter.getView(position, scrapView, this);   if (scrapView != null) {       //如果重绑定失败,将scrapView重新入回收池,采用新生成的view       if (child != scrapView) {           // Failed to re-bind the data, return scrap to the heap.           mRecycler.addScrapView(scrapView, position);       } else {           isScrap[0] = true;           child.dispatchFinishTemporaryDetach();       }   }    //设置子view的一些属性   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;}

3. Transient State

说到这里读者肯定回问什么是TransientState的View,大家可以看到View类中其实有两个方法,一个叫做hasTransientState,一个叫setHasTransientState,如果一个view被设置了具有transient state,那么系统会尽量保持view的状态属性,不让其它被其他数据模型绑定,比如这个view正在执行动画操作,或者这个view正在跟踪用户的选择。比如我们正在对某个view执行动画操作时,我们可以设定setHasTransientView(true),动画结束后,再设定setHasTransientView(false),注意这两个必须成对出现. 将view设成transient state的其实是对view的一个保护,不让其被其填充新的数据。

RecycleBin对这种状态的view做了单独的处理,其内部有两个SparseArray,用来存储已经滚出屏幕但是设置了transient state状态的view。

 private SparseArray<View> mTransientStateViews; private LongSparseArray<View> mTransientStateViewsById;

两者的区别在于,前者可以通过item的位置找到view,后者通过item的id,找到view。
在前面的分析中,我们可以看到,每次scroll开始时,都会对滚出的屏幕的view调用addScrapView。其实在addScrap过程中,会优先考虑是否添加到这两个容器里面。

final boolean scrapHasTransientState = scrap.hasTransientState();if (scrapHasTransientState) {    if (mAdapter != null && mAdapterHasStableIds) {//Adapter对一个对象产生唯一的id        // If the adapter has stable IDs, we can reuse the view for        // the same data.        if (mTransientStateViewsById == null) {            mTransientStateViewsById = new LongSparseArray<View>();        }        mTransientStateViewsById.put(lp.itemId, scrap);    } else if (!mDataChanged) {//数据没变,添加到position->view容器中        // If the data hasn't changed, we can reuse the views at        // their old positions.        if (mTransientStateViews == null) {            mTransientStateViews = new SparseArray<View>();        }        mTransientStateViews.put(position, scrap);    } else {        // 否则将其放入skippedScrap,有待回收        if (mSkippedScrap == null) {            mSkippedScrap = new ArrayList<View>();        }        mSkippedScrap.add(scrap);    }} else {    //真正执行回收操作    if (mViewTypeCount == 1) {        mCurrentScrap.add(scrap);    } else {        mScrapViews[viewType].add(scrap);    }    //调用RecyclerListener的onMoveToScrapHeap函数,执行当前view已经被回收。    if (mRecyclerListener != null) {        mRecyclerListener.onMovedToScrapHeap(scrap);    }}

在scrapActiveViews函数中也有类似的操作。如果每个view都被设成了transient state,那么scrapView中将不会收到任何view,以至于每次都要重新生成新的view,也就是adapter的getView函数传来的convertView为null,这是因为所有滚出屏幕的view都被添加到transientViews中去了。大家可以试试在adapter的getview函数中,view返回前将其设置为transientState的,那每次我们都需要用inflater去inflate,或者new出新的view。

值得注意的是,只有在view被放入mCurrentScrap或mScrapViews中时,才会去调用onMoveToScrapHeap通知回收监听器,当前view已经被回收,是时候释放一些view所持有的资源了,比如释放图片

4. SkippedScrap

最后,RecycleBin中还有一个不太重要的mSkippedScrap,什么时候添加view到其中呢?简单搜一下,只有一处,就是在addScrapView函数中,当当前view有transient state,但是却不满足stableId或者 mAdapter数据没有发生变化这两个条件, 这个view就会被添加到skippedScrap中,因为这个view,不能被回收,却又找不到对应的数据item。RecycleBin还有一个函数removeSkippedScrap

void removeSkippedScrap() {    if (mSkippedScrap == null) {        return;    }    final int count = mSkippedScrap.size();    for (int i = 0; i < count; i++) {//detach        removeDetachedView(mSkippedScrap.get(i), false);    }    mSkippedScrap.clear();//清空}

在trackMotionScroll,和layoutChildren中会去调用这段代码,很好理解,因为只有在滚动时重新layout时,才会view可能被加入skippedScrap中去。

好了,到了这里RecycleBin大部分的原理都讲得差不多了,其实看透了就很简单。大家在写类似view回收池时可以参考RecycleBin的写法哦。谢谢大家的阅读!

0 0
原创粉丝点击