前两天看到文章RecyclerView 和 ListView 使用对比分析,加上之前在公司做code reiview的时候也遇到过RecyclerView和ListView比较的问题,所以想在这里写一篇文章。
这篇文章的重点不会放在两者使用上的不同,因为前面提到的RecyclerView 和 ListView 使用对比分析这篇文章已经写的非常完美了,我就不重复造轮子了,主要想说明的问题和标题一样,就是我们开发者[为什么]要使用RecyclerView去代替ListView。
前言
说起RecyclerView,大家的第一反应就是它是一个威力加强版的ListView,所以在很多时候,我们会对其造成一定的误解——什么?界面顿卡了?上RecyclerView!什么?组件出bug了?上RecyclerView!等等等等…….
首先我们还是先看看Google官方是如何定义RecyclerView的。
The RecyclerView widget is a more advanced and flexible version of ListView. This widget is a container for displaying large data sets that can be scrolled very efficiently by maintaining a limited number of views. Use the RecyclerView widget when you have data collections whose elements change at runtime based on user action or network events.
这段解释意境很清楚,我们确实可以把它看作一个[advanced]的ListView,但是这里我想说的是,千万不要把RecyclerView看成能和ListView[等价替换]的一个组件,更不要把它看做是拯救你滑动组件的救星。
使用过RecyclerView的同学都知道,这个组件是[比较难用]的,为什么这么说呢,我们来想想原来使用ListView的时候,好像一切都已经封装好了,一个api就能添加header和footer,点击事件有对应的listener等等。而RecyclerView好像什么都没有,没有点击事件,没有header和footer,甚至连最起码的divider都没有,什么都要自己写,用起来简直让人抓狂,好像它唯一的优点就是那个无法从代码中体现的[性能]了。
其实并不是这样的,这篇文章也是想让阐述这样的一个观点,让大家知道Google之所以这样做是有他们这样做的道理的。
首先,让我们从性能这一方面入手,看看两个组件在它们各自[回收机制]上有什么区别。
收回机制比较
首先先看ListView的,想要搞清楚回收机制,我们当然要去看它的onTouchEvent中对应的ACTION_MOVE方法。
ListView继承自AbsListView,所以其滑动逻辑会在AbsListView中处理,最终会走到trackMotionScroll这个方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | boolean (int deltaY, int incrementalDeltaY) { final int childCount = getChildCount(); if (childCount == 0) { return true; } final int firstTop = getChildAt(0).getTop(); final int lastBottom = getChildAt(childCount - 1).getBottom(); final Rect listPadding = mListPadding; // "effective padding" In this case is the amount of padding that affects // how much space should not be filled by items. If we don't clip to padding // there is no effective padding. int effectivePaddingTop = 0; int effectivePaddingBottom = 0; if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { effectivePaddingTop = listPadding.top; effectivePaddingBottom = listPadding.bottom; } // FIXME account for grid vertical spacing too? final int spaceAbove = effectivePaddingTop - firstTop; final int end = getHeight() - effectivePaddingBottom; final int spaceBelow = lastBottom - end; final int height = getHeight() - mPaddingBottom - mPaddingTop; if (deltaY < 0) { deltaY = Math.max(-(height - 1), deltaY); } else { deltaY = Math.min(height - 1, deltaY); } if (incrementalDeltaY < 0) { incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY); } else { incrementalDeltaY = Math.min(height - 1, incrementalDeltaY); } final int firstPosition = mFirstPosition; // Update our guesses for where the first and last views are if (firstPosition == 0) { mFirstPositionDistanceGuess = firstTop - listPadding.top; } else { mFirstPositionDistanceGuess += incrementalDeltaY; } if (firstPosition + childCount == mItemCount) { mLastPositionDistanceGuess = lastBottom + listPadding.bottom; } else { mLastPositionDistanceGuess += incrementalDeltaY; } final boolean cannotScrollDown = (firstPosition == 0 && firstTop >= listPadding.top && incrementalDeltaY >= 0); final boolean cannotScrollUp = (firstPosition + childCount == mItemCount && lastBottom <= getHeight() - listPadding.bottom && incrementalDeltaY <= 0); if (cannotScrollDown || cannotScrollUp) { return incrementalDeltaY != 0; } final boolean down = incrementalDeltaY < 0; final boolean inTouchMode = isInTouchMode(); if (inTouchMode) { hideSelector(); } final int headerViewsCount = getHeaderViewsCount(); final int footerViewsStart = mItemCount - getFooterViewsCount(); int start = 0; int count = 0; 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. 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. child.clearAccessibilityFocus(); mRecycler.addScrapView(child, position); } } } } mMotionViewNewTop = mMotionViewOriginalTop + deltaY; mBlockLayoutRequests = true; if (count > 0) { detachViewsFromParent(start, count); mRecycler.removeSkippedScrap(); } // invalidate before moving the children to avoid unnecessary invalidate // calls to bubble up from the children all the way to the top if (!awakenScrollBars()) { invalidate(); } offsetChildrenTopAndBottom(incrementalDeltaY); if (down) { mFirstPosition += count; } final int absIncrementalDeltaY = Math.abs(incrementalDeltaY); if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) { fillGap(down); } if (!inTouchMode && mSelectedPosition != INVALID_POSITION) { final int childIndex = mSelectedPosition - mFirstPosition; if (childIndex >= 0 && childIndex < getChildCount()) { positionSelector(mSelectedPosition, getChildAt(childIndex)); } } else if (mSelectorPosition != INVALID_POSITION) { final int childIndex = mSelectorPosition - mFirstPosition; if (childIndex >= 0 && childIndex < getChildCount()) { positionSelector(INVALID_POSITION, getChildAt(childIndex)); } } else { mSelectorRect.setEmpty(); } mBlockLayoutRequests = false; invokeOnItemScrollListener(); return false; } |
1 | final RecycleBin mRecycler = new RecycleBin(); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | class { ........ private ArrayList<View> mCurrentScrap; private ArrayList<View>[] mScrapViews; ........ void (View scrap, int position) { final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams(); if (lp == null) { return; } 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)) { 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<View>(); } 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<View>(); } mTransientStateViews.put(position, scrap); } else { // Otherwise, we'll have to remove the view and start over. if (mSkippedScrap == null) { mSkippedScrap = new ArrayList<View>(); } mSkippedScrap.add(scrap); } } else { if (mViewTypeCount == 1) { mCurrentScrap.add(scrap); } else { mScrapViews[viewType].add(scrap); } if (mRecyclerListener != null) { mRecyclerListener.onMovedToScrapHeap(scrap); } } } } |
1 2 3 4 5 | if (mViewTypeCount == 1) { mCurrentScrap.add(scrap); } else { mScrapViews[viewType].add(scrap); } |
现在我们可以知道,在AsbListView中,如果一个View已经滑出了屏幕,那么说明它已经被废弃(scrap)了,就会被添加到RecycleBin中(transient的View除外,这个忽略)。那么我们什么时候去取出这里面存储的View呢?让我们回到onTouchEvent函数。
我们可以看到在trackMotionScroll函数中有一个fillGap方法。
1 | abstract void fillGap(boolean down); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | void (boolean down) { final int count = getChildCount(); if (down) { 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 { 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()); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | View (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); } } // Scrap view implies temporary detachment. isScrap[0] = true; return transientView; } final View scrapView = mRecycler.getScrapView(position); final View child = mAdapter.getView(position, scrapView, this); if (scrapView != null) { if (child != scrapView) { // Failed to re-bind the data, return scrap to the heap. mRecycler.addScrapView(scrapView, position); } else { isScrap[0] = true; child.dispatchFinishTemporaryDetach(); } } 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; } |
接下去,让我们看看RecyclerView是怎么做的,在RecyclerView的ACTION_MOVE方法中,实际会调用其内部的LayoutManager的scrollBy方法去完成滑动,这里我们看LinearLayoutManager的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | int (int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { if (getChildCount() == 0 || dy == 0) { return 0; } mLayoutState.mRecycle = true; ensureLayoutState(); final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START; final int absDy = Math.abs(dy); updateLayoutState(layoutDirection, absDy, true, state); final int freeScroll = mLayoutState.mScrollingOffset; final int consumed = freeScroll + fill(recycler, mLayoutState, state, false); if (consumed < 0) { if (DEBUG) { Log.d(TAG, "Don't have any more elements to scroll"); } return 0; } final int scrolled = absDy > consumed ? layoutDirection * consumed : dy; mOrientationHelper.offsetChildren(-scrolled); if (DEBUG) { Log.d(TAG, "scroll req: " + dy + " scrolled: " + scrolled); } mLayoutState.mLastScrollDelta = scrolled; return scrolled; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public final class { .......... final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<ViewHolder>(); private ArrayList<ViewHolder> mChangedScrap = null; final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>(); private final List<ViewHolder> mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap); private int mViewCacheMax = DEFAULT_CACHE_SIZE; private RecycledViewPool mRecyclerPool; private ViewCacheExtension mViewCacheExtension; private static final int DEFAULT_CACHE_SIZE = 2; .......... |
从这里我们可以看出,RecyclerView提供了比AbsListView更加完善的回收机制,配以细节的优化和postOnAnimation方法所保证的”Android 16ms”机制,RecyclerView在滑动性能上确实会比AbsListView更出色。
但是虽然经过了优化,但是就像我前面说的RecyclerView并不是万能,对于一些非常复杂的item布局,一旦处理不好,RecyclerView所表现出来的性能也ListView是相差无几的,所以[使用RecyclerView代替ListView]并不仅仅应该体现在这里。
为什么要使用RV
在回答这个问题之前,让我们先思考另外一个问题,为什么在Google推出了RecyclerView并且经过了这么多个版本迭代之后,ListView没有被标明为deprecated呢?在ViewPager和HorizontalScrollView推出以后,Gallery就惨遭deprecated的命运,为毛ListView就没有呢?难道因为它是Google的亲儿子?
在我看来,RecyclerView虽然被标上了加强版AbsListView的标记,但是它们其实是两个不同的控件。AbsListView的子类们有着完善的功能,如果你的滑动组件只想简单的使用滑动显示这个功能,并且想轻松的使用divider,header,footer或者点击事件这些功能,那么使用AbsListView是完全没有问题的。
RecyclerView的重点应该放在[flexible]上,灵活是它[最大]的特点,由于AbsListView的功能完善,所以你想要定制它其实是很困难的,换句话说,AbsLisView已经强耦合了很多和[滑动,回收]无关的功能。这个时候RecyclerView的强大之处就显示出来了,LayoutManager,Adapter,ItemAnimator,ItemDecoration等等各司其职,这使得RecyclerView能够实现深度的定制化。系统提供的三种LayoutManager可以无缝衔接ListView和GridView,瀑布流的实现也变得无分简单。滑动删除和长按交换只需要添加几个类就可以实现。
除此之外,RecyclerView的动画配以局部刷新也是它比较出色的地方,在AbsListView时代,只有一个notifyDatasetChanged方法,想要做局部刷新需要自己去实现,动画更是难做,但是在RecyclerView中,有很多适配局部刷新的api,还有ItemAnimator这样的神器去支持动画,谁用谁知道。
至于点击事件,我在想Google将RecyclerView取名叫这个名字的原因就是想让这个组件只关注[Recycle],关于点击事件,在ViewHolder中添加是轻而易举的事,封装起来也不难,而且如果把这个逻辑写在组件内部,它的position和动画将会比较难处理,AbsListView里就花了比较多的精力去处理这一方面的逻辑。此外,在我们使用ListView的过程中,如果item中有可点击组件,例如button,那么点击事件的冲突也是一个让开发者很烦恼的事情,但是RecyclerView的好处就是把点击事件的控制权完全的交给开发者,避免了这样的痛苦。
最后,RecyclerView天生支持嵌套滑动,可以很好的配合NestedScrollView或者CoordinatorLayout,而AbsListView则是需要在一定的版本上才支持这个机制,这也算是RV的一个优势吧。
共同学习,写下你的评论
评论加载中...
作者其他优质文章