ListView 源碼分析

前言

雖然現(xiàn)在在展示數(shù)據(jù)的時(shí)候,更多的是使用 RecyclerView 而不是 ListView。但了解 ListView 還是很有必要的,通過(guò)了解 ListView,既可以幫助理解更加復(fù)雜的 RecyclerView,也可以更進(jìn)一步地理解 ListView 和 RecyclerView 的區(qū)別。本文將基于 API28 分析 ListView 源碼。

RecycleBin

RecycleBin 是 AbsListView 中的一個(gè)內(nèi)部類(lèi),所以繼承于 AbsListView 的子類(lèi),也就是 ListView 和 GridView,都可以使用這個(gè)類(lèi)。RecycleBin 機(jī)制是 ListView 能夠?qū)崿F(xiàn)成百上千條數(shù)據(jù)都不會(huì) OOM 的一個(gè)重要原因。

類(lèi)注釋

    /**
     * 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.
     */

通過(guò)類(lèi)注釋?zhuān)梢缘弥?/p>

RecycleBin 有兩個(gè)重要的存儲(chǔ):ActiveViews 和 ScrapViews。ActiveViews 是在布局開(kāi)始時(shí)在屏幕上顯示的那些視圖。在布局結(jié)束時(shí),ActiveViews 中的所有視圖都降級(jí)為 ScrapViews。ScrapViews 是適配器可能使用的舊視圖,避免不必要地重新分配視圖。

主要成員變量

    private View[] mActiveViews = new View[0];

    // 之所以使用集合數(shù)組,是因?yàn)榭赡苡卸喾N類(lèi)型的 item,同一類(lèi)型的廢棄 item 放在同一 list 中
    private ArrayList<View>[] mScrapViews;
    
    // 指向 scrapViews[0](只有一種類(lèi)型 item 的時(shí)候使用它)
    private ArrayList<View> mCurrentScrap;
    
    private ArrayList<View> mSkippedScrap;

主要方法

先看一下主要的方法:

  • void fillActiveViews(int childCount, int firstActivePosition):第一個(gè)參數(shù)表示要存儲(chǔ)的 View 的數(shù)量,第二個(gè)參數(shù)表示 ListView 中第一個(gè)可見(jiàn)元素的索引。調(diào)用該方法后就可以根據(jù)參數(shù)將 ListView 中的指定元素存儲(chǔ)到 mActiveViews 數(shù)組中。
  • View getActiveView(int position):根據(jù)索引獲取相應(yīng)的 ActiveView,獲取到后就將該 View 從 ActiveViews 從移除,下次再獲取該位置的 ActiveView,將會(huì)返回 false,也就是說(shuō) ActiveView 不能被重復(fù)利用。
  • void addScrapView(View scrap, int position):該方法將一個(gè)廢棄(比如滾動(dòng)出了屏幕)的 View 緩存起來(lái)。RecycleBin 中使用 mScrapViews 和 mCurrentScrap 來(lái)存儲(chǔ)廢棄的 View。
  • View getScrapView(int position):根據(jù)索引找到對(duì)應(yīng)類(lèi)型的 ScrapViews,并從中獲取一個(gè) ScrapView 返回。
  • public void setViewTypeCount(int viewTypeCount):Adapter 可以重寫(xiě) getViewTypeCount() 方法來(lái)表示 ListView 有幾種類(lèi)型的 item,而 setViewTypeCount 根據(jù)類(lèi)型數(shù)來(lái)初始化 mScrapViews 數(shù)組,mCurrentScrap 指向第 0 號(hào)數(shù)組,所以如果只有一種類(lèi)型,就可以使用 mCurrentScrap;如果有多種類(lèi)型,就使用 mScrapViews。

onLayout

View 的三大流程中,對(duì)于 ListView 而言,onMeasure 并沒(méi)有什么特別的,因?yàn)樗K歸是一個(gè) View,占用的空間最多也就是整個(gè)屏幕。onDraw 也沒(méi)有什么意義,因?yàn)?ListView 本身并不負(fù)責(zé)繪制,繪制的任務(wù)交由子元素自己完成。ListView 大部分的神奇功能都是在 onLayout 中完成的,因此下面分析一些 ListView 的 onLayout過(guò)程。

ListView 并沒(méi)有重寫(xiě) onLayout 方法,重寫(xiě) onLayout 的邏輯在其父類(lèi) 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 方法,該方法對(duì)子元素進(jìn)行布局,該方法在 AbsListView 是一個(gè)空方法,ListView 重寫(xiě)了該方法:

