ListView源码分析

Posted 花花young

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ListView源码分析相关的知识,希望对你有一定的参考价值。

前言

android所有常用的原生控件当中,用法最复杂的应该就是ListView了,几乎所有的app都应用了列表来展示数据,并且能加载大量数据而不发生OOM。作为开发者有责任和义务去研究这个神秘的控件,接下来本文将为你揭开ListView的神秘面纱

开篇一问

ListView我们也用了很久,本文将结合以下问题进行深入:

  1. ListView为什么加载上百上千条数据而不发生OOM异常?
  2. Adapter的notifyDataSetChanged方法如何让ListView进行刷新的?
  3. ListView刷新显示Item列表的源码探索。
  4. ListView滑动情况Item列表的源码探索。

在开始之前我们来了解一下ListView的继承关系

对于ListView是用来显示列表,它不直接和数据源打交道,这才有了Adapter适配器做ListView和数据源的桥梁。可能你会问如果没有Adapter那也是可以的做的啊?但是你想过没有如果没有Adapter适配器那ListView做适配的工作就非常复杂了,因为数据源有多种类型所以会写多种相应的方法进行接收数据源,当有了新的类型数据源还需要在ListView源码里面去更改,这样会导致ListView的扩展性极差,超出了它本身应该承担的工作。OK,来张图体会一下:

问题一 ListView为什么加载上百上千条数据而不发生OOM异常?

ListView之所以没有发生OOM异常的原因在于内部引入了RecycleBin机制,另外我们需要说一点的是因为RecycleBin是AbsListView的内部类,所以继承AbsListView的GridView也具有RecycleBin机制。废话不多说,我们来了解一下RecycleBin的源码

    class RecycleBin 
        private View[] mActiveViews = new View[0];
        private ArrayList<View>[] mScrapViews;
        private ArrayList<View> mCurrentScrap;
        public void setViewTypeCount(int viewTypeCount) 
            .....
        
        /**
         * RecycleBin当中使用mActiveViews这个数组来存储View,调用这个方法后就会根据传入的参数来将ListView中的指定元素存         * 储到mActiveViews数组当中。
         */
        void fillActiveViews(int childCount, int firstActivePosition) 
            ......
        

        /**
         * 一旦被获取了之后就会从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参数,当有某个View确定要废弃掉的时候(比如滚动出了屏幕),就应该调        * 用这个方法来对View进行缓存,RecycleBin当中使用mScrapViews和mCurrentScrap这两个List来存储废弃View
         */
        void addScrapView(View scrap, int position) 
            ......
        

OK,RecycleBin源码分析到这,来做个总结

  1. RecycleBin有两个存储,活动状态mActiveViews和废弃状态mScrapViews
  2. mActiveViews用来存储屏幕显示的View,当划出屏幕的View将存入mScrapViews,新进来的View则先从mScrapView取,如果存在则将其置为活动状态
问题二 Adapter的notifyDataSetChanged方法如何让ListView进行刷新的?

notifyDataSetChanged通知ListView进行刷新其实内部使用到了观察者模式

public abstract class BaseAdapter implements ListAdapter, SpinnerAdapter 
    private final DataSetObservable mDataSetObservable = new DataSetObservable(); // 被观察者
    public void registerDataSetObserver(DataSetObserver observer) 
        mDataSetObservable.registerObserver(observer);// 无非就是将Oberver存入protected final ArrayList<T> mObservers = new ArrayList<T>();
    

    public void unregisterDataSetObserver(DataSetObserver observer) 
        mDataSetObservable.unregisterObserver(observer);
    

    //更新数据
    public void notifyDataSetChanged() 
        mDataSetObservable.notifyChanged();
    

这里有几点需要注意:
1. BaseAdapter为被观察者 ListView为观察者
2. registerDataSetObserver在setAdapter的时候就调用了此方法,DataSetObservable便持有了观察者对象,观察者存入了mObservers集合中,这也说明适配器可以接受多个ListView
3. notifyDataSetChanged用来通知观察者进行刷新数据

