likes
comments
collection
share

【源码解析】ListView的工作原理

作者站长头像
站长
· 阅读数 3

一、引言

ListView组件,相信读者已经是非常熟悉了,所以本文就不再讲述如何使用ListView了,而是从源码的角度出发,去探寻ListView背后的绘制过程与缓存机制。

用到ListView,就不得不提Adapter。ListView采用了Adapter适配器模式,通过Adapter,ListView可以和数据进行交互,从而展现出各种丰富多样的列表内容。下图展示了ListView、Adapter、数据源之间的关系:

【源码解析】ListView的工作原理

ListView依赖Adapter,而Adapter直接和数据源进行交互,从而达到ListView间接操作数据源的效果。

二、源码解析-绘制流程

从上图可知ListView和Adapter的关系为依赖关系,我们在平时使用ListView时,都需要通过调用setAdapter方法将Adapter和ListView绑定起来,我们就从setAdapter方法入手进行源码解析。

@Override
public void setAdapter(ListAdapter adapter) {
    
    ...
    if (mAdapter != null) {
        ...
        mOldItemCount = mItemCount;
        mItemCount = mAdapter.getCount();
        checkFocus();

        mDataSetObserver = new AdapterDataSetObserver();
        mAdapter.registerDataSetObserver(mDataSetObserver);

        mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());

        int position;
        if (mStackFromBottom) {
            position = lookForSelectablePosition(mItemCount - 1, false);
        } else {
            position = lookForSelectablePosition(0, true);
        }
        ...
    } 
    ...

    requestLayout();
}

在setAdapter方法中,mOldItemCount 变量将保存旧列表的item个数,然后调用了RecycleBin的setViewTypeCount方法设置item的类别个数。最后通过requestLayout方法进行UI绘制。

@Override
public void requestLayout() {
    if (!mBlockLayoutRequests && !mInLayout) {
        super.requestLayout();
    }
}

我们在ListView的父类AbsListView中找到了requestLayout方法,该方法中继续调用了父类的requestLayout方法,从而跳转到了View的requestLayout方法。

public void requestLayout() {
    
    ...
    if (mParent != null && !mParent.isLayoutRequested()) {
        mParent.requestLayout();
    }
    if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
        mAttachInfo.mViewRequestingLayout = null;
    }
}

mParent是ViewParent类型,而ViewParent是一个接口,其实际上的类型为ViewRootImpl类型,所以这里调用的是ViewRootImpl的requestLayout方法。

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

通过scheduleTraversals方法,开始对View进行绘制。而绘制的过程就离不开三个步骤:

  • onMeasure方法:测量View的大小
  • onLayout方法:确定View的布局
  • onDraw方法:绘制View

因为onMeasure方法主要是测量View的大小,对于ListView来说,经常用到的是Match_Parent;而onDraw方法是将View绘制到屏幕的过程,这和普通View的绘制过程差不多,所以我们着重解析一下onLayout方法。

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    ...

    layoutChildren();

    mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;

    ...
    mInLayout = false;
}

ListView并没有onLayout方法,在其父类AbsListView找到,在onLayout方法中,调用了layoutChildren方法,该方法是一个空实现,具体的实现逻辑在子类中,于是我们又回到了ListView的layoutChildren方法。

@Override
protected void layoutChildren() {
    ...

    try {
       	...

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

        switch (mLayoutMode) {
        case LAYOUT_SET_SELECTION:
            ...
            break;
        case LAYOUT_SYNC:
            ...
            break;
        case LAYOUT_FORCE_BOTTOM:
            ...
            break;
        case LAYOUT_FORCE_TOP:
            ...
            break;
        case LAYOUT_SPECIFIC:
            ...
            break;
        case LAYOUT_MOVE_SELECTION:
            ...
            break;
        default:
            if (childCount == 0) {
                if (!mStackFromBottom) {
                    final int position = lookForSelectablePosition(0, true);
                    setSelectedPositionInt(position);
                    sel = fillFromTop(childrenTop);
                } else {
                    final int position = lookForSelectablePosition(mItemCount - 1, false);
                    setSelectedPositionInt(position);
                    sel = fillUp(mItemCount - 1, childrenBottom);
                }
            } else {
                if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
                    sel = fillSpecific(mSelectedPosition,
                            oldSel == null ? childrenTop : oldSel.getTop());
                } else if (mFirstPosition < mItemCount) {
                    sel = fillSpecific(mFirstPosition,
                            oldFirst == null ? childrenTop : oldFirst.getTop());
                } else {
                    sel = fillSpecific(0, childrenTop);
                }
            }
            break;
        }

        ...
    } finally {
        if (mFocusSelector != null) {
            mFocusSelector.onLayoutComplete();
        }
        if (!blockLayoutRequests) {
            mBlockLayoutRequests = false;
        }
    }
}

