Android源码分析之ListView源码

系列文章

  1. Android源码分析之ListView源码
  2. Android源码分析之RecyclerView源码分析(一)——绘制流程
  3. Android源码分析之RecyclerView源码分析(二)——缓存机制

前言

ListView 是用来展示大量数据的控件,且不会因为展示大量数据而出现内存溢出的现象,其原因是相关缓存机制保证了内存的合理使用。

ListView的使用也相对比较简单,大家也都会,现在官方基本都推荐使用RecyclerView去替代ListView,二者之间有相似之处,也有不同之处,本文先分析ListView的源码,重点是缓存的实现原理,后续再补充RecyclerView的原理分析,并将二者进行对比讨论。

ListView的使用可以参考ListView简单实用

ListView继承自AbsListViewAbsListView又继承自AdapterView,AdapterView继承自ViewGroup

ListView继承关系.png

RecycleBin机制

在ListView的缓存机制中,有一个类我们必须提前了解:RecycleBin,它是ListView缓存的核心机制。RecycleBin是AbsListView的一个内部类,而ListView继承AbsListView,所以ListView可以使用这个机制。下面是RecycleBin的部分关键源码:

class RecycleBin {
        private RecyclerListener mRecyclerListener;

        private int mFirstActivePosition;
        
        // 存储View
        private View[] mActiveViews = new View[0];
        
        // 存储废弃View
        private ArrayList[] mScrapViews;

        private int mViewTypeCount;
        
        // 存储废弃View
        private ArrayList mCurrentScrap;
        
        // Adapter当中可以重写一个getViewTypeCount()来表示ListView中有几种类型的数据项
        // 而setViewTypeCount()方法的作用就是为每种类型的数据项都单独启用一个RecycleBin缓存机制
        public void setViewTypeCount(int viewTypeCount) {
            if (viewTypeCount < 1) {
                throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
            }
            //noinspection unchecked
            ArrayList[] scrapViews = new ArrayList[viewTypeCount];
            for (int i = 0; i < viewTypeCount; i++) {
                scrapViews[i] = new ArrayList();
            }
            mViewTypeCount = viewTypeCount;
            mCurrentScrap = scrapViews[0];
            mScrapViews = scrapViews;
        }
        
        // 第一个参数表示要存储的view的数量,第二个参数表示ListView中第一个可见元素的position值
        // 根据传入的参数来将ListView中的指定元素存储到mActiveViews数组当中。
        void fillActiveViews(int childCount, int firstActivePosition) {
            if (mActiveViews.length < childCount) {
                mActiveViews = new View[childCount];
            }
            mFirstActivePosition = firstActivePosition;

            final View[] activeViews = mActiveViews;
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);
                AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
                if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                    activeViews[i] = child;
                    lp.scrappedFromPosition = firstActivePosition + i;
                }
            }
        }
        
        // 从mActiveViews数组当中获取数据
        // 该方法接收一个position参数,表示元素在ListView当中的位置
        // 方法内部会自动将position值转换成mActiveViews数组对应的下标值
        // mActiveViews当中所存储的View,一旦被获取了之后就会从mActiveViews当中移除
        // 下次获取同样位置的View将会返回null,也就是说mActiveViews不能被重复利用
        View getActiveView(int position) {
            int index = position - mFirstActivePosition;
            final View[] activeViews = mActiveViews;
            if (index >=0 && index < activeViews.length) {
                final View match = activeViews[index];
                activeViews[index] = null;
                return match;
            }
            return null;
        }
        
        // 从废弃缓存中取出一个View,这些废弃缓存中的View是没有顺序可言的
        // 因此getScrapView()方法中的算法也非常简单
        // 就是直接从mCurrentScrap当中获取尾部的一个scrap view进行返回
        View getScrapView(int position) {
            final int whichScrap = mAdapter.getItemViewType(position);
            if (whichScrap < 0) {
                return null;
            }
            if (mViewTypeCount == 1) {
                return retrieveFromScrap(mCurrentScrap, position);
            } else if (whichScrap < mScrapViews.length) {
                return retrieveFromScrap(mScrapViews[whichScrap], position);
            }
            return null;
        }

        // 将一个废弃的View进行缓存
        // 该方法接收一个View参数,当有某个View确定要废弃掉的时候(比如滚动出了屏幕)
        // 应该调用这个方法来对View进行缓存
        void addScrapView(View scrap, int position) {
            final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
            if (lp == null) {
                return;
            }

            lp.scrappedFromPosition = position;

            final int viewType = lp.viewType;
            if (!shouldRecycleViewType(viewType)) {
                if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                    getSkippedScrap().add(scrap);
                }
                return;
            }

            scrap.dispatchStartTemporaryDetach();

            notifyViewAccessibilityStateChangedIfNeeded(
                    AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);

            final boolean scrapHasTransientState = scrap.hasTransientState();
            if (scrapHasTransientState) {
                if (mAdapter != null && mAdapterHasStableIds) {
                    if (mTransientStateViewsById == null) {
                        mTransientStateViewsById = new LongSparseArray<>();
                    }
                    mTransientStateViewsById.put(lp.itemId, scrap);
                } else if (!mDataChanged) {
                    if (mTransientStateViews == null) {
                        mTransientStateViews = new SparseArray<>();
                    }
                    mTransientStateViews.put(position, scrap);
                } else {
                    getSkippedScrap().add(scrap);
                }
            } else {
                if (mViewTypeCount == 1) {
                    mCurrentScrap.add(scrap);
                } else {
                    mScrapViews[viewType].add(scrap);
                }

                if (mRecyclerListener != null) {
                    mRecyclerListener.onMovedToScrapHeap(scrap);
                }
            }
        }
    }