那我们来验证一下setAdapter源码验证当时的结论:

    @Override
    public void setAdapter(ListAdapter adapter) 
        if (mAdapter != null && mDataSetObserver != null) 
            mAdapter.unregisterDataSetObserver(mDataSetObserver);
        
        if (mAdapter != null) 
            ......
            //将观察者传入到了Adapter中
            mDataSetObserver = new AdapterDataSetObserver();
            mAdapter.registerDataSetObserver(mDataSetObserver);
       
    

那我们回来接着研究notifyDataSetChanged的源码,内部调用了mDataSetObservable.notifyChanged();

public class DataSetObservable extends Observable<DataSetObserver> 
    /**
     * 唤醒每个观察者,当内容数据发生改变的时候将会调用
     */
    public void notifyChanged() 
        synchronized(mObservers)  // 一个适配器可以对应多个ListView,当进行刷新的时候会按顺序进行刷新防止发生错乱
            for (int i = mObservers.size() - 1; i >= 0; i--) 
                mObservers.get(i).onChanged();
            
        
    

内部使用synchronized同步代码块用于防止多个ListView同时刷新同一个Adapter造成数据错乱的情况,接着就是用个循环遍历通知观察者刷新,OK,进入onChanged一探究竟:

    class AdapterDataSetObserver extends AdapterView<ListAdapter>.AdapterDataSetObserver 
        @Override
        public void onChanged() 
            super.onChanged();
            if (mFastScroll != null) 
                mFastScroll.onSectionsChanged();
            
        
    

接下来又调用了super.onChanged()方法,在AdapterView内部类

   class AdapterDataSetObserver extends DataSetObserver 
        private Parcelable mInstanceState = null;
        @Override
        public void onChanged() 
            mDataChanged = true;
            mOldItemCount = mItemCount;
            mItemCount = getAdapter().getCount();
            if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
                    && mOldItemCount == 0 && mItemCount > 0) 
                AdapterView.this.onRestoreInstanceState(mInstanceState);
                mInstanceState = null;
             else 
                rememberSyncState();
            
            checkFocus();
            requestLayout();
        

在onChange方法里面为一些变量赋了值,在这里最重要的还是requestLayout()方法通知ListView去调用onLayout方法对Item重新进行布局,也就是相应的刷新列表。

综上,我们从notifyDataSetChanged方法,一步步分析,最终通过调用ListView的onLayout方法进行刷新。在这里面我们了解到观察者模式

问题三 ListView刷新显示Item列表的源码探索

因为ListView继承的ViewGroup自然也离不开UI绘制的三大方法:onMeasure、onLayout、onDraw。在ListView当中,onMeasure()占用的空间最多并且通常也就是整个屏幕。onDraw()在ListView当中也没有什么意义,因为ListView本身并不负责绘制,而是由ListView当中的子元素来进行绘制的。所以说onLayout才是ListView的核心所在,并且在上面分析出来notifyDataSetChanged方法最终调用的是ListView的onLayout的方法,所以我们应该从ListView的onLayout方法开始分析:

    /**
     * Subclasses should NOT override this method but
     *  @link #layoutChildren() instead.
     */
    @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();
         ......
    