ListView的默认排列方式是从顶部开始往底部逐个item绘制的,所以最终会执行default的分支。第一次执行时ListView的item个数为0,也就是childCount == 0 成立,而mStackFromBottom变量表示ListView从底部开始往顶部绘制,因此这里是false,于是调用了fillFromTop方法,而fillFromTop方法又执行了fillDown方法。

private View fillDown(int pos, int nextTop) {
    View selectedView = null;

    int end = (mBottom - mTop);
    ...

    while (nextTop < end && pos < mItemCount) {
        boolean selected = pos == mSelectedPosition;
        View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);

        nextTop = child.getBottom() + mDividerHeight;
        if (selected) {
            selectedView = child;
        }
        pos++;
    }

    setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
    return selectedView;
}

fillDown方法中主要的逻辑是while循环,nextTop表示下一个item的Top坐标,end表示ListView的高度,pos表示当前item的下标值,mItemCount表示数据集中item的个数。

while循环的判断条件告诉我们,ListView不管数据集中有多少数据,即使是一千条、一万条,它都只会绘制最多一个屏幕的数据。

在while循环中,调用了makeAndAddView方法去获取一个子item对应的View。

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
        boolean selected) {
    if (!mDataChanged) {
        final View activeView = mRecycler.getActiveView(position);
        if (activeView != null) {
            setupChild(activeView, position, y, flow, childrenLeft, selected, true);
            return activeView;
        }
    }

    final View child = obtainView(position, mIsScrap);

    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

    return child;
}

当数据有变化时,mDataChanged的值才会为true,因此第一次执行时,这里mDataChanged的值为false,mRecyler.getActiveView方法表示返回一个屏幕上的View,由于是第一次执行,这里的返回值肯定为null,因此最终会执行obtainView方法。

View obtainView(int position, boolean[] outMetadata) {
    ...

    final View scrapView = mRecycler.getScrapView(position);
    final View child = mAdapter.getView(position, scrapView, this);
    if (scrapView != null) {
        if (child != scrapView) {            
            mRecycler.addScrapView(scrapView, position);
        } else if (child.isTemporarilyDetached()) {
            outMetadata[0] = true;

            child.dispatchFinishTemporaryDetach();
        }
    }

    ...
    return child;
}

obtainView中调用了mAdapter的getView方法,这个方法我们已经很熟悉了。getView方法返回的是ListView中item对应的View。

在obtainView中返回了item对应的View后,在makeAndAddView方法中就调用了setupChild方法。

private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
        boolean selected, boolean isAttachedToWindow) {
  	...

    if ((isAttachedToWindow && !p.forceAdd) || (p.recycledHeaderFooter
            && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
        attachViewToParent(child, flowDown ? -1 : 0, p);

        // If the view was previously attached for a different position,
        // then manually jump the drawables.
        if (isAttachedToWindow
                && (((AbsListView.LayoutParams) child.getLayoutParams()).scrappedFromPosition)
                        != position) {
            child.jumpDrawablesToCurrentState();
        }
    } else {
        p.forceAdd = false;
        if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
            p.recycledHeaderFooter = true;
        }
        addViewInLayout(child, flowDown ? -1 : 0, p, true);
        // add view in layout will reset the RTL properties. We have to re-resolve them
        child.resolveRtlPropertiesIfNeeded();
    }

    ...
}

在setupChild方法中,调用了addViewInLayout方法将ListView中item对应的View添加到了ListView中。前面说过,在fillDown方法的while循环中,在item的View将ListView控件填满后则会跳出循环,当我们滚动屏幕时,屏幕外的item才会被绘制并展示到屏幕中,而之前在屏幕内的item则被移除出去,这就涉及到了ListView的一个重要机制——缓存机制。

三、源码解析-缓存机制

上节分析了ListView的绘制流程,本节将分析ListView的缓存机制,正因为有了缓存机制,ListView才能流畅地将数据展示出来。

ListView本质上也是一个View,其中最重要的莫过于ListView的onTouchEvent方法,ListView的滑动过程就是从执行onTouchEvent开始的。

@Override
public boolean onTouchEvent(MotionEvent ev) {
    ...
    switch (actionMasked) {
        case MotionEvent.ACTION_DOWN: {
            onTouchDown(ev);
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            onTouchMove(ev, vtev);
            break;
        }
       	...  
    }
    ...
    return true;
}

onTouchEvent方法中,我们重点关注Motion.ACTION_MOVE分支,其中执行了onTouchMove方法。

private void onTouchMove(MotionEvent ev, MotionEvent vtev) {
    ...
    switch (mTouchMode) {
        case TOUCH_MODE_DOWN:
        case TOUCH_MODE_TAP:
        case TOUCH_MODE_DONE_WAITING:
            ...
            break;
        case TOUCH_MODE_SCROLL:
        case TOUCH_MODE_OVERSCROLL:
            scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev);
            break;
    }
}