下面就上述几个关键变量和方法做一些说明:

  • mActiveViews:用来存放正在展示在屏幕上的view,从显示在屏幕山上的第一个view到最后一个view
  • mScrapViews存放可以由适配器用作convert view的view,是一个数组,数组的每个元素类型为ArrayList
  • mCurrentScrap是mScrapViews的第0个元素,当view种类数量为1时存放废弃view
  • fillActiveViews():这个方法接收两个参数,第一个参数表示mActiveViews数组最小要保存的View数量,第二个参数表示ListView中第一个可见元素的position值。根据传入的参数来将ListView中的指定元素存储到mActiveViews数组当中。
  • getActiveView()从mActiveViews数组当中取出特定元素,position参数表示元素在ListView当中的位置,方法内部会自动将position值转换成mActiveViews数组对应的下标值,mActiveViews当中所存储的View,一旦被获取了之后就会从mActiveViews当中移除,下次获取同样位置的View将会返回null,也就是说mActiveViews不能被重复利用。如果在mActiveViews数组中没有找到,则返回null。
  • addScrapView()将一个废弃的View进行缓存,该方法接收一个View参数,当有某个View确定要废弃掉的时候(比如滚动出了屏幕),应该调用这个方法来对View进行缓存,当view类型为1时则用mCurrentScrap存储废弃view,否则使用mScrapViews添加废弃view。
  • getScrapView(): 从废弃缓存中取出一个View,这些废弃缓存中的View是没有顺序可言,就是直接从mCurrentScrap当中获取尾部的一个scrap view进行返回
  • setViewTypeCount():Adapter当中可以重写一个getViewTypeCount()来表示ListView中有几种类型的数据项,而setViewTypeCount()方法的作用就是为每种类型的数据项都单独启用一个RecycleBin缓存机制

RecycleBin类的核心方法和变量的解释暂且放在此处,后续讲解时会用到此处信息。

ListView的绘制流程

ListView本质上还是一个View,因此绘制的过程还是分为三步:onMeasure、onLayout、onDraw,onMeasure测出其占用屏幕空间,最大为整个屏幕,而onDraw用于将ListView内容绘制到屏幕上,在ListView中无实际意义,因为ListView本身只是提供了一种布局方式,真正的绘制是ListView中的子View完成的,因此onLayout方法是最为关键的。

onLayout方法

ListView的OnLayout实现在AbsListView中,具体源码如下:

@Override
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;
    
    ...
}

从上面代码可以看出,首先调用了父类的onLayout方法,再判断ListView是否发生了变化(大小,位置),如果ListView发生了变化,则changed变量为truechanged为true则强制每个子布局都进行重新绘制,还需要注意一点的是同时还进行了mRecycler.markChildrenDirty()这个操作,其中mRecycler就是一个RecycleBin的对象,而markChildrenDirty()方法会为每一个scrap view调用forceLayout();判断完changed变量后又调用了layoutChildren()方法,点开此方法可以发现他是一个空方法,因为每个子元素的布局实现应该由自己来实现,所以它的具体实现在ListView中。