当ListView的位置和大小发生改变时changed为true,则会遍历子View进行强制刷新;然而此次研究重点不在这里,继续往下看我们看到layoutChildren方法,用于对子View的排列布局,因为父类AbsListView layoutChildren是个空方法,所以实现在ListView中:

    @Override
    protected void layoutChildren() 
        try 
            super.layoutChildren();
            invalidate();
            final int childrenTop = mListPadding.top;
            final int childrenBottom = mBottom - mTop - mListPadding.bottom;
            final int childCount = getChildCount();

            ......//这里主要是获取焦点代码,和此次分析无关

             // Pull all children into the RecycleBin.
            // These views will be reused if possible
            final int firstPosition = mFirstPosition;
            final RecycleBin recycleBin = mRecycler;
            if (dataChanged)  // 重点1
                for (int i = 0; i < childCount; i++) 
                    recycleBin.addScrapView(getChildAt(i), firstPosition+i);
                
             else 
                recycleBin.fillActiveViews(childCount, firstPosition);
            

            // Clear out old views
            detachAllViewsFromParent(); // 将所有的View取消父类的关联,目的是将View从ViewTree中拆分出来
            recycleBin.removeSkippedScrap();

            switch (mLayoutMode)  //默认情况mLayoutMode为LAYOUT_NORMAL,所以直接执行default里面的代码
            ..... //其它的case代码
            default: // 重点2
                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;
            

            // Flush any cached views that did not get reused above
            recycleBin.scrapActiveViews();

            // remove any header/footer that has been temp detached and not re-attached
            removeUnusedFixedViews(mHeaderViewInfos);
            removeUnusedFixedViews(mFooterViewInfos);
            ...... //设置ListView的selection
    

ok,来看一下我们的重点:
1、dataChanged只有当数据源发生改变的时候才会为true,所以第一次onLayout会执行fillActiveViews方法,因为刚开始的时候子View还未加载到ListView中,所以childCount=0,这个方法并没有什么作用。
2、默认情况mLayoutMode为LAYOUT_NORMAL所以会执行default代码块,又因为childCount=0并且默认的布局顺序是从上往下,因此会执行fillFromTop()方法

    /**
     * 填充ListView
     */
    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);
    

从mFirstPosition位置开始,自上而下开始去填充ListView,ok,进入fillDown方法

    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)  // mItemCount = getAdapter().getCount();
            // 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;
    

一开始nextTop的值是第一个item顶部距离整个ListView顶部的像素值,pos则是刚刚传入的mFirstPosition的值,而end是ListView底部减去顶部所得的像素值
所以退出循环有两种情况一种是mItemCount超出一屏且nextTop超出了当前屏幕;另一种是mItemCount不满一屏且nextTop没有超出当前屏幕。除了这里还调用了一个重要的方法makeAndAddView(),得到View并添加到ListView中:

    /**
     * Obtains the view and adds it to our list of children. The view can be
     * made fresh, converted from an unused view, or used as is if it was in
     * the recycle bin.
     */
    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); // 重点1
        // This needs to be positioned and measured.
        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); // 重点2

        return child;
    

