[关闭]
@universal 2018-05-30T10:48:56.000000Z 字数 9529 阅读 219

RecycleBin/Recycler的回收机制(上)

view


众所周知,RecycleBin是Listview,Recycler是RecyclerView的回收机制,而这个机制也正是列表view加载大量数据不会发生OOM的核心所在。
这里为什么想要对这两个机制作对比呢?因为前不久看到一个问题,我们现在可能开发大多数场景都会使用RecyclerView来实现复杂好看的UI,毕竟RecyclerView的拓展性好太多。那么既然RecyclerView都已经这样了,为什么谷歌还不舍弃ListView呢?既然表象已经被超越了,那么就看内在,总会有那么一丢丢的闪光点。


RecycleBin

我们知道,ListView继承自AbsListView。而RecycleBin是AbsListView的一个内部类,代码也很短。先来看看官方解释:

  1. /**
  2. * The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of
  3. * storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the
  4. * start of a layout. By construction, they are displaying current information. At the end of
  5. * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that
  6. * could potentially be used by the adapter to avoid allocating views unnecessarily.
  7. */

大意就是: RecycleBin用于view重用,里面的views有两个级别分为ActiveViews和ScrapViews,前者就是屏幕上正在显示的view,后者存储废弃的view(比如说滑出屏幕)。


具体看看recyclerbin中的属性

  1. class RecycleBin {
  2. private RecyclerListener mRecyclerListener;
  3. //The position of the first view stored in mActiveViews.
  4. private int mFirstActivePosition;
  5. /**
  6. * Views that were on screen at the start of layout. This array is populated at the start of
  7. * layout, and at the end of layout all view in mActiveViews are moved to mScrapViews.
  8. * Views in mActiveViews represent a contiguous range of Views, with position of the first
  9. * view store in mFirstActivePosition.
  10. */
  11. private View[] mActiveViews = new View[0];
  12. /**
  13. * Unsorted views that can be used by the adapter as a convert view.
  14. */
  15. private ArrayList<View>[] mScrapViews;
  16. private int mViewTypeCount;
  17. private ArrayList<View> mCurrentScrap;
  18. private ArrayList<View> mSkippedScrap;
  19. private SparseArray<View> mTransientStateViews;
  20. private LongSparseArray<View> mTransientStateViewsById;
  21. }

(1)RecyclerListener接口只有一个方法onMovedToScrapHeap(),表示某个view被回收到了scrap heap. 该view不再被显示,任何相关的昂贵资源应该被丢弃。该回调是处理回收时view中的资源释放,比如网络加载图片资源未完成,就可以再回调中停止加载。

(2)mActiveViews; 根据官方解释,该数组在布局view显示的时候会被填充,view废弃的时候会被移至mScrapViews。

(3)mScrapViews; 根据官方解释,该数组中的view可以被adapter拿来用作ConvertView,其实就是adapter中getView()方法中ConvertView参数的来源。注:这是个ArrayList型的数组,对应不同的ViewType。

(4)mSkippedScrap、mTransientStateViews和mTransientStateViewsById都是用来缓存具有TransientState的view。

这个Transient是view的一种状态,官方解释:
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.

也就是说,比如view如果正在执行某个动画,那么它就是不稳定状态,无法被重新绑定数据。

mTransientStateViews通过item的位置找到view,mTransientStateViewsById通过item的id找到view,如果都不符合则放入skippedScrap,有待回收。具体的逻辑如下:在addScrapView()中

  1. final boolean scrapHasTransientState = scrap.hasTransientState();
  2. if (scrapHasTransientState) {
  3. if (mAdapter != null && mAdapterHasStableIds) {//Adapter对一个对象产生唯一的id
  4. // If the adapter has stable IDs, we can reuse the view for
  5. // the same data.
  6. if (mTransientStateViewsById == null) {
  7. mTransientStateViewsById = new LongSparseArray<View>();
  8. }
  9. mTransientStateViewsById.put(lp.itemId, scrap);
  10. } else if (!mDataChanged) {//数据没变,添加到position->view容器中
  11. // If the data hasn't changed, we can reuse the views at
  12. // their old positions.
  13. if (mTransientStateViews == null) {
  14. mTransientStateViews = new SparseArray<View>();
  15. }
  16. mTransientStateViews.put(position, scrap);
  17. } else {
  18. // 否则将其放入skippedScrap,有待回收
  19. if (mSkippedScrap == null) {
  20. mSkippedScrap = new ArrayList<View>();
  21. }
  22. mSkippedScrap.add(scrap);
  23. }
  24. } else {
  25. //真正执行回收操作
  26. if (mViewTypeCount == 1) {
  27. mCurrentScrap.add(scrap);
  28. } else {
  29. mScrapViews[viewType].add(scrap);
  30. }
  31. //调用RecyclerListener的onMoveToScrapHeap函数,执行当前view已经被回收。
  32. if (mRecyclerListener != null) {
  33. mRecyclerListener.onMovedToScrapHeap(scrap);
  34. }
  35. }