ListView#layoutChildren

    @Override
    protected void layoutChildren() {
        // ...

        try {
            // ...
            
            final int childrenTop = mListPadding.top;
            // 當(dāng)前擁有的子 View 個(gè)數(shù),第一次 layout 時(shí)子 View 個(gè)數(shù)為 0
            final int childCount = getChildCount();

            // ...

            // 在調(diào)用 adapter.notifyDatasetChanged() 方法時(shí),dataChanged 為 true
            // 默認(rèn)情況下,dataChanged 為 false
            boolean dataChanged = mDataChanged;

            // ...

            final int firstPosition = mFirstPosition;
            final RecycleBin recycleBin = mRecycler;
            if (dataChanged) {
                // 將當(dāng)前所有 item 的 View 添加到 RecycleBin 的 ScrapViews 中保存起來(lái)
                for (int i = 0; i < childCount; i++) {
                    recycleBin.addScrapView(getChildAt(i), firstPosition+i);
                }
            } else {
                // 將當(dāng)前所有 item 的 View 添加到 RecycleBin 的 ActiveViews 中保存起來(lái)
                recycleBin.fillActiveViews(childCount, firstPosition);
            }

            // 清除所有子 View
            detachAllViewsFromParent();
            recycleBin.removeSkippedScrap();

            // 通常情況下,mLayoutMode 為 LAYOUT_NORMAL,走 default
            switch (mLayoutMode) {
            // ...
            
            default:
                // 第一次 onLayout 時(shí),childCount 為 0
                if (childCount == 0) {
                    // 判斷布局是從上往下還是從下往上,默認(rèn)為從上往下,進(jìn)入 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. 后續(xù)已經(jīng)存在子 View,但數(shù)據(jù)發(fā)送改變時(shí),例如調(diào)用了 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 移動(dòng)到 ScrapViews 中
            recycleBin.scrapActiveViews();

            // ...
            
            // 布局完成后,重置 layoutMode 和 mDataChanged 
            mLayoutMode = LAYOUT_NORMAL;
            mDataChanged = false;
    
            // ...
        } // ...
    }

該方法較長(zhǎng),只列出了主要代碼。重點(diǎn)看 switch 塊,這里根據(jù) layoutMode 進(jìn)行布局,一般走 default?,F(xiàn)在先分析第一次 onLayout 的情況,默認(rèn)從上往下布局,調(diào)用 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 的合理性,之后調(diào)用了 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;
        }

        // 當(dāng)子元素超出當(dāng)前屏幕或全部子元素遍歷完時(shí),退出循環(huán)
        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;
    }

重點(diǎn)看 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;
            }
        }

        // 通過(guò) obtainView 方法獲取子 View
        final View child = obtainView(position, mIsScrap);

        // 測(cè)量和放置子 View
        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

        return child;
    }

該方法先從 RecycleBin 的 ActiveViews 或通過(guò) obtainView 方法獲取子 View,再通過(guò) setupChild 方法測(cè)量和放置子 View。

第一次 layout 時(shí),RecycleBin 并沒(méi)有緩存 ActiveViews,所以只能通過(guò) obtainView 方法獲取子 View,ListView 并沒(méi)有該方法,該方法在其父類(lèi) AbsListView 中

AbsListView#obtainView

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

        // 從 ScrapViews 中獲取一個(gè) scrapView
        final View scrapView = mRecycler.getScrapView(position);
        // 從 Adapter 的 getView 方法獲取子 View,并將剛才得到的 scrapView 作為第二個(gè)參數(shù)傳入
        final View child = mAdapter.getView(position, scrapView, this);
        if (scrapView != null) {
            if (child != scrapView) {
                // 該 scrapView 沒(méi)有被用戶(hù)利用,將其返回到 ScrapViews 中
                mRecycler.addScrapView(scrapView, position);
            } else {
                if (child.isTemporarilyDetached()) {
                    isScrap[0] = true;
                    child.dispatchFinishTemporaryDetach();
                } else {
                    isScrap[0] = false;
                }

            }
        }
        // ...

        return child;
    }

在該方法,先從 ScrapViews 中獲取一個(gè) scrapView,之后調(diào)用 Adapter 的 getView 方法獲取子 View,并將剛才得到的 scrapView 作為第二個(gè)參數(shù)傳入。

