ListView工作原理详细分析(一)

时间:2022-04-06 15:55:26

转发请标明转载地址:http://blog.csdn.net/coder_nice/article/details/54908216

引言

ListView是Android系统中最常用也是最复杂的原生控件,了解ListView的工作原理非常必要。

ListView加载非常多数据的时候,上下滑动也不会造成oom等问题,随着手指的滑动,屏幕中会移除和新增View,无缝衔接这个过程,并且不会增加更多的内存消耗,复用是listview最核心的原理。

在研究ListView源码的时候,郭神的文章ListView工作原理给我很大的帮助,在此感谢郭神。

正文

想了解ListView的工作原理,一定需要了解AdapterView,AbsListView,Adapter,ListView,RecycleBin五个部分。

ListView工作原理详细分析(一)

可以看得到AdapterView是ViewGroup以下的最高一级,然后是AbsListView,最后才是ListView。

Adapter中文解释为适配器,生活中你一定经常听到电源适配器,通过电源适配器就很直观的理解适配器就是把让原本不能相连的两端通过适配器相连起来。
而ListView想和数据源链接起来,就需要Adapter充当适配器的角色。

RecycleBin是AbsListView的内部类,也是复用的控制中心,正是RecycleBin的存在让ListView有了复用的能力。

AdapterView

继承关系

ListView工作原理详细分析(一)

从AdapterView继承关系图可以看的出,AdapterView是所有使用适配器的View统一的父类。

作用

ListView工作原理详细分析(一)

如果你想使用Adapter来做数据与View之间的桥梁,那View一定是AdapterView的子类。

所以AdapterView就是专门为了Adapter适配器,基于ViewGroup封装的一个抽象类。

具体实现

AdapterView中根据Adapter,抽象出了一些共用的属性和方法。

  1. setAdapter()方法就是一个抽象方法,因为不同子类中setAdapter()方法中实现和逻辑有很多差别,在AbsListView子类中,setAdapter()接收的都是ListAdapter,而在 AbsSpinner的子类中接收的对象都是SpinnerAdapter,并且不同的子类中setAdapter()也相差很多,所以setAdapter()做成抽象的方法就很好理解了。
  2. AdapterView中对addView和removeView做了抛出异常的处理,因为AdapterView的子类中,child view的增加和减少全部依赖于数据源,随着数据源的变化而变化,不能直接调用addView和removeView来改变child view的数量。
  3. AdapterView中记录了child view的数量和当前屏幕显示的第一个和最后一个view的位置,实现了OnItemClickListener等监听接口。
  4. 还有一些关于选中的属性和方法,就不再过多解释,AdapterView类本身代码不多,而且比较简洁,大家可以自行阅读源码。

AbsListView

继承关系

ListView工作原理详细分析(一)

从继承关系可以看出,AbsListView是所有列表View的统一父类,常见的子类就是GridView和ListView。

作用

ListView工作原理详细分析(一)

AdapterView抽象封装的是所有使用适配器的view共用的属性和方法,AdapterView有三种使用情景,分别对应三个直接的子类,AbsListView, AbsSpinner, AdapterViewAnimator。

ListView工作原理详细分析(一)

AbsSpinner针对的是从多个选项中选择一个,AdapterViewAnimator针对的是多个view之间切换时自定义动画,而AbsListView专门针对列表展示数据内容的这种使用情景,又进一步做了封装和抽象。

实现

GridView和ListView这种有多个item,可复用的列表类View,最核心的逻辑都是在AbsListView中实现的,GridView和ListView其实从原理上看是基本一致的,只是布局上一个是列表,一个是网格。复用、滑动、处理数据变化等最重要最核心的,也是GridView和ListView共用的逻辑部分,都在AbsListView中实现的。

ListView工作原理详细分析(一)

可以看的出AbsListView内部结构和实现的逻辑是比较复杂的,本篇着重讲关乎工作原理的几个方法,其他的如点击事件、选中状态等方法就不再做解释,感兴趣的同学可以自行查看阅读源码。

onLayout()方法

  protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