layoutChildren方法

 @Override
 protected void layoutChildren() {
    ...
    
    final int childCount = getChildCount();
    
    ...
    
    boolean dataChanged = mDataChanged;
      
    ...
    
    if (dataChanged) {
        for (int i = 0; i < childCount; i++) {
            recycleBin.addScrapView(getChildAt(i), firstPosition+i);
        }
    } else {
        recycleBin.fillActiveViews(childCount, firstPosition);
    }

    ...
    
    switch (mLayoutMode) {
        ...
        
        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;
    }

    ...
    
 }

它的方法过长,只贴出来一部分,此方法中首先会获取子元素的数量,但此时ListView中还没有任何子View,因为数据都是由Adapter管理的,还没有展示到界面上。接着又会判断dataChanged这个值,如果数据源发生变化则该值变为true,紧接着调用了RecycleBin的fillActiveViews()方法;可是这时ListView中还没有子View,因此fillActiveViews的缓存功能无法起作用。

那么我们再接着往下分析,接下来又会判断mLayoutMode的值,默认情况下该值都是LAYOUT_NORMAL,此模式下会直接进入default语句中,其中有多次if条件判断,因为当前ListView中还没有任何子View所以当前childCount数量为0,mStackFromBottom变量代表的是布局的顺序,默认的布局顺序是从上至下,因此会进入fillFromTop方法中

fillFromTop方法

此方法具体代码如下:

private View fillFromTop(int nextTop) {
    mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
    mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
    if (mFirstPosition < 0) {
        mFirstPosition = 0;
    }
    return fillDown(mFirstPosition, nextTop);
}

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

    int end = (mBottom - mTop);
    if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
        end -= mListPadding.bottom;
    }

    while (nextTop < end && pos < mItemCount) {
        // is this the selected item?
        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;
}

fillFromTop首先计算出了mFirstPosition的值,并从mFirstPosition开始自顶至下调用fillDown填充。

fillDown中采用了while循环来填充,一开始时nextTop的值是第一个子元素顶部距离整个ListView顶部的像素值pos是传入的mFirstPosition的值,end是ListView底部减去顶部所得的像素值mItemCount是Adapter中的元素数量,因此nextTop是小于end的,pos也小于mItemCount,每次执行while循环时,pos加1,nextTop也会累加当nextTop大于end时,也就是子元素超出屏幕了,或者pos大于mItemCount时,即Adapter中所有元素都被遍历了,出现以上两种情况中一种便会跳出while循环。

在此while循环中,我们注意到调用了makeAddView这个方法,下面具体分析下。

makeAddView方法

首先贴下代码:

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
        boolean selected) {
    if (!mDataChanged) {
        // Try to use an existing view for this position.
        final View activeView = mRecycler.getActiveView(position);
        if (activeView != null) {
            // Found it. We're reusing an existing child, so it just needs
            // to be positioned like a scrap view.
            setupChild(activeView, position, y, flow, childrenLeft, selected, true);
            return activeView;
        }
    }

    // Make a new view for this position, or convert an unused view if
    // possible.
    final View child = obtainView(position, mIsScrap);

    // This needs to be positioned and measured.
    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

    return child;
}

Adapter的数据源未发生变化时,会从RecycleBin中获取一个activeView,但是目前RecycleBin中还没有缓存任何的View,因此这里得到的child为null,接着又调用了obtainView方法来获取一个View,再来看看这个方法吧,源码如下:

obtainView方法

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

    outMetadata[0] = false;
    
    ...
    
    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 if (child.isTemporarilyDetached()) {
            outMetadata[0] = true;

            // Finish the temporary detach started in addScrapView().
            child.dispatchFinishTemporaryDetach();
        }
    }

    ...
    
    return child;
}

首先调用了RecycleBingetScrapView方法来尝试获取一个废弃缓存中的View,但是这里是获取不到的;接着又调用了getView方法,即自定的Adapter中的getView方法,getView方法接收三个参数,第一个是当前子元素位置,第二个参数是convertView上面传入的是null,说明没有covertView可以利用,因此在Adapter中判断convertView为null时可以调用LayoutInflater的inflate方法去加载一个布局,并将此view返回。