在该方法中我们着重关注TOUCH_MODE_SCROLL分支,其中执行了scrollIfNeeded方法。当滑动ListView时,incrementalDeltaY不等于0。

if (incrementalDeltaY != 0) {
    atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
}

所以在scrollIfNeeded方法中又执行了trackMotionScroll方法。

boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
    ...

    if (incrementalDeltaY < 0) {
        incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
    } else {
        incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
    }
    ...

    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) {
                    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) {
                    child.clearAccessibilityFocus();
                    mRecycler.addScrapView(child, position);
                }
            }
        }
    }

    ...

    if (count > 0) {
        detachViewsFromParent(start, count);
        mRecycler.removeSkippedScrap();
    }

    ...

    final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
    if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
        fillGap(down);
    }
    ...
    return false;
}

incrementalDeltaY表示手指距离上次坐标点竖直方向上的距离。当ListView向下滑动时,手指在竖直方向的坐标值不断减小,因此incrementalDeltaY为负值;当ListView向上滑动时,手指在竖直方向的坐标值不断增大,因此incrementalDeltaY为正值。

down等于true时,ListView向下滑动,循环遍历所有ListView的所有子View。当子View底部距离ListView顶部的距离(child.getBottom)小于滑动的Delta距离(top),则表示该子View移出了屏幕。 down等于false时,ListView向上滑动,循环遍历所有ListView的所有子View。当子View的顶部距离ListView顶部的距离(child.getTop)大于ListView的高度减去Delta值(bottom),则表示该子View移出了屏幕。

移出屏幕的子View都调用了RecycleBin的addScrapView方法。

void addScrapView(View scrap, int position) {
    ...
    if (scrapHasTransientState) {
        ...
    } else {
        clearScrapForRebind(scrap);
        if (mViewTypeCount == 1) {
            mCurrentScrap.add(scrap);
        } else {
            mScrapViews[viewType].add(scrap);
        }
    }
}

addScrapView方法通过mCurrentScrap和mScrapViews将View加入到废弃View的列表中,以便后续能够快速获取并恢复。 调用addScrapView方法后,然后调用了detachViewsFromParent方法,该方法将上面刚移出屏幕的View从ListView中detach掉。

然后进行判断,如果ListView的第一个View的底部移入了屏幕或者最后一个View的顶部移入了屏幕,则调用fillGap方法。从该方法名也可以看出,该方法是将屏幕外的数据渲染成Item并移入到ListView中。fillGap方法是一个抽象方法,具体实现在ListView中。

void fillGap(boolean down) {
    final int count = getChildCount();
    if (down) {
        int paddingTop = 0;
        ...
        fillDown(mFirstPosition + count, startOffset);
    } else {
        ...
        fillUp(mFirstPosition - 1, startOffset);
    }
}

其中调用了fillDown和fillUp方法,而这两个方法最终都会调用makeAndAddView方法。

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
        boolean selected) {
    if (!mDataChanged) {
        final View activeView = mRecycler.getActiveView(position);
        if (activeView != null) {
            setupChild(activeView, position, y, flow, childrenLeft, selected, true);
            return activeView;
        }
    }
    final View child = obtainView(position, mIsScrap);
    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
    return child;
}

由于该Item是从屏幕外移入到ListView的,所以mRecyler.getActiveView返回为null,因此接下来会执行obtainView方法。

View obtainView(int position, boolean[] outMetadata) {
    ...
    final View scrapView = mRecycler.getScrapView(position);
    final View child = mAdapter.getView(position, scrapView, this);
    if (scrapView != null) {
        if (child != scrapView) {
            mRecycler.addScrapView(scrapView, position);
        } else if (child.isTemporarilyDetached()) {
            outMetadata[0] = true;
            child.dispatchFinishTemporaryDetach();
        }
    }
    ...
    return child;
}

obtainView方法中,通过调用mRecyler.getScrapView方法从废弃的View列表中获取一个scrapView,然后将该scrapView作为参数传递给Adapter的getView方法。Adapter的getView方法我们都很熟悉了,一般我们会在getView方法中将新数据重新赋值给scrapView,而getView的返回结果child就是移入到ListView的新Item。

四、总结

ListView的渲染过程最重要的是onLayout过程,其中第一次渲染由于缓存列表中没有缓存View,所以第一次调用的getView是新生成的,后续通过滑动ListView,RecyclerBin生成了缓存列表,之后可以从缓存列表中获取View,避免了重复创建View的过程,从而提供了性能。下面用一张图来总结一下ListView的缓存原理。

【源码解析】ListView的工作原理