在第一次 layout 中,由于 scrapView 為 null,所以所有的子 View 都是通過(guò) LayoutInflater 的 inflate 方法加載出來(lái)的,相對(duì)比較耗時(shí),不過(guò)一開(kāi)始只會(huì)加載第一屏的數(shù)據(jù),這樣就保證了 ListView 的內(nèi)容能夠迅速顯示在屏幕上。

第二次 layout

在某些手機(jī)版本中(9.0 版本好像沒(méi)有這種情況),View 在展示到界面上時(shí)會(huì)經(jīng)歷兩次 onLayout。如果 ListView 進(jìn)行了兩次 onLayout 的話(huà),就會(huì)存在一份重復(fù)的元素了。因此 ListView 在 layoutChildren 中對(duì)第二次 layout 做了處理,非常巧妙地解決了這個(gè)問(wèn)題。

下面就來(lái)分析一些 ListView 的第二次 layout 過(guò)程,首先看 layoutChildren 方法中的變化:

ListView#layoutChildren

    @Override
    protected void layoutChildren() {
        // ...

        try {
            // ...

            // 當(dāng)前擁有的子 View 個(gè)數(shù),第二次 layout 時(shí)子 View 個(gè)數(shù)不為 0
            final int childCount = getChildCount();

            // ...

            // 將當(dāng)前所有 item 的 View 添加到 RecycleBin 的 ActiveViews 中保存起來(lái)
            recycleBin.fillActiveViews(childCount, firstPosition);

            // 清除所有子 View
            detachAllViewsFromParent();

            // 通常情況下,mLayoutMode 為 LAYOUT_NORMAL,走 default
            switch (mLayoutMode) {
            // ...
            
            default:
                if (childCount == 0) {// ...}
                // 第二次 layout 時(shí)進(jìn)入 else 塊
                else {
                    // 一開(kāi)始沒(méi)有選中 item,mSelectedPosition 的值為 -1
                    // 所以不會(huì)進(jìn)入 if 塊,而是調(diào)用 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 數(shù)量不為 0,所有子 View 先添加到 RecycleBin 的 ActiveViews 中保存起來(lái)。然后清除所有舊的子 View。由于子 View 數(shù)量不為 0,之后會(huì)調(diào)用 fillSpecific 方法:

ListView#fillSpecific

    private View fillSpecific(int position, int top) {
        boolean tempIsSelected = position == mSelectedPosition;
        
        // 獲取并設(shè)置當(dāng)前 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 為中心,分別向上和向下獲取并設(shè)置其他子 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;
        }
    }

該方法先設(shè)置當(dāng)前 position 的子 View,然后以 position 為中心,分別向上和向下設(shè)置其他子 View。由于第二次 layout 時(shí)傳入的 position 就是第一個(gè)子 View 的位置,所以和第一次 layout 的布局順序是差不多的。獲取并設(shè)置子 View 還是通過(guò) makeAndAddView 方法。

ListView#makeAndAddView

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

這里和第一次 layout 不同的是,由于之前已經(jīng)把舊的子 View 存到了 ActiveViews,所以可以直接從 ActiveViews 中獲取到子 View,無(wú)需再通過(guò) inflate 方法加載子 View。

小結(jié)

第一次 layout 時(shí),由于當(dāng)前子 View 數(shù)量為 0,且在 RecycleBin 的 ActiveViews 和 ScrapViews 都沒(méi)有緩存,所以只能在 Adapter 的 getView 方法中,通過(guò) LayoutInflate 的 inflate 方法加載子 View,相對(duì)來(lái)說(shuō)比較耗時(shí),不過(guò)一開(kāi)始只會(huì)加載第一屏的數(shù)據(jù),這樣就保證了 ListView 的內(nèi)容能夠迅速顯示在屏幕上。

在某些手機(jī)版本中,第一次顯示 ListView 時(shí)可能會(huì)發(fā)生兩次 layout。和第一次 layout 過(guò)程不同,在進(jìn)行第二次 layout 時(shí),子 View 數(shù)量不為 0,就可以先將所有子View 添加到 RecycleBin 的 ActiveViews 中保存起來(lái)。然后清除舊的子 View,之后再次設(shè)置新的子 View 時(shí),由于之前已經(jīng)把舊的子 View 存到了 ActiveViews,所以可以直接從 ActiveViews 中獲取到子 View,無(wú)需再通過(guò) inflate 方法加載子 View。

(注:在 Android 9.0 版本中,Button 顯示時(shí)調(diào)用了兩次 onMeasure、一次 onLayout、兩次 onDraw;TextView 顯示時(shí)調(diào)用了兩次 onMeasure、一次 onLayout、一次 onDraw;ListView 會(huì)調(diào)用多次 onMeasure、一次 onLayout、多次 onDraw。所以在 9.0 版本并不會(huì)發(fā)生第二次 layout。)

滑動(dòng)加載更多數(shù)據(jù)

上面 layout 過(guò)程分析的只是加載第一頁(yè)的數(shù)據(jù),如果有很多數(shù)據(jù),剩下的數(shù)據(jù)將會(huì)在滑動(dòng)過(guò)程中加載。下面將分析一下滑動(dòng)加載數(shù)據(jù)的過(guò)程。

該過(guò)程涉及到事件分發(fā),所以是從 AbsListView 的 onTouchEvent 方法開(kāi)始,滑動(dòng)對(duì)應(yīng) ACTION_MOVE,所以接下來(lái)調(diào)用 onTouchMove 方法,里面又有一個(gè) switch 語(yǔ)句判斷 mTouchMode,這里對(duì)應(yīng) TOUCH_MODE_SCROLL,所以接下來(lái)調(diào)用 scrollIfNeeded 方法,里面又繼續(xù)調(diào)用 trackMotionScroll 方法。

下面看一下 trackMotionScroll 方法:

AbsListView#trackMotionScroll

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

        // incrementalDeltaY < 0,說(shuō)明是向下滑動(dòng)(這里指內(nèi)容,手指是向上滑動(dòng)的)
        final boolean down = incrementalDeltaY < 0;

        // getHeaderViewsCount 和 getFooterViewsCount 默認(rèn)返回 0
        final int headerViewsCount = getHeaderViewsCount();
        final int footerViewsStart = mItemCount - getFooterViewsCount();

        int start = 0;  // 開(kāi)始移除的索引
        int count = 0;  // 移除的數(shù)量

        // 向下滑動(dòng)
        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 值大于等于滑動(dòng)的距離
                // 說(shuō)明該子 View 以及其后的 View 都在屏幕上,退出循環(huán)
                if (child.getBottom() >= top) {
                    break;
                } 
                // 如果該子 View 的 bottom 值小于滑動(dòng)的距離,說(shuō)明該子 View 已經(jīng)不在屏幕上
                else { 
                    count++;
                    int position = firstPosition + i;   // 該子 View 的索引
                    // 將不在屏幕的子 View 添加進(jìn) RecycleBin 的 ScrapViews 中
                    if (position >= headerViewsCount && position < footerViewsStart) {
                        child.clearAccessibilityFocus();
                        mRecycler.addScrapView(child, position);
                    }
                }
            }
        } else {
            // 向上滑動(dòng),和向下滑動(dòng)的過(guò)程相似,也是將不在屏幕上的子 View 添加進(jìn) RecycleBin 的 ScrapViews 中
            // ...
        }

        // 將不在屏幕的子 View 全部 detach 掉
        if (count > 0) {
            detachViewsFromParent(start, count);
            mRecycler.removeSkippedScrap();
        }

        // 讓所有的子 View 進(jìn)行相應(yīng)的偏移,達(dá)到內(nèi)容隨手指的拖動(dòng)而滾動(dòng)的效果
        offsetChildrenTopAndBottom(incrementalDeltaY);
        
        // 向下滑動(dòng)時(shí),更新 mFirstPosition(之后填充布局時(shí)會(huì)用到)
        if (down) {
            mFirstPosition += count;
        }

        final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
        // 如果第一個(gè) View 的頂部或最后一個(gè) View 的底部移入屏幕
        // 說(shuō)明要加載屏幕外的數(shù)據(jù)來(lái)填充布局
        if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
            fillGap(down);
        }
    
        // ...

        return false;
    }