mInLayout = true;
final int childCount = getChildCount();
if (changed) {
for (int i = 0; i < childCount; i++) {
getChildAt(i).forceLayout();
}
mRecycler.markChildrenDirty();
}
layoutChildren();
mInLayout = false;
mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;
// TODO: Move somewhere sane. This doesn't belong in onLayout().
if (mFastScroll != null) {
mFastScroll.onItemCountChanged(getChildCount(), mItemCount);
}
}
  • 在第4行判断屏幕是否发生改变,如果发生改变强制所有子view重新布局,mRecycler是RecycleBin的一个引用,markChildrenDirty()方法中也是强制所有在缓冲池中的子view强制重新进行onLayout过程,RecycleBin是AbsListView的一个子类,是实现复用的核心类,后面讲到RecycleBin时再一进步说这个类。

  • 在第11行,调用了 layoutChildren()方法,此方法在AbsListView中是一个protected的空实现,具体实现都在子类中,后面讲到ListView时再进一步说这个方法。

handleDataChanged() 方法

handleDataChanged() 方法做了许多重置的工作,包括重置选择的位置和行数,选中的id等,而与工作原理相关的是清掉了RecycleBin中的缓冲池,其他代码就先忽略了,只看清掉缓冲池的代码。

// TODO: In the future we can recycle these views based on stable ID instead.
mRecycler.clearTransientStateViews();

这里先不展开说明这句代码具体做了什么事情,你可能会感到懵逼,先不要急,因为ListView稍复杂一些,而且关乎好几个类,所以只有在全部类都解释完之后,才能有一个总体的理解,这里你只需要记得在AbsListView中的handleDataChanged()调用时清掉了缓冲池的子View即可。

obtainView()方法

obtainView()这个方法用郭神的话来说就是:非常非常重要的逻辑,不夸张的说,整个ListView中最重要的内容可能就在这个方法里了。

概括来说,就是AbsListView的子类在构建视图时,每一个子View都是从obtainView()中获取到的子View。

  • 创建第一屏子View时,obtainView()会调用Adapter中的getView()方法创建子View。

  • 随后滑动导致的新的子View是obtainView()从RecycleBin的缓冲池中获取的可复用的子View。

 /**
* Get a view and have it show the data associated with the specified
* position. This is called when we have already discovered that the view is
* not available for reuse in the recycle bin. The only choices left are
* converting an old view or making a new one.
*
* @param position The position to display
* @param isScrap Array of at least 1 boolean, the first entry will become true if
* the returned view was taken from the scrap heap, false if otherwise.
*
* @return A view displaying the data associated with the specified position
*/

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);
}
}
isScrap[0] = true;
// Finish the temporary detach started in addScrapView().
transientView.dispatchFinishTemporaryDetach();
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;
// Finish the temporary detach started in addScrapView().
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;
}
  • 18到34行,查看是否有过度状态的子view,如果有瞬态的子view,在viewtype没有发生改变的情况下,就会尝试把这个子view传递会Adapter的getView()方法中重新去绑定数据,如果绑定失败,就会把getView返回的子view缓存到scrap池中去。

  • 35行到46行,使用是scrap池来实现复用的。35行的scrapView在最初是null,因为还没有创建第一屏view时缓冲池中也全是空的。当35行返回的scrapView是空时,36行传递给getView中第二个参数也是null,而第二个参数正是convertView,当convertView为空时,就是从LayoutInflater中Inflater进来一个新的view。当35行的scrapView不为空时,此时的convertView也就不为空,如果viewtype没有发生改变,此时的convertView就可以直接复用了。

  • 在老版本的AbaListView中,RecycleBin只有两级缓存,activeView,scrapView,新版本中又增加了transientView,已经有了三层缓存。根据优先使用的原则大致是这样一个排序,activeView》transientView》scrapView,activeView在obtainView没有体现,后面会提到,这里先不做介绍,具体的复用逻辑也在后面再具体讲述。

onTouchEvent等一系列有关滑动的方法

真正出现复用,是因为滑动导致有些View滑出的屏幕,一些新的数据项出现在屏幕上,新的数据项出现在屏幕上没有再次Inflater,而是复用了之前滑出屏幕的view,这样才完成了复用。