那我们来看重点1,第一次进来的时候mRecycler.getActiveView得到的是null,所以会执行obrainView方法生成View

    View obtainView(int position, boolean[] outMetadata) 
         outMetadata[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) 
            .......
            outMetadata[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 if (child.isTemporarilyDetached()) 
                outMetadata[0] = true;

                // Finish the temporary detach started in addScrapView().
                child.dispatchFinishTemporaryDetach();
            
        
        setItemViewLayoutParams(child, position);
        return child;
    

同样第一次的时候scrapView=null,所以开始的时候会调用mAdapter.getView(position,null,this),这种情况我们会自己inflate View,这里我们贴一下我们在使用Adapter的时候代码

    class MyAdapter extends BaseAdapter
        @Override
        public int getCount() 
            return 0;
        

        @Override
        public Object getItem(int position) 
            return null;
        

        @Override
        public long getItemId(int position) 
            return 0;
        

        @Override
        public View getView(int position, View convertView, ViewGroup parent) 
            if(convertView == null)
                //inflate view
            else
                //重用view
            
            return null;
        
    

从源码也能看出来,当第一次进来的时候由于scrapView==null,所以我们在getView方法中converView参数就为null,需要重新inflate View。
OK,View得到了,那我们来看一下上面的重点2

   /**
     * Adds a view as a child and make sure it is measured (if necessary) and
     * positioned properly.
     */
    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();
        
        ......//需要去测量View
    

第一次layout的时候isAttachedToWindow为false,所以会执行addViewInLayout方法

    /**
     * Adds a view during layout. This is useful if in your onLayout() method,
     * you need to add more views (as does the list view for example).
     *
     * If index is negative, it means put it at the end of the list.
     */
    protected boolean addViewInLayout(View child, int index, LayoutParams params,
            boolean preventRequestLayout) 
        if (child == null) 
            throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
        
        child.mParent = null;
        addViewInner(child, index, params, preventRequestLayout);
        child.mPrivateFlags = (child.mPrivateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
        return true;
    

进入addViewInner方法

    private void addViewInner(View child, int index, LayoutParams params,
            boolean preventRequestLayout) 
        if (child.getParent() != null)  
            throw new IllegalStateException("The specified child already has a parent. " +
                    "You must call removeView() on the child's parent first.");
        
        if (!checkLayoutParams(params)) 
            params = generateLayoutParams(params);
        

        if (preventRequestLayout) 
            child.mLayoutParams = params;
         else 
            child.setLayoutParams(params);
        
        addInArray(child, index);
        // tell our children
        if (preventRequestLayout) 
            child.assignParent(this);
         else 
            child.mParent = this;
        
        ......
        dispatchViewAdded(child); // 当添加View之后会进行回调onViewAdded方法
        ......
    

第一次layout就这样结束了。

但这样就结束了吗?对于一个简单的View界面显示都会经历至少两次onMeasure、onLayout方法,一次或者两次对我们开发者来说影响并不大,但是对于ListView却不同,ListView如果经过两次之后便会存在一份重复的数据,但是Google开发工程师巧妙的解决了这个问题,现在就让我们来分析一下它的巧妙之处吧,为了方便,这次我们从layoutChildren开始分析

           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;
            

因为第二次layout,ItemCount不为0所以detachAllViewsFromParent()方法起作用了,这个方法会将所有ListView当中的子View全部清除掉,从而保证第二次Layout过程不会产生一份重复的数据。那有的朋友可能会问了,这样把已经加载好的View又清除掉,待会还要再重新加载一遍,这不是严重影响效率吗?不用担心,还记得我们刚刚调用了RecycleBin的fillActiveViews()方法来缓存子View吗,待会儿将会直接使用这些缓存好的View来进行加载,而并不会重新执行一遍inflate过程,因此效率方面并不会有什么明显的影响。这里我们只看default代码,会调用fillSpecific

    /**
     * Put a specific item at a specific location on the screen and then build
     * up and down from there.
     */
    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 
            ......
        
    

fillSpecific()这算是一个新方法了,不过其实它和fillUp()、fillDown()方法功能也是差不多的,主要的区别在于,fillSpecific()方法会优先将指定位置的子View先加载到屏幕上,然后再加载该子View往上以及往下的其它子View。那么由于这里我们传入的position就是第一个子View的位置,于是fillSpecific()方法的作用就基本上和fillDown()方法是差不多的了,OK,我们进入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;
    

由于第二次layout,现在activeView!=null,所以将不再inflate View而是直接重用activeView

    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();
        
       ...... // 测量代码
    

因为上面传入的isAttachedToWindow为true,所以会执行attachViewToParent方法。相比第一次layout的addViewInLayout的最大区别在于addViewInLayout是创建一个新View添加到ListView中,而attachViewToParent是将detach的View添加到ListView中,恰巧在layoutChildren方法中调用了detachAllViewsFromParent()将所有的子View都处于Detach状态。
经历了这样一个detach又attach的过程,ListView中所有的子View又都可以正常显示出来了,那么第二次Layout过程结束。

问题四 ListView滑动情况Item列表的源码探索

上面我们分析的只不过当第一次进入的ListView如何加载Item的过程,并非ListView的核心代码,ListView的核心代码其实是在滑动的过程中,Item如何进行重用的。OK,我们来看AbsListView的onTouchEvent事件

   private void onTouchMove(MotionEvent ev, MotionEvent vtev) 
        if (mDataChanged) 
            // Re-sync everything if data has been changed
            // since the scroll operation can query the adapter.
            layoutChildren();
        
        final int y = (int) ev.getY(pointerIndex);
        switch (mTouchMode) 
            ......
            case TOUCH_MODE_SCROLL:
            case TOUCH_MODE_OVERSCROLL:
                scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev);
                break;
        
    

这里的事件TOUCH_MODE_SCROLL,所以会执行scrollIfNeed方法,进而会执行trackMotionScroll方法

    /**
     * int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY;
     * Track a motion scroll
     */
    boolean trackMotionScroll(int deltaY, int incrementalDeltaY) 
        final int childCount = getChildCount();
        final int firstTop = getChildAt(0).getTop();
        final int lastBottom = getChildAt(childCount - 1).getBottom();
        final Rect listPadding = mListPadding;
        if (incrementalDeltaY < 0)  //手指在Y方向上位置的改变量incrementalDeltaY小于0,说明是向下滑动,否则就是向上滑动
            incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
         else 
            incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
        
        if (cannotScrollDown || cannotScrollUp)  //当不能滑动的时候则直接返回
            return incrementalDeltaY != 0;
        

        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);
                    
                
            
        
        mMotionViewNewTop = mMotionViewOriginalTop + deltaY;
        mBlockLayoutRequests = true;
        if (count > 0) 
            detachViewsFromParent(start, count);
            mRecycler.removeSkippedScrap();
        
        offsetChildrenTopAndBottom(incrementalDeltaY);
        final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
        //
        if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) 
            fillGap(down);
        
        ......
        return false;
    