該方法首先將滑出屏幕的子 View 添加進(jìn) RecycleBin 的 ScrapViews 中,并全部 detach 掉。然后讓剩下的子 View 進(jìn)行相應(yīng)的偏移,達(dá)到內(nèi)容隨手指的拖動(dòng)而滾動(dòng)的效果。最后調(diào)用 fillGap 方法加載屏幕外的數(shù)據(jù)來(lái)填充布局,fillGap 在 AbsListView 是一個(gè)抽象方法,ListView 中有具體實(shí)現(xiàn)。

ListView#fillGap

    @Override
    void fillGap(boolean down) {
        final int count = getChildCount();
        // 如果是向下滑動(dòng),就通過(guò) 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());
        } 
        // 如果是向下滑動(dòng),就通過(guò) 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());
        }
    }

該方法根據(jù)滑動(dòng)方向,調(diào)用 fillDown 或 fillUp 方法添加子 View,無(wú)論調(diào)用拿個(gè)方法,最終都是調(diào)用 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;
            }
        }

        // 通過(guò) obtainView 方法獲取子 View
        final View child = obtainView(position, mIsScrap);

        // 測(cè)量和放置子 View
        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

        return child;
    }

先從 RecycleBin 的 ActiveViews 中獲取,如果還沒(méi)有進(jìn)行第二次 layout 的話(huà),是可以獲取到的,如果已經(jīng)進(jìn)行過(guò)第二次 layout,那么就獲取不到了,因?yàn)榈诙?layout 的時(shí)候已經(jīng)從 ActiveViews 中拿到過(guò)子 View,而 ActiveViews 不能重復(fù)利用,所以就獲取不到了。