同时我们可以看到,这个view最终也会作为obtainView方法的返回结果,并传入makeAddView方法中后续调用setupChild()方法中,上面过程可以说明第一次layout过程中,所有子View都是调用LayoutInflater的inflate方法动态加载对应布局而产生的,解析布局的过程肯定是耗时的,但是在后续过程中,这种情况不会出现了。接下来,继续看下setupChild方法源码:

private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
        boolean selected, boolean isAttachedToWindow) {
    ...
    
    addViewInLayout(child, flowDown ? -1 : 0, p, true);
    
    ....
}

setupChild方法中会调用addViewInLayout方法将它添加到ListView中,那么回到fillDown方法,其中的while循环就会让子元素View将整个ListView控件填满然后跳出,也就是说即使Adapter中有很多条数据,ListView也只会加载第一屏数据。下图是第一次onLayout的过程:

ListView_onLayout1.png

第二次onLayout

即使是一个再简单的View,在展示到界面上之前都会经历至少两次onMeasure()和两次onLayout()的过程,自然ListView的绘制过程也不例外,同样我们关注的重点还是onLayout过程。

首先还是从layoutChildren()方法看起:

layoutChildren方法

再来看一遍该方法源码:

 @Override
 protected void layoutChildren() {
    ...
    
    final int childCount = getChildCount();
    
    ...
    
    boolean dataChanged = mDataChanged;
      
    ...
    
    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();
    
    switch (mLayoutMode) {
        ...
        
        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;
    }

    ...
    
 }

首先还是会获取子元素的数量,不同于第一次onLayout,此时获取到的子View数量不再为0,而是ListView中显示的子元素数量;下面又调用了RecycleBinfillActiveViews()方法,目前ListView已经有子View了,这样所有的子View都会被缓存到RecycleBin中mActiveViews数组中,后面会使用到他们。

接下来有一个重要的方法:detachAllViewsFromParent(),这个方法会将所有ListView当中的子View全部清除掉,从而保证第二次Layout过程不会产生一份重复的数据,因为layoutChildren方法会向ListView中添加View,在第一次layout中已经添加了一次,如果第二次layout继续添加,那么必然会出现数据重复的问题,因此这里先调用detachAllViewsFromParent方法将第一次添加的View清除掉

这样把已经加载好的View又清除掉,待会还要再重新加载一遍,这不是严重影响效率吗?不用担心,我们刚刚调用了RecycleBinfillActiveViews()方法来缓存子View,等会将直接使用这些缓存好的View来进行添加子View,而并不会重新执行一遍inflate过程,因此效率方面并不会有什么明显的影响。

摘自Android ListView工作原理完全解析,带你从源码的角度彻底理解

再进入判断childCount是否为0的逻辑中,此时会走和第一次layout相反的else逻辑分支,这其中又有三条逻辑分支,第一条一般不成立,因为开始时我们还没选中任何子View,第二条一般成立,mFirstPosition开始时为0,只要Adapter中数据量大于0即可,所以进入了fillSpecific方法:

fillSpecific方法

此方法源码如下:

private View fillSpecific(int position, int top) {
    boolean tempIsSelected = position == mSelectedPosition;
    View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
    // Possibly changed again in fillUp if we add rows above this one.
    mFirstPosition = position;

    View above;
    View below;

    final int dividerHeight = mDividerHeight;
    if (!mStackFromBottom) {
        above = fillUp(position - 1, temp.getTop() - dividerHeight);
        // This will correct for the top of the first view not touching the top of the list
        adjustViewsUpOrDown();
        below = fillDown(position + 1, temp.getBottom() + dividerHeight);
        int childCount = getChildCount();
        if (childCount > 0) {
            correctTooHigh(childCount);
        }
    } else {
        below = fillDown(position + 1, temp.getBottom() + dividerHeight);
        // This will correct for the bottom of the last view not touching the bottom of the list
        adjustViewsUpOrDown();
        above = fillUp(position - 1, temp.getTop() - dividerHeight);
        int childCount = getChildCount();
        if (childCount > 0) {
             correctTooLow(childCount);
        }
    }

    if (tempIsSelected) {
        return temp;
    } else if (above != null) {
        return above;
    } else {
        return below;
    }
}

fillSpecific()方法的功能和fillUp,fillDown差不多,但是在fillSpecific()方法会优先加载指定位置的View,再加载该View上下的其它子View,由于这里我们传入的position就是第一个子元素的位置,因此此时其效果和上述的fillDown()基本一致

