«

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

时间:2024-3-2 17:30     作者:韩俊     分类: Android


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的函数。

  1. 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函数所做的操作了。

@Override
void 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;
}
  1. 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所持有的资源了,比如释放图片

  1. 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的写法哦。谢谢大家的阅读!

标签: android

热门推荐