如果 ActiveViews 獲取不到,就會(huì)調(diào)用 obtainView 方法獲?。?/p>

AbsListView#obtainView

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

        // 從 ScrapViews 中獲取一個(gè) scrapView
        final View scrapView = mRecycler.getScrapView(position);
        // 從 Adapter 的 getView 方法獲取子 View,并將剛才得到的 scrapView 作為第二個(gè)參數(shù)傳入
        final View child = mAdapter.getView(position, scrapView, this);
        if (scrapView != null) {
            if (child != scrapView) {
                // 該 scrapView 沒(méi)有被用戶(hù)利用,將其返回到 ScrapViews 中
                mRecycler.addScrapView(scrapView, position);
            } else {
                if (child.isTemporarilyDetached()) {
                    isScrap[0] = true;
                    child.dispatchFinishTemporaryDetach();
                } else {
                    isScrap[0] = false;
                }

            }
        }
        // ...

        return child;
    }

這次和第一次 layout 的情況不一樣,因?yàn)橹鞍岩瞥聊坏淖?View 添加到了 ScrapViews 中,所以現(xiàn)在就可以從 ScrapViews 中得到之前移除的子 View,并傳入 Adapter 的 getView 方法。用戶(hù)就可以利用這個(gè)緩存 View,不用再 inflate 一個(gè)子 View 了。

小結(jié)

ListView 在滑動(dòng)時(shí),先將滑出屏幕的子 View 添加進(jìn) RecycleBin 的 ScrapViews 中,并從父布局中 detach 掉。然后讓剩下的子 View 進(jìn)行相應(yīng)的偏移,達(dá)到內(nèi)容隨手指的拖動(dòng)而滾動(dòng)的效果。最后通過(guò)加載屏幕外的數(shù)據(jù)來(lái)填充布局,這時(shí)就可以從 ScrapViews 中得到之前移除的子 View,并傳入 Adapter 的 getView 方法。用戶(hù)就可以重復(fù)利用這個(gè)緩存 View,無(wú)需再重新 inflate 一個(gè)子 View。

Adapter 相關(guān)

ListView 只是負(fù)責(zé)展示各子 View,各子 View 具體如何填充數(shù)據(jù)是交由 Adapter 來(lái)完成的。ListView 通過(guò) setAdapter 方法和 Adapter 建立聯(lián)系。先看一下該方法:

ListView#setAdapter

    @Override
    public void setAdapter(ListAdapter adapter) {
        
        // 如果之前綁定過(guò) Adapter,先取消注冊(cè) AdapterDataSetObserver
        if (mAdapter != null && mDataSetObserver != null) {
            mAdapter.unregisterDataSetObserver(mDataSetObserver);
        }

        resetList();
        mRecycler.clear();

        // 設(shè)置新的 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;
            // 設(shè)置 item 個(gè)數(shù)
            mItemCount = mAdapter.getCount();
            checkFocus();

            // 生成并在 Adapter 中注冊(cè) AdapterDataSetObserver,用于通知數(shù)據(jù)源的改變
            mDataSetObserver = new AdapterDataSetObserver();
            mAdapter.registerDataSetObserver(mDataSetObserver);

            mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());

            //...
        } // ...

        // 進(jìn)行視圖重繪
        requestLayout();
    }