所以关于复用的触发和处理都是在滑动事件中实现的,AbsListView中关于滑动事件处理的代码非常多,各种触屏选中动作的处理,每次看的我也是晕晕绕绕的。

如果只挑跟滑动事件、listview工作原理相关的代码来看,就要清晰了很多,大致是这样一个逻辑。

onTouchEvent > MotionEvent.ACTION_MOVE > onTouchMove > TOUCH_MODE_SCROLL > scrollIfNeeded > trackMotionScroll()

所以我们就重点关注trackMotionScroll()方法就可以了

先大致描述一下trackMotionScroll()方法的作用,关于滑出屏幕,删除,滑入屏幕,复用的逻辑都在这里了。

当手指滑动页面上下滑动时,会循环遍历检测每一个view的边界和listview的边界,有滑出屏幕的view就会加入到scrapView中去,然后listview会把已经滑出屏幕的view从ViewGroup中删除,释放内存,经过滑动后还在屏幕上的view会根据手指滑动的距离发生偏移,达到view跟着手指滑动的目的,然后判断是否有新的view的边界滑进了屏幕,如果有,从scrapView中获取一个子view,传递给Adapter的getView()中去,重新绑定新数据,再次显示到屏幕上。

 /**
* Track a motion scroll
*
* @param deltaY Amount to offset mMotionView. This is the accumulated delta since the motion
* began. Positive numbers mean the user's finger is moving down the screen.
* @param incrementalDeltaY Change in deltaY from the previous event.
* @return true if we're already at the beginning/end of the list and have nothing to do.
*/

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;
}
  • deltaY表示从手指按下时的位置到当前手指位置的距离,距离一定是正值,incrementalDeltaY则表示据上次触发event事件手指在Y方向上位置的改变量,屏幕左上角是(0,0)点,越往屏幕下方移动,Y越大,所以可以通过incrementalDeltaY的正负值情况来判断用户是向上还是向下滑动的了,如果incrementalDeltaY小于0,说明是向下滑动,否则就是向上滑动。

  • 82行以前,分别在计算偏移量(滑动的距离)incrementalDeltaY,滑动方向down,屏幕上第一个view的上边界和最后一个view的下边界。

  • 84行到125行,在判断滑动方向后,根据滑动距离,循环每一个子view判断是否滑出屏幕。向上滑动时,判断每一个view的底部边界和top相比,如果view的底部边界小于top,则已经滑出屏幕,回收这个子view到缓冲池;向下滑动时,判断每一个子view的顶部边界和bottom相比,如果view的顶部边界大于bottom,则已经滑出屏幕,回收这个子view到缓冲池。

  • 这里的down这个变量的名字有点坑,其实dowm不是只向下的意思,如果从滑动方向来看,down=true时是向上滑动。其实这里的dowm指的是遍历的方向向下,从上到下遍历,或者也可以理解成down=true时是向上滑动。郭神的博客对这里的解释是错的,我也是迷茫好久,各方求证才明白是这样的。

也许你不太信dowm居然不是指滑动向下,那我们这里解释一下为什么。

final boolean down = incrementalDeltaY < 0

当incrementalDeltaY小于0时,dowm为正,incrementalDeltaY表示滑动前后在Y方向的差值,如果incrementalDeltaY=100-200,那么incrementalDeltaY就是-100,此时dowm就为正了,100是终点,200是起点,而左上角为(0,0)原点,越往下Y值越大,所以从200滑动到100一定是向上滑动。

  • 131行到134行,如果count>0,说明有view滑出屏幕,132行把已经滑出屏幕的view从屏幕上移除,133行清空skipped缓冲池。

  • 142行,把在Y方向移动的差值传递给ViewGroup,对还存在屏幕上的所有view做偏移,达到view随着手指滑动跟滑动的效果。

  • 150行,把屏幕上空余的空间补充上新的view,达到随着手指的滑动,从屏幕的顶部和底部滑动出新的view的效果。fillGap()是一个抽象的方法,具体在ListView或者GridView中实现,因为ListView和GridView布局的不同,新出现的view所呈现的方式也不同,所以需要不同的实现。

到这里AbsListVIew就讲述完了,先到这里,ListView实现会在下一篇讲。

希望能帮到你。