美文网首页Ui
Android源码分析之ListView源码

Android源码分析之ListView源码

作者: Hengtao24 | 来源:发表于2020-03-06 20:41 被阅读0次

    系列文章

    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<View>[] mScrapViews;
    
            private int mViewTypeCount;
            
            // 存储废弃View
            private ArrayList<View> 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<View>[] scrapViews = new ArrayList[viewTypeCount];
                for (int i = 0; i < viewTypeCount; i++) {
                    scrapViews[i] = new ArrayList<View>();
                }
                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<View>
    • 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源码

        本文链接:https://www.haomeiwen.com/subject/gggnrhtx.html