在該方法中,ListView 綁定傳入的 adapter,并為 adapter 注冊(cè) AdapterDataSetObserver,用于通知數(shù)據(jù)源的改變。 最后調(diào)用 requestLayout 方法,該方法最終會(huì)調(diào)用 ListView 的 onLayout,來(lái)到第一次 onLayout 的過(guò)程。

如果數(shù)據(jù)源發(fā)生了改變,想要更新 ListView 的時(shí)候,我們會(huì)調(diào)用 Adapter 的 notifyDataSetChanged 方法:

BaseAdapter#notifyDataSetChanged

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

又調(diào)用了 DataSetObservable 的 notifyChanged:

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

這里的 mObservers 定義在 DataSetObservable 的父類(lèi) Observable 中:

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

mObservers 的元素是從哪里來(lái)的呢?要從 setAdapter 的這一句說(shuō)起:

    mAdapter.registerDataSetObserver(mDataSetObserver);

這一句最終調(diào)用了 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 注冊(cè)的 AdapterDataSetObserver 添加進(jìn)了 mObservers 中。

所以饒了一大圈,Adapter 的 notifyDataSetChanged 方法最終調(diào)用了 AdapterDataSetObserver(AdapterView 的一個(gè)內(nèi)部類(lèi))的 onChanged 方法:

AdapterDataSetObserver#onChanged

    @Override
    public void onChanged() {
        // 將 mDataChanged 屬性設(shè)置為 true
        mDataChanged = true;
        mOldItemCount = mItemCount;
        // 更新 item 數(shù)量
        mItemCount = getAdapter().getCount();
        
        // ...
        
        // 最后進(jìn)行視圖重繪
        requestLayout();
    }

在該方法中,首先將 mDataChanged 屬性設(shè)置為 true,并更新 item 數(shù)量,最后進(jìn)行視圖重繪,在 onLayout 中更新子 View。

寫(xiě)在最后

到此為止,對(duì)于 ListView 的 分析就告一段落了。在分析 ListView 的過(guò)程,發(fā)現(xiàn)經(jīng)常遇到也是最重要的就是 onLayout 過(guò)程以及 RecycleBin 機(jī)制。

無(wú)論是設(shè)置 Adapter 還是通知 Adapter 更新數(shù)據(jù)的過(guò)程,最終都會(huì)回到視圖重繪,也就是 onLayout。而 onLayout 過(guò)程也會(huì)根據(jù)是第一次 layout、第二次 layout 還是數(shù)據(jù)源改變的情況從不同途徑獲取子 View,是通過(guò) inflate 加載還是從 RecycleBin 的 ActiveViews 或 ScrapViews 獲取。

RecycleBin 對(duì)子 View 的回收也是 ListView 的一個(gè)重點(diǎn)或是巧妙之處,在第二次 layout 時(shí),會(huì)把子 View 添加到 RecycleBin 的 ActiveViews 中,之后獲取新的子 View 時(shí)就可以直接從 ActiveViews 獲取。在滑動(dòng)過(guò)程中,滑出屏幕的子 View 又會(huì)被添加到 RecycleBin 的 ScrapViews 中,在之后填充布局時(shí)重新利用。

參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 本文主要內(nèi)容 Listview是一種常用的控件,它的主要特點(diǎn)是能夠復(fù)用,上下滑動(dòng)時(shí)不至于卡頓,內(nèi)存波動(dòng)等。要實(shí)現(xiàn)這...
    某昆閱讀 757評(píng)論 1 2
  • 本文主要從源碼的角度分析 ListView 的工作原理和使用方法,如有不正確的地方歡迎大家指正。 Adapter ...
    small_yg閱讀 648評(píng)論 0 0
  • ListView源碼分析 項(xiàng)目中使用ListView還是挺多的,之前看過(guò)幾次,很是容易遺忘,今特做記錄如下 And...
    Nvsleep閱讀 3,496評(píng)論 2 11
  • Android源碼之ListView的適配器模式 Adapter Pattern適配器模式分為兩種,即類(lèi)適配器,對(duì)...
    cxm11閱讀 4,360評(píng)論 1 30
  • vue cli創(chuàng)建后的目錄vue cli創(chuàng)建后的目錄.png babel 配置 配置文件就是.babelrc,使...
    deep_sadness閱讀 1,779評(píng)論 1 2

友情鏈接更多精彩內(nèi)容