下面就看看RecycleBin是如何被使用的

从listview的layout开始

  1. //由父类的onLayout调用
  2. @Override
  3. protected void layoutChildren() {
  4. .......
  5. // Pull all children into the RecycleBin.
  6. // These views will be reused if possible
  7. final int firstPosition = mFirstPosition;
  8. final RecycleBin recycleBin = mRecycler;
  9. if (dataChanged) {
  10. //如果数据发生了改变,则将view都回收mScrapViews中
  11. for (int i = 0; i < childCount; i++) {
  12. recycleBin.addScrapView(getChildAt(i), firstPosition+i);
  13. }
  14. } else {
  15. //否则就将view全部放入mActiveViews中
  16. recycleBin.fillActiveViews(childCount, firstPosition);
  17. }
  18. // 解绑所有的子view,防止创建两次
  19. detachAllViewsFromParent();
  20. recycleBin.removeSkippedScrap();
  21. .......
  22. //将所有保留在mActiveViews中的视图移动到mScrapViews。
  23. recycleBin.scrapActiveViews();
  24. }

listview的layout的后续过程就不详细分析了,可以直接看下面的图,下图是以listview第一次layout为例,后面的layout就是判断条件会发生变化,但是逻辑流程还是差不多,下面会给出相关的源码解释。

图1
图1

来具体看看listview是如何利用有限数量的view加载更多的item

listView对滑动监听的实现是在父类AbListView中,主要是对move事件的处理

  1. @Override
  2. public boolean onTouchEvent(MotionEvent ev) {
  3. .....
  4. case MotionEvent.ACTION_MOVE: {
  5. onTouchMove(ev, vtev);
  6. break;
  7. }
  8. .....
  9. }
  10. private void onTouchMove(MotionEvent ev, MotionEvent vtev) {
  11. .....
  12. case TOUCH_MODE_SCROLL:
  13. case TOUCH_MODE_OVERSCROLL:
  14. scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev);
  15. break;
  16. .....
  17. }
  18. private void scrollIfNeeded(int x, int y, MotionEvent vtev) {
  19. .....
  20. if (mTouchMode == TOUCH_MODE_SCROLL) {
  21. if (incrementalDeltaY != 0) {
  22. atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
  23. }
  24. }
  25. .....
  26. }

抽丝剥茧下来,其实最重要的就是trackMotionScroll()方法,手指在屏幕上的滑动都会调用这个方法。

  1. //Track a motion scroll
  2. //incrementalDeltaY则表示据上次触发event事件手指在Y方向上位置的改变量,incrementalDeltaY小于0,说明是向下滑动,否则就是向上滑动
  3. boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
  4. ......
  5. if (down) {
  6. //向下滑
  7. int top = -incrementalDeltaY;
  8. if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
  9. top += listPadding.top;
  10. }
  11. for (int i = 0; i < childCount; i++) {
  12. final View child = getChildAt(i);
  13. if (child.getBottom() >= top) {
  14. break;
  15. } else {
  16. count++;
  17. int position = firstPosition + i;
  18. if (position >= headerViewsCount && position < footerViewsStart) {
  19. // The view will be rebound to new data, clear any
  20. // system-managed transient state.
  21. child.clearAccessibilityFocus();
  22. mRecycler.addScrapView(child, position);
  23. }
  24. }
  25. }
  26. } else {
  27. //向上滑
  28. int bottom = getHeight() - incrementalDeltaY;
  29. if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
  30. bottom -= listPadding.bottom;
  31. }
  32. for (int i = childCount - 1; i >= 0; i--) {
  33. final View child = getChildAt(i);
  34. if (child.getTop() <= bottom) {
  35. break;
  36. } else {
  37. start = i;
  38. count++;
  39. int position = firstPosition + i;
  40. if (position >= headerViewsCount && position < footerViewsStart) {
  41. // The view will be rebound to new data, clear any
  42. // system-managed transient state.
  43. child.clearAccessibilityFocus();
  44. mRecycler.addScrapView(child, position);
  45. }
  46. }
  47. }
  48. }
  49. if (count > 0) {
  50. detachViewsFromParent(start, count);
  51. mRecycler.removeSkippedScrap();
  52. }
  53. //使所有的子View都按照传入的参数值进行相应的偏移,实现滚动效果
  54. offsetChildrenTopAndBottom(incrementalDeltaY);
  55. final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
  56. if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
  57. fillGap(down);
  58. }
  59. .....
  60. }

