转载

我们为什么要使用RecyclerView

前两天看到葱花同学的文章 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这个方法。

boolean trackMotionScroll(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;
}

逻辑非常的多,我们看主要的。这个方法的两个入参scrollY表示从开始滑动的时候到当前滑动的y轴的距离,incrementalDeltaY表示滑动时y轴的增量值,这个值可以通过判断正负来确定是向上滑还是向下滑。这里我们以一个方向为准,如果整个滑动是向下滑的,也就是局部变量down为true,那么在这种情况下,如果child.getBottom() < top,也就是说bottom值小于top值,那么说明在向下滑动这种场景下,这个child已经滑出屏幕不在显示范围内了,于是,AbsListView就调用了mRecycler.addScrapView(child, position);这个函数将其加入到了mRecycler中。这里有一个很重要的点,就是mRecycler,它也是AbsListView和它的子类(ListView GridView等等)能正常运作最主要原因。

final RecycleBin mRecycler = new RecycleBin();

可以看到它其实是一个RecycleBin对象。

class RecycleBin {

........
private ArrayList<View> mCurrentScrap;
private ArrayList<View>[] mScrapViews;
........

void addScrapView(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);
}
}
}
}

可以看到刚刚说起的那个addScrapView方法,其中有一段方法:

if (mViewTypeCount == 1) {
mCurrentScrap.add(scrap);
} else {
mScrapViews[viewType].add(scrap);
}

根据ViewTypeCount去添加已经废弃的View。从这个方法就可以说明,ListView的回收机制是和ViewType有关的,每一个ViewType对应一个对应的废弃list。

现在我们可以知道,在AsbListView中,如果一个View已经滑出了屏幕,那么说明它已经被废弃(scrap)了,就会被添加到RecycleBin中(transient的View除外,这个忽略)。那么我们什么时候去取出这里面存储的View呢?让我们回到onTouchEvent函数。

我们可以看到在trackMotionScroll函数中有一个fillGap方法。

abstract void fillGap(boolean down);

这是一个抽象方法,这是合理的,因为AbsListView的子类显示情况都不一样,ListView有ListView的显示,GridView有GridView的显示,所以需要它们自己去判断。

void fillGap(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());
}
}

在这个函数中,通过down这个参数判断滑动方向,如果向下滑,则调用fillDown方法,否则调用fillUp方法。而在这两个方法中,都会去调用obtainView方法从对应的RecycleBin中取出scrapView填充到AbsListView中。

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);
}
}

// 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;
}

这下我们就清楚了,原来通过RecycleBin这样一个东西,就算有再多的item,AbsListView中永远只会存在那么几个,滑出屏幕的child回收,然后再重用,于是就尽可能的避免了oom的发生。

接下去,让我们看看RecyclerView是怎么做的,在RecyclerView的ACTION_MOVE方法中,实际会调用其内部的LayoutManager的scrollBy方法去完成滑动,这里我们看LinearLayoutManager的实现。

int scrollBy(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;
}

其中的fill方法最终会调用到layoutChunk方法,而在这个方法中,会去recycler中取废弃的View进行填充。说到这儿大家可以发现,其实RecyclerView和AbsListView的回收机制在大致框架上是一致的,就是通过一个类似Recycler的回收器去存储和获取废弃的View,下面让我们看看RecyclerView的回收器。

public final class Recycler {

..........

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;

..........

可以看到在Recycler中存这么多的List,而在获取View的过程中,Recycler会先从scrap的List中获取,如果没有获取到就从cache中获取。另外还有ViewCacheExtension和RecycledViewPool这两个额外的选项需要开发者手动去设置。

从这里我们可以看出,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的一个优势吧。

原文  http://zjutkz.net/2016/08/10/我们为什么要使用RecyclerView/
正文到此结束
Loading...