美文网首页
ListView 源码分析

ListView 源码分析

作者: MrFengZH | 来源:发表于2019-07-31 18:45 被阅读0次

    前言

    虽然现在在展示数据的时候,更多的是使用 RecyclerView 而不是 ListView。但了解 ListView 还是很有必要的,通过了解 ListView,既可以帮助理解更加复杂的 RecyclerView,也可以更进一步地理解 ListView 和 RecyclerView 的区别。本文将基于 API28 分析 ListView 源码。

    RecycleBin

    RecycleBin 是 AbsListView 中的一个内部类,所以继承于 AbsListView 的子类,也就是 ListView 和 GridView,都可以使用这个类。RecycleBin 机制是 ListView 能够实现成百上千条数据都不会 OOM 的一个重要原因。

    类注释

        /**
         * The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of
         * storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the
         * start of a layout. By construction, they are displaying current information. At the end of
         * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that
         * could potentially be used by the adapter to avoid allocating views unnecessarily.
         */
    

    通过类注释,可以得知:

    RecycleBin 有两个重要的存储:ActiveViews 和 ScrapViews。ActiveViews 是在布局开始时在屏幕上显示的那些视图。在布局结束时,ActiveViews 中的所有视图都降级为 ScrapViews。ScrapViews 是适配器可能使用的旧视图,避免不必要地重新分配视图。

    主要成员变量

        private View[] mActiveViews = new View[0];
    
        // 之所以使用集合数组,是因为可能有多种类型的 item,同一类型的废弃 item 放在同一 list 中
        private ArrayList<View>[] mScrapViews;
        
        // 指向 scrapViews[0](只有一种类型 item 的时候使用它)
        private ArrayList<View> mCurrentScrap;
        
        private ArrayList<View> mSkippedScrap;
    

    主要方法

    先看一下主要的方法:

    • void fillActiveViews(int childCount, int firstActivePosition):第一个参数表示要存储的 View 的数量,第二个参数表示 ListView 中第一个可见元素的索引。调用该方法后就可以根据参数将 ListView 中的指定元素存储到 mActiveViews 数组中。
    • View getActiveView(int position):根据索引获取相应的 ActiveView,获取到后就将该 View 从 ActiveViews 从移除,下次再获取该位置的 ActiveView,将会返回 false,也就是说 ActiveView 不能被重复利用。
    • void addScrapView(View scrap, int position):该方法将一个废弃(比如滚动出了屏幕)的 View 缓存起来。RecycleBin 中使用 mScrapViews 和 mCurrentScrap 来存储废弃的 View。
    • View getScrapView(int position):根据索引找到对应类型的 ScrapViews,并从中获取一个 ScrapView 返回。
    • public void setViewTypeCount(int viewTypeCount):Adapter 可以重写 getViewTypeCount() 方法来表示 ListView 有几种类型的 item,而 setViewTypeCount 根据类型数来初始化 mScrapViews 数组,mCurrentScrap 指向第 0 号数组,所以如果只有一种类型,就可以使用 mCurrentScrap;如果有多种类型,就使用 mScrapViews。

    onLayout

    View 的三大流程中,对于 ListView 而言,onMeasure 并没有什么特别的,因为它终归是一个 View,占用的空间最多也就是整个屏幕。onDraw 也没有什么意义,因为 ListView 本身并不负责绘制,绘制的任务交由子元素自己完成。ListView 大部分的神奇功能都是在 onLayout 中完成的,因此下面分析一些 ListView 的 onLayout过程。

    ListView 并没有重写 onLayout 方法,重写 onLayout 的逻辑在其父类 AbsListView 中:

    AbsListView#onLayout

        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            super.onLayout(changed, l, t, r, b);
            // ...
    
            layoutChildren();
    
            // ...
        }
    

    主要看 layoutChildren 方法,该方法对子元素进行布局,该方法在 AbsListView 是一个空方法,ListView 重写了该方法:

    ListView#layoutChildren

        @Override
        protected void layoutChildren() {
            // ...
    
            try {
                // ...
                
                final int childrenTop = mListPadding.top;
                // 当前拥有的子 View 个数,第一次 layout 时子 View 个数为 0
                final int childCount = getChildCount();
    
                // ...
    
                // 在调用 adapter.notifyDatasetChanged() 方法时,dataChanged 为 true
                // 默认情况下,dataChanged 为 false
                boolean dataChanged = mDataChanged;
    
                // ...
    
                final int firstPosition = mFirstPosition;
                final RecycleBin recycleBin = mRecycler;
                if (dataChanged) {
                    // 将当前所有 item 的 View 添加到 RecycleBin 的 ScrapViews 中保存起来
                    for (int i = 0; i < childCount; i++) {
                        recycleBin.addScrapView(getChildAt(i), firstPosition+i);
                    }
                } else {
                    // 将当前所有 item 的 View 添加到 RecycleBin 的 ActiveViews 中保存起来
                    recycleBin.fillActiveViews(childCount, firstPosition);
                }
    
                // 清除所有子 View
                detachAllViewsFromParent();
                recycleBin.removeSkippedScrap();
    
                // 通常情况下,mLayoutMode 为 LAYOUT_NORMAL,走 default
                switch (mLayoutMode) {
                // ...
                
                default:
                    // 第一次 onLayout 时,childCount 为 0
                    if (childCount == 0) {
                        // 判断布局是从上往下还是从下往上,默认为从上往下,进入 if 块
                        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);
                        }
                    } 
                    // 非第一次 onLayout,childCount 不为 0,包括两种情况:
                    // 1. 首次布局中的第二次 onLayout
                    // 2. 后续已经存在子 View,但数据发送改变时,例如调用了 adapter.nitifyDatasetChanged()
                    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;
                }
    
                // 将未使用的 ActiveViews 移动到 ScrapViews 中
                recycleBin.scrapActiveViews();
    
                // ...
                
                // 布局完成后,重置 layoutMode 和 mDataChanged 
                mLayoutMode = LAYOUT_NORMAL;
                mDataChanged = false;
        
                // ...
            } // ...
        }
    

    该方法较长,只列出了主要代码。重点看 switch 块,这里根据 layoutMode 进行布局,一般走 default。现在先分析第一次 onLayout 的情况,默认从上往下布局,调用 fillFromTop 方法:

    ListView#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);
        }
    

    该方法先保证 mFirstPosition 的合理性,之后调用了 fillDown 方法:

    ListView#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) {
                boolean selected = pos == mSelectedPosition;
                // 添加子 View
                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;
        }
    

    重点看 makeAndAddView 方法,该方法用于添加子 View

    ListView#makeAndAddView

        private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
                boolean selected) {
            if (!mDataChanged) {
                // 先尝试从 ActiveView 中获取
                final View activeView = mRecycler.getActiveView(position);
                if (activeView != null) {
                    setupChild(activeView, position, y, flow, childrenLeft, selected, true);
                    return activeView;
                }
            }
    
            // 通过 obtainView 方法获取子 View
            final View child = obtainView(position, mIsScrap);
    
            // 测量和放置子 View
            setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
    
            return child;
        }
    

    该方法先从 RecycleBin 的 ActiveViews 或通过 obtainView 方法获取子 View,再通过 setupChild 方法测量和放置子 View。

    第一次 layout 时,RecycleBin 并没有缓存 ActiveViews,所以只能通过 obtainView 方法获取子 View,ListView 并没有该方法,该方法在其父类 AbsListView 中

    AbsListView#obtainView

        View obtainView(int position, boolean[] isScrap) {
            // ...
    
            // 从 ScrapViews 中获取一个 scrapView
            final View scrapView = mRecycler.getScrapView(position);
            // 从 Adapter 的 getView 方法获取子 View,并将刚才得到的 scrapView 作为第二个参数传入
            final View child = mAdapter.getView(position, scrapView, this);
            if (scrapView != null) {
                if (child != scrapView) {
                    // 该 scrapView 没有被用户利用,将其返回到 ScrapViews 中
                    mRecycler.addScrapView(scrapView, position);
                } else {
                    if (child.isTemporarilyDetached()) {
                        isScrap[0] = true;
                        child.dispatchFinishTemporaryDetach();
                    } else {
                        isScrap[0] = false;
                    }
    
                }
            }
            // ...
    
            return child;
        }
    

    在该方法,先从 ScrapViews 中获取一个 scrapView,之后调用 Adapter 的 getView 方法获取子 View,并将刚才得到的 scrapView 作为第二个参数传入。

    在第一次 layout 中,由于 scrapView 为 null,所以所有的子 View 都是通过 LayoutInflater 的 inflate 方法加载出来的,相对比较耗时,不过一开始只会加载第一屏的数据,这样就保证了 ListView 的内容能够迅速显示在屏幕上。

    第二次 layout

    在某些手机版本中(9.0 版本好像没有这种情况),View 在展示到界面上时会经历两次 onLayout。如果 ListView 进行了两次 onLayout 的话,就会存在一份重复的元素了。因此 ListView 在 layoutChildren 中对第二次 layout 做了处理,非常巧妙地解决了这个问题。

    下面就来分析一些 ListView 的第二次 layout 过程,首先看 layoutChildren 方法中的变化:

    ListView#layoutChildren

        @Override
        protected void layoutChildren() {
            // ...
    
            try {
                // ...
    
                // 当前拥有的子 View 个数,第二次 layout 时子 View 个数不为 0
                final int childCount = getChildCount();
    
                // ...
    
                // 将当前所有 item 的 View 添加到 RecycleBin 的 ActiveViews 中保存起来
                recycleBin.fillActiveViews(childCount, firstPosition);
    
                // 清除所有子 View
                detachAllViewsFromParent();
    
                // 通常情况下,mLayoutMode 为 LAYOUT_NORMAL,走 default
                switch (mLayoutMode) {
                // ...
                
                default:
                    if (childCount == 0) {// ...}
                    // 第二次 layout 时进入 else 块
                    else {
                        // 一开始没有选中 item,mSelectedPosition 的值为 -1
                        // 所以不会进入 if 块,而是调用 fillSpecific 方法
                        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 中,子 View 数量不为 0,所有子 View 先添加到 RecycleBin 的 ActiveViews 中保存起来。然后清除所有旧的子 View。由于子 View 数量不为 0,之后会调用 fillSpecific 方法:

    ListView#fillSpecific

        private View fillSpecific(int position, int top) {
            boolean tempIsSelected = position == mSelectedPosition;
            
            // 获取并设置当前 position 的子 View
            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;
    
            // 以 position 为中心,分别向上和向下获取并设置其他子 View
            if (!mStackFromBottom) {
                above = fillUp(position - 1, temp.getTop() - dividerHeight);
                adjustViewsUpOrDown();
                below = fillDown(position + 1, temp.getBottom() + dividerHeight);
                int childCount = getChildCount();
                if (childCount > 0) {
                    correctTooHigh(childCount);
                }
            } else {
                below = fillDown(position + 1, temp.getBottom() + dividerHeight);
                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;
            }
        }
    

    该方法先设置当前 position 的子 View,然后以 position 为中心,分别向上和向下设置其他子 View。由于第二次 layout 时传入的 position 就是第一个子 View 的位置,所以和第一次 layout 的布局顺序是差不多的。获取并设置子 View 还是通过 makeAndAddView 方法。

    ListView#makeAndAddView

        private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
                boolean selected) {
            if (!mDataChanged) {
                // 第二次 layout 时,可以从 ActiveViews 中获取到子 View
                final View activeView = mRecycler.getActiveView(position);
                if (activeView != null) {
                    setupChild(activeView, position, y, flow, childrenLeft, selected, true);
                    return activeView;
                }
            }
            
            // ... 
        }
    

    这里和第一次 layout 不同的是,由于之前已经把旧的子 View 存到了 ActiveViews,所以可以直接从 ActiveViews 中获取到子 View,无需再通过 inflate 方法加载子 View。

    小结

    第一次 layout 时,由于当前子 View 数量为 0,且在 RecycleBin 的 ActiveViews 和 ScrapViews 都没有缓存,所以只能在 Adapter 的 getView 方法中,通过 LayoutInflate 的 inflate 方法加载子 View,相对来说比较耗时,不过一开始只会加载第一屏的数据,这样就保证了 ListView 的内容能够迅速显示在屏幕上。

    在某些手机版本中,第一次显示 ListView 时可能会发生两次 layout。和第一次 layout 过程不同,在进行第二次 layout 时,子 View 数量不为 0,就可以先将所有子View 添加到 RecycleBin 的 ActiveViews 中保存起来。然后清除旧的子 View,之后再次设置新的子 View 时,由于之前已经把旧的子 View 存到了 ActiveViews,所以可以直接从 ActiveViews 中获取到子 View,无需再通过 inflate 方法加载子 View。

    (注:在 Android 9.0 版本中,Button 显示时调用了两次 onMeasure、一次 onLayout、两次 onDraw;TextView 显示时调用了两次 onMeasure、一次 onLayout、一次 onDraw;ListView 会调用多次 onMeasure、一次 onLayout、多次 onDraw。所以在 9.0 版本并不会发生第二次 layout。)

    滑动加载更多数据

    上面 layout 过程分析的只是加载第一页的数据,如果有很多数据,剩下的数据将会在滑动过程中加载。下面将分析一下滑动加载数据的过程。

    该过程涉及到事件分发,所以是从 AbsListView 的 onTouchEvent 方法开始,滑动对应 ACTION_MOVE,所以接下来调用 onTouchMove 方法,里面又有一个 switch 语句判断 mTouchMode,这里对应 TOUCH_MODE_SCROLL,所以接下来调用 scrollIfNeeded 方法,里面又继续调用 trackMotionScroll 方法。

    下面看一下 trackMotionScroll 方法:

    AbsListView#trackMotionScroll

        boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
            // ...
    
            // incrementalDeltaY < 0,说明是向下滑动(这里指内容,手指是向上滑动的)
            final boolean down = incrementalDeltaY < 0;
    
            // getHeaderViewsCount 和 getFooterViewsCount 默认返回 0
            final int headerViewsCount = getHeaderViewsCount();
            final int footerViewsStart = mItemCount - getFooterViewsCount();
    
            int start = 0;  // 开始移除的索引
            int count = 0;  // 移除的数量
    
            // 向下滑动
            if (down) {
                int top = -incrementalDeltaY;
                if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
                    top += listPadding.top;
                }
                // 从上往下遍历子 View
                for (int i = 0; i < childCount; i++) {
                    final View child = getChildAt(i);
                    // 如果该子 View 的 bottom 值大于等于滑动的距离
                    // 说明该子 View 以及其后的 View 都在屏幕上,退出循环
                    if (child.getBottom() >= top) {
                        break;
                    } 
                    // 如果该子 View 的 bottom 值小于滑动的距离,说明该子 View 已经不在屏幕上
                    else { 
                        count++;
                        int position = firstPosition + i;   // 该子 View 的索引
                        // 将不在屏幕的子 View 添加进 RecycleBin 的 ScrapViews 中
                        if (position >= headerViewsCount && position < footerViewsStart) {
                            child.clearAccessibilityFocus();
                            mRecycler.addScrapView(child, position);
                        }
                    }
                }
            } else {
                // 向上滑动,和向下滑动的过程相似,也是将不在屏幕上的子 View 添加进 RecycleBin 的 ScrapViews 中
                // ...
            }
    
            // 将不在屏幕的子 View 全部 detach 掉
            if (count > 0) {
                detachViewsFromParent(start, count);
                mRecycler.removeSkippedScrap();
            }
    
            // 让所有的子 View 进行相应的偏移,达到内容随手指的拖动而滚动的效果
            offsetChildrenTopAndBottom(incrementalDeltaY);
            
            // 向下滑动时,更新 mFirstPosition(之后填充布局时会用到)
            if (down) {
                mFirstPosition += count;
            }
    
            final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
            // 如果第一个 View 的顶部或最后一个 View 的底部移入屏幕
            // 说明要加载屏幕外的数据来填充布局
            if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
                fillGap(down);
            }
        
            // ...
    
            return false;
        }
    

    该方法首先将滑出屏幕的子 View 添加进 RecycleBin 的 ScrapViews 中,并全部 detach 掉。然后让剩下的子 View 进行相应的偏移,达到内容随手指的拖动而滚动的效果。最后调用 fillGap 方法加载屏幕外的数据来填充布局,fillGap 在 AbsListView 是一个抽象方法,ListView 中有具体实现。

    ListView#fillGap

        @Override
        void fillGap(boolean down) {
            final int count = getChildCount();
            // 如果是向下滑动,就通过 fillDown 方法从上往下添加子 View
            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());
            } 
            // 如果是向下滑动,就通过 fillUp 方法从下往上添加子 View
            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());
            }
        }
    

    该方法根据滑动方向,调用 fillDown 或 fillUp 方法添加子 View,无论调用拿个方法,最终都是调用 makeAndAddView 方法:

    ListView#makeAndAddView

        private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
                boolean selected) {
            if (!mDataChanged) {
                // 先尝试从 ActiveViews 中获取
                final View activeView = mRecycler.getActiveView(position);
                if (activeView != null) {
                    setupChild(activeView, position, y, flow, childrenLeft, selected, true);
                    return activeView;
                }
            }
    
            // 通过 obtainView 方法获取子 View
            final View child = obtainView(position, mIsScrap);
    
            // 测量和放置子 View
            setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
    
            return child;
        }
    

    先从 RecycleBin 的 ActiveViews 中获取,如果还没有进行第二次 layout 的话,是可以获取到的,如果已经进行过第二次 layout,那么就获取不到了,因为第二次 layout 的时候已经从 ActiveViews 中拿到过子 View,而 ActiveViews 不能重复利用,所以就获取不到了。

    如果 ActiveViews 获取不到,就会调用 obtainView 方法获取:

    AbsListView#obtainView

        View obtainView(int position, boolean[] isScrap) {
            // ...
    
            // 从 ScrapViews 中获取一个 scrapView
            final View scrapView = mRecycler.getScrapView(position);
            // 从 Adapter 的 getView 方法获取子 View,并将刚才得到的 scrapView 作为第二个参数传入
            final View child = mAdapter.getView(position, scrapView, this);
            if (scrapView != null) {
                if (child != scrapView) {
                    // 该 scrapView 没有被用户利用,将其返回到 ScrapViews 中
                    mRecycler.addScrapView(scrapView, position);
                } else {
                    if (child.isTemporarilyDetached()) {
                        isScrap[0] = true;
                        child.dispatchFinishTemporaryDetach();
                    } else {
                        isScrap[0] = false;
                    }
    
                }
            }
            // ...
    
            return child;
        }
    

    这次和第一次 layout 的情况不一样,因为之前把移除屏幕的子 View 添加到了 ScrapViews 中,所以现在就可以从 ScrapViews 中得到之前移除的子 View,并传入 Adapter 的 getView 方法。用户就可以利用这个缓存 View,不用再 inflate 一个子 View 了。

    小结

    ListView 在滑动时,先将滑出屏幕的子 View 添加进 RecycleBin 的 ScrapViews 中,并从父布局中 detach 掉。然后让剩下的子 View 进行相应的偏移,达到内容随手指的拖动而滚动的效果。最后通过加载屏幕外的数据来填充布局,这时就可以从 ScrapViews 中得到之前移除的子 View,并传入 Adapter 的 getView 方法。用户就可以重复利用这个缓存 View,无需再重新 inflate 一个子 View。

    Adapter 相关

    ListView 只是负责展示各子 View,各子 View 具体如何填充数据是交由 Adapter 来完成的。ListView 通过 setAdapter 方法和 Adapter 建立联系。先看一下该方法:

    ListView#setAdapter

        @Override
        public void setAdapter(ListAdapter adapter) {
            
            // 如果之前绑定过 Adapter,先取消注册 AdapterDataSetObserver
            if (mAdapter != null && mDataSetObserver != null) {
                mAdapter.unregisterDataSetObserver(mDataSetObserver);
            }
    
            resetList();
            mRecycler.clear();
    
            // 设置新的 Adapter
            if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
                // 如果 ListView 有 headerView 或 footerView,需包装传入的 adapter
                mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, adapter);
            } else {
                mAdapter = adapter;
            }
    
            // AbsListView#setAdapter will update choice mode states.
            super.setAdapter(adapter);
    
            if (mAdapter != null) {
                mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();
                mOldItemCount = mItemCount;
                // 设置 item 个数
                mItemCount = mAdapter.getCount();
                checkFocus();
    
                // 生成并在 Adapter 中注册 AdapterDataSetObserver,用于通知数据源的改变
                mDataSetObserver = new AdapterDataSetObserver();
                mAdapter.registerDataSetObserver(mDataSetObserver);
    
                mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
    
                //...
            } // ...
    
            // 进行视图重绘
            requestLayout();
        }
    

    在该方法中,ListView 绑定传入的 adapter,并为 adapter 注册 AdapterDataSetObserver,用于通知数据源的改变。 最后调用 requestLayout 方法,该方法最终会调用 ListView 的 onLayout,来到第一次 onLayout 的过程。

    如果数据源发生了改变,想要更新 ListView 的时候,我们会调用 Adapter 的 notifyDataSetChanged 方法:

    BaseAdapter#notifyDataSetChanged

        public void notifyDataSetChanged() {
            mDataSetObservable.notifyChanged();
        }
    

    又调用了 DataSetObservable 的 notifyChanged:

        public void notifyChanged() {
            synchronized(mObservers) {
                for (int i = mObservers.size() - 1; i >= 0; i--) {
                    mObservers.get(i).onChanged();
                }
            }
        }
    

    这里的 mObservers 定义在 DataSetObservable 的父类 Observable 中:

        protected final ArrayList<T> mObservers = new ArrayList<T>();
    

    mObservers 的元素是从哪里来的呢?要从 setAdapter 的这一句说起:

        mAdapter.registerDataSetObserver(mDataSetObserver);
    

    这一句最终调用了 Observable 的 registerObserver 方法:

        public void registerObserver(T observer) {
            if (observer == null) {
                throw new IllegalArgumentException("The observer is null.");
            }
            synchronized(mObservers) {
                if (mObservers.contains(observer)) {
                    throw new IllegalStateException("Observer " + observer + " is already registered.");
                }
                mObservers.add(observer);
            }
        }
    

    可以看到,这里将 Adapter 注册的 AdapterDataSetObserver 添加进了 mObservers 中。

    所以饶了一大圈,Adapter 的 notifyDataSetChanged 方法最终调用了 AdapterDataSetObserver(AdapterView 的一个内部类)的 onChanged 方法:

    AdapterDataSetObserver#onChanged

        @Override
        public void onChanged() {
            // 将 mDataChanged 属性设置为 true
            mDataChanged = true;
            mOldItemCount = mItemCount;
            // 更新 item 数量
            mItemCount = getAdapter().getCount();
            
            // ...
            
            // 最后进行视图重绘
            requestLayout();
        }
    

    在该方法中,首先将 mDataChanged 属性设置为 true,并更新 item 数量,最后进行视图重绘,在 onLayout 中更新子 View。

    写在最后

    到此为止,对于 ListView 的 分析就告一段落了。在分析 ListView 的过程,发现经常遇到也是最重要的就是 onLayout 过程以及 RecycleBin 机制。

    无论是设置 Adapter 还是通知 Adapter 更新数据的过程,最终都会回到视图重绘,也就是 onLayout。而 onLayout 过程也会根据是第一次 layout、第二次 layout 还是数据源改变的情况从不同途径获取子 View,是通过 inflate 加载还是从 RecycleBin 的 ActiveViews 或 ScrapViews 获取。

    RecycleBin 对子 View 的回收也是 ListView 的一个重点或是巧妙之处,在第二次 layout 时,会把子 View 添加到 RecycleBin 的 ActiveViews 中,之后获取新的子 View 时就可以直接从 ActiveViews 获取。在滑动过程中,滑出屏幕的子 View 又会被添加到 RecycleBin 的 ScrapViews 中,在之后填充布局时重新利用。

    参考

    相关文章

      网友评论

          本文标题:ListView 源码分析

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