此外可以看到,fillSpecific()方法中也调用了makeAndAddView()方法,因为我们之前调用detachAllViewsFromParent()方法把所有ListView当中的子View全部清除掉了,这里肯定要重新再加上,在makeAndAddView()方法中:

makeAndAddView方法

再来看一遍此方法源码:

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
        boolean selected) {
    if (!mDataChanged) {
        // Try to use an existing view for this position.
        final View activeView = mRecycler.getActiveView(position);
        if (activeView != null) {
            // Found it. We're reusing an existing child, so it just needs
            // to be positioned like a scrap view.
            setupChild(activeView, position, y, flow, childrenLeft, selected, true);
            return activeView;
        }
    }

    // Make a new view for this position, or convert an unused view if
    // possible.
    final View child = obtainView(position, mIsScrap);

    // This needs to be positioned and measured.
    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

    return child;
}

首先还是会从RecycleBin中获取ActiveView,不同于第一次layout,这次能获取到了,那肯定就不会进入obtainView中了,而是直接调用setupChild()方法,此时setupChild()方法的最后一个参数是true,表明当前的view是被回收过的,再来看看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);

        ...
        
    } else {
    
        ...
        
    }

    ...
    
}

可以看到,setupChild()方法的最后一个参数是isAttachedToWindow,方法执行过程中会对这个变量进行判断,由于isAttachedToWindow现在是true,所以会执行attachViewToParent()方法,而第一次Layout过程则是执行的else语句中的addViewInLayout()方法。

这两个方法最大的区别在于,如果我们需要向ViewGroup中添加一个新的子View,应该调用addViewInLayout()方法,而如果是想要将一个之前detach的View重新attach到ViewGroup上,就应该调用attachViewToParent()方法。那么由于前面在layoutChildren()方法当中调用了detachAllViewsFromParent()方法,这样ListView中所有的子View都是处于detach状态的,所以这里attachViewToParent()方法是正确的选择。

经历了这样一个detach又attach的过程,ListView中所有的子View又都可以正常显示出来了,那么第二次Layout过程结束。

摘自Android ListView工作原理完全解析,带你从源码的角度彻底理解

下图展示了第二次onLayout的过程:


ListView_onLayout_2.png

ListView如何做到滑动加载更多子View?

onTouchEvent方法

通过以上两次layout,我们已经能看到ListView的第一屏内容了,但是如果Adapter中有大量数据,剩下的数据怎么加载呢?我们知道实际使用过程中,ListView滑动的时候剩余的数据便显示出来了,那滑动首先肯定要监听触摸事件,相关代码在AbsListView中的onTouchEvent中:

@Override
public boolean onTouchEvent(MotionEvent ev) {

    ...
    
    switch (actionMasked) {
    
        ...

        case MotionEvent.ACTION_MOVE: {
            onTouchMove(ev, vtev);
            break;
        }
        
        ...
        
    }

    ...
    
    return true;
}

我们主要关注ACTION_MOVE滑动事件,因为ListView是随着滑动而加载更多子View的,其中调用了onTouchMove方法:

private void onTouchMove(MotionEvent ev, MotionEvent vtev) {
    
    ...

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

此方法中判断了mTouchMode,当手指在屏幕上滑动时,TouchMode是等于TOUCH_MODE_SCROLL,接下来调用了scrollIfNeeded()方法:

scrollIfNeeded方法

private void scrollIfNeeded(int x, int y, MotionEvent vtev) {
   
    ...
    
    final int deltaY = rawDeltaY;
    int incrementalDeltaY =
            mLastY != Integer.MIN_VALUE ? y - mLastY + scrollConsumedCorrection : deltaY;
    int lastYCorrection = 0;

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

            ...
            
        }
    } 
    
    ...
    
}

注意到其中会调用trackMotionScroll方法,只要我们手指滑动了一点距离,此方法就会被调用,自然如果手指在屏幕上滑动了很多,此方法就会被调用很多次。

trackMotionScroll方法

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

    final boolean down = incrementalDeltaY < 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);
                }
            }
        }
    }

    ...

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

    ...
    
    if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
        fillGap(down);
    }
        
    ...
    
    return false;
}

这个方法接收两个参数,一个是deltaY,表示手指从按下的位置到当前手指位置的Y方向的距离incrementalDeltaY则表示距离上次触发event事件手指在Y方向上位置的改变量,可以通过incrementalDeltaY的正负知道用户是往上还是往下滑动。