然后在第84行会进行判断,如果ListView中最后一个View的底部已经移入了屏幕,或者ListView中第一个View的顶部移入了屏幕,就会调用fillGap()方法,那么因此我们就可以猜出fillGap()方法是用来加载屏幕外数据

    /**
     * @inheritDoc
     */
    @Override
    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参数用于表示ListView是向下滑动还是向上滑动的,可以看到,如果是向下滑动的话就会调用fillDown()方法,而如果是向上滑动的话就会调用fillUp()方法。那么这两个方法我们都已经非常熟悉了,内部都是通过一个循环来去对ListView进行填充,所以这两个方法我们就不看了,但是填充ListView会通过调用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的getActiveView()方法来获取子布局,只不过肯定是获取不到的了,因为在第二次Layout过程中我们已经从mActiveViews中获取过了数据,而根据RecycleBin的机制,mActiveViews是不能够重复利用的,因此这里返回的值肯定是null。既然getActiveView()方法返回的值是null,那么还是会走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;
    

RecyleBin的getScrapView()方法来尝试从废弃缓存中获取一个View,那么废弃缓存有没有View呢?当然有,因为刚才在trackMotionScroll()方法中我们就已经看到了,一旦有任何子View被移出了屏幕,就会将它加入到废弃缓存中,而从obtainView()方法中的逻辑来看,一旦有新的数据需要显示到屏幕上,就会尝试从废弃缓存中获取View。所以它们之间就形成了一个生产者和消费者的模式,那么ListView神奇的地方也就在这里体现出来了,不管你有任意多条数据需要显示,ListView中的子View其实来来回回就那么几个,移出屏幕的子View会很快被移入屏幕的数据重新利用起来,因而不管我们加载多少数据都不会出现OOM的情况,甚至内存都不会有所增加

以上是关于ListView源码分析的主要内容,如果未能解决你的问题,请参考以下文章

ScrollView镶嵌listview显示不全的原因

ListView 源码分析

通过源码分析View的测量

Android绘制源码分析(下)

基于ListView的源码分析工作原理

Jetpack Compose 测量流程源码分析