方法的核心其实都在这个down块中,根据滑动的方向和view的位置,将对应的滑出屏幕的view回收到srapViews中,并且detachView。然后如果ListView中最后一个View的底部已经移入了屏幕,或者ListView中第一个View的顶部移入了屏幕,就会调用fillGap()方法去加载屏幕外数据。fillGap的具体实现是在ListView中。

  1. @Override
  2. void fillGap(boolean down) {
  3. final int count = getChildCount();
  4. if (down) {
  5. .....
  6. fillDown(mFirstPosition + count, startOffset);
  7. correctTooHigh(getChildCount());
  8. } else {
  9. .....
  10. fillUp(mFirstPosition - 1, startOffset);
  11. correctTooLow(getChildCount());
  12. }
  13. }

代码逻辑很简单,根据滑动方向,调用fillDown或者fillUp方法去填充listview,fillDown和fillUp逻辑类似,只是填充时候的方向不同。

  1. private View fillDown(int pos, int nextTop) {
  2. View selectedView = null;
  3. int end = (mBottom - mTop);
  4. if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
  5. end -= mListPadding.bottom;
  6. }
  7. while (nextTop < end && pos < mItemCount) {
  8. // is this the selected item?
  9. boolean selected = pos == mSelectedPosition;
  10. View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
  11. nextTop = child.getBottom() + mDividerHeight;
  12. if (selected) {
  13. selectedView = child;
  14. }
  15. pos++;
  16. }
  17. setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
  18. return selectedView;
  19. }

还是图1的逻辑,调用makeAndAddView()

  1. private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
  2. boolean selected) {
  3. if (!mDataChanged) {
  4. // Try to use an existing view for this position.
  5. final View activeView = mRecycler.getActiveView(position);
  6. if (activeView != null) {
  7. // Found it. We're reusing an existing child, so it just needs
  8. // to be positioned like a scrap view.
  9. setupChild(activeView, position, y, flow, childrenLeft, selected, true);
  10. return activeView;
  11. }
  12. }
  13. // Make a new view for this position, or convert an unused view if
  14. // possible.
  15. final View child = obtainView(position, mIsScrap);
  16. // This needs to be positioned and measured.
  17. setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
  18. return child;
  19. }

因为在第二次layout的时候,我们已经用过存储的activeviews,而activeView是不能被重复利用的,所以这里activeView还是null。
进入到obtainView中

  1. View obtainView(int position, boolean[] outMetadata) {
  2. // Check whether we have a transient state view. Attempt to re-bind the
  3. // data and discard the view if we fail.
  4. final View transientView = mRecycler.getTransientStateView(position);
  5. if (transientView != null) {
  6. // If the view type hasn't changed, attempt to re-bind the data.
  7. if (params.viewType == mAdapter.getItemViewType(position)) {
  8. final View updatedView = mAdapter.getView(position, transientView, this);
  9. //数据重新绑定失败,废弃更新的view放入scrap中
  10. if (updatedView != transientView) {
  11. setItemViewLayoutParams(updatedView, position);
  12. mRecycler.addScrapView(updatedView, position);
  13. }
  14. }
  15. return transientView;
  16. }
  17. final View scrapView = mRecycler.getScrapView(position);//第一次layout的时候为null
  18. final View child = mAdapter.getView(position, scrapView, this);
  19. .....
  20. return child;
  21. }

这里先去判断当前position的view是否具有transient状态,若没有再去获取scrapView。
两种情况都会去调用Adapter.getView(position, scrapView, this);
看看我们平时在adapter中写的getView方法。

  1. @Override
  2. public View getView(int position, View convertView, ViewGroup parent) {
  3. Fruit fruit = getItem(position);
  4. View view;
  5. if (convertView == null) {
  6. view = LayoutInflater.from(getContext()).inflate(R.layout.item_layout, null);
  7. } else {
  8. view = convertView;
  9. }
  10. TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
  11. fruitName.setText(fruit.getName());
  12. return view;
  13. }

可以看到第二参数就是我们熟悉的convertview,所以绕了一圈又回来了。如果convertView等于null,就调用inflate()方法来加载布局,不等于null就可以直接利用convertView。加上上面一连串的逻辑可以看出来,convertView就是我们一直循环利用的view,只不过被移出屏幕后进入到了废弃缓存中,现在又重新拿出来使用而已,然后我们只需要把convertView中的数据更新成当前位置上应该显示的数据,这样就实现了有限数量的view加载更多的数据。

既然拿到了convertview,后面直接在makeAndAddView()中调用setupChild(),实现attachViewToParent(),重新与listview attach。


分析到这,recyclerBin的回收复用机制其实就分析的差不多了,总共加载的convertview数其实只有屏幕能显示的那么多,通过mScrapViews的回收和重新绑定数据,来实现四两拨千斤的效果。整篇写下来才发现,listview的回收机制有点多..0.0,所以recyclerview的就放在下一篇,先缓一缓,消化消化...。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注