如果incrementalDeltaY<0,说明是向下滑动,进入if (down) 分支中,其中有个for循环,从上往下获取子View,如果子View的bottom小于ListView的Top说明这个子View已经移出屏幕了,则调用RecycleBin的addScrapView方法将其加入到废弃缓存中,并将计数器count+1,计数器用于记录有多少个子View被移出了屏幕

那么如果是ListView向上滑动的话,其实过程是基本相同的,只不过变成了从下往上依次获取子View,然后判断该子View的top值是不是大于bottom值了,如果大于的话说明子View已经移出了屏幕,同样把它加入到废弃缓存中,并将计数器加1

接下来,如果count大于0,说明有子View被加入废弃缓存了,则会调用detachViewsFromParent()方法将所有移出屏幕的子View全部detach掉。有View被移出,那么自然就需要添加新的View,所以如果ListView中最后一个View的底部已经移入了屏幕,或者ListView中第一个View的顶部移入了屏幕,就会调用fillGap()方法,fillGap()方法是用来加载屏幕外数据的,如下所示:

fillGap方法

此方法的实现在ListView中,

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

fillGap接受一个down参数此参数代表之前ListView是向下还是向上滑动,如果向下则调用fillDown()方法,如果向上滑动则调用fillUp()方法,这两个方法之前已经说过了,内部有一个while循环来对ListView进行填充,填充的过程是通过makeAndAddView来实现的,好吧,再去makeAndAddView方法中看看。

makeAndAddView方法

这是第三次来看此方法源码了:

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
        boolean selected) {
    if (!mDataChanged) {
        // Try to use an existing view for this position.
        final View activeView = mRecycler.getActiveView(position);
        if (activeView != null) {
            // Found it. We're reusing an existing child, so it just needs
            // to be positioned like a scrap view.
            setupChild(activeView, position, y, flow, childrenLeft, selected, true);
            return activeView;
        }
    }

    // Make a new view for this position, or convert an unused view if
    // possible.
    final View child = obtainView(position, mIsScrap);

    // This needs to be positioned and measured.
    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

    return child;
}

首先还是从RecycleBin中获取activeView,不过此时已经获取不到了,因为第二次layout过程中被获取过了,所以只好调用obtainView方法了。

obtainView方法

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

    ...

    final View scrapView = mRecycler.getScrapView(position);
    final View child = mAdapter.getView(position, scrapView, this);
    
    ...

    return child;
}

这里会调用mRecycler.getScrapView方法来获取废弃的缓存View,而刚好我们前面在trackMotionScroll方法中处理已移出屏幕的View时将其加入废弃缓存view中了,也就是说一旦有任何子View被移出了屏幕,就会将它加入到废弃缓存中,而从obtainView()方法中的逻辑来看,一旦有新的数据需要显示到屏幕上,就会尝试从废弃缓存中获取View

所以它们之间就形成了一个生产者和消费者的模式,那么ListView神奇的地方也就在这里体现出来了,不管你有任意多条数据需要显示,ListView中的子View其实来来回回就那么几个移出屏幕的子View会很快被移入屏幕的数据重新利用起来,因而不管我们加载多少数据都不会出现OOM的情况,甚至内存都不会有所增加

摘自Android ListView工作原理完全解析,带你从源码的角度彻底理解

还有一点需要注意,我们将获取到的scrapView传入了mAdapter.getView()方法中,那么这个参数具体是什么用呢,我们来看一个Adapter的getView例子:

public View getView(int position, View convertView, ViewGroup parent) {
    String url = getItem(position);
    View view;
    if (convertView == null) {
        view = LayoutInflater.from(getContext()).inflate(R.layout.image_item, null);
    } else {
        view = convertView;
    }
    ...
    return view;
}

第二个参数就是convertView,所以当这个参数不为null的时候,我们会直接复用此View,然后更新下对应的数据即可,而为null时才去加载布局文件

再之后的代码就是调用setupChild()方法,将获取到的view重新attach到ListView当中,因为废弃缓存中的View也是之前从ListView中detach掉的

至此已经基本将ListView的工作原理说清楚了,下图是滑动时ListView工作原理:


ListView Move

总结一下ListView的缓存原理如下:

ListView cache

参考信息

  • Android ListView工作原理完全解析,带你从源码的角度彻底理解

你可能感兴趣的:(Android源码分析之ListView源码)