ListView 源碼分析

前言

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

RecycleBin

RecycleBin 是 AbsListView 中的一個內(nèi)部類阐虚,所以繼承于 AbsListView 的子類,也就是 ListView 和 GridView逊彭,都可以使用這個類侮叮。RecycleBin 機制是 ListView 能夠?qū)崿F(xiàn)成百上千條數(shù)據(jù)都不會 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 是在布局開始時在屏幕上顯示的那些視圖圆到。在布局結(jié)束時芽淡,ActiveViews 中的所有視圖都降級為 ScrapViews挣菲。ScrapViews 是適配器可能使用的舊視圖掷邦,避免不必要地重新分配視圖抚岗。

主要成員變量

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

    // 之所以使用集合數(shù)組宣蔚,是因為可能有多種類型的 item胚委,同一類型的廢棄 item 放在同一 list 中
    private ArrayList<View>[] mScrapViews;
    
    // 指向 scrapViews[0](只有一種類型 item 的時候使用它)
    private ArrayList<View> mCurrentScrap;
    
    private ArrayList<View> mSkippedScrap;

主要方法

先看一下主要的方法:

  • void fillActiveViews(int childCount, int firstActivePosition):第一個參數(shù)表示要存儲的 View 的數(shù)量亩冬,第二個參數(shù)表示 ListView 中第一個可見元素的索引。調(diào)用該方法后就可以根據(jù)參數(shù)將 ListView 中的指定元素存儲到 mActiveViews 數(shù)組中鸠姨。
  • View getActiveView(int position):根據(jù)索引獲取相應(yīng)的 ActiveView讶迁,獲取到后就將該 View 從 ActiveViews 從移除巍糯,下次再獲取該位置的 ActiveView祟峦,將會返回 false宅楞,也就是說 ActiveView 不能被重復(fù)利用厌衙。
  • void addScrapView(View scrap, int position):該方法將一個廢棄(比如滾動出了屏幕)的 View 緩存起來婶希。RecycleBin 中使用 mScrapViews 和 mCurrentScrap 來存儲廢棄的 View喻杈。
  • View getScrapView(int position):根據(jù)索引找到對應(yīng)類型的 ScrapViews筒饰,并從中獲取一個 ScrapView 返回瓷们。
  • public void setViewTypeCount(int viewTypeCount):Adapter 可以重寫 getViewTypeCount() 方法來表示 ListView 有幾種類型的 item换棚,而 setViewTypeCount 根據(jù)類型數(shù)來初始化 mScrapViews 數(shù)組,mCurrentScrap 指向第 0 號數(shù)組歹茶,所以如果只有一種類型,就可以使用 mCurrentScrap燎孟;如果有多種類型揩页,就使用 mScrapViews爆侣。

onLayout

View 的三大流程中兔仰,對于 ListView 而言乎赴,onMeasure 并沒有什么特別的榕吼,因為它終歸是一個 View友题,占用的空間最多也就是整個屏幕度宦。onDraw 也沒有什么意義戈抄,因為 ListView 本身并不負(fù)責(zé)繪制划鸽,繪制的任務(wù)交由子元素自己完成裸诽。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;
            // 當(dāng)前擁有的子 View 個數(shù)恨统,第一次 layout 時子 View 個數(shù)為 0
            final int childCount = getChildCount();

            // ...

            // 在調(diào)用 adapter.notifyDatasetChanged() 方法時畜埋,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 中保存起來
                for (int i = 0; i < childCount; i++) {
                    recycleBin.addScrapView(getChildAt(i), firstPosition+i);
                }
            } else {
                // 將當(dāng)前所有 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) {
                    // 判斷布局是從上往下還是從下往上码耐,默認(rè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ā)送改變時钞速,例如調(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 移動到 ScrapViews 中
            recycleBin.scrapActiveViews();

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

該方法較長驾凶,只列出了主要代碼调违。重點看 switch 塊技肩,這里根據(jù) layoutMode 進行布局虚婿,一般走 default∪蝗現(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)前屏幕或全部子元素遍歷完時辛蚊,退出循環(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;
    }

重點看 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 作為第二個參數(shù)傳入
        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锣披,之后調(diào)用 Adapter 的 getView 方法獲取子 View贞间,并將剛才得到的 scrapView 作為第二個參數(shù)傳入。

在第一次 layout 中雹仿,由于 scrapView 為 null增热,所以所有的子 View 都是通過 LayoutInflater 的 inflate 方法加載出來的,相對比較耗時胧辽,不過一開始只會加載第一屏的數(shù)據(jù)峻仇,這樣就保證了 ListView 的內(nèi)容能夠迅速顯示在屏幕上。

第二次 layout

在某些手機版本中(9.0 版本好像沒有這種情況)邑商,View 在展示到界面上時會經(jīng)歷兩次 onLayout摄咆。如果 ListView 進行了兩次 onLayout 的話,就會存在一份重復(fù)的元素了奠骄。因此 ListView 在 layoutChildren 中對第二次 layout 做了處理豆同,非常巧妙地解決了這個問題。

下面就來分析一些 ListView 的第二次 layout 過程含鳞,首先看 layoutChildren 方法中的變化:

ListView#layoutChildren

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

        try {
            // ...

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

            // ...

            // 將當(dāng)前所有 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 塊,而是調(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 中保存起來。然后清除所有舊的子 View桅狠。由于子 View 數(shù)量不為 0讼载,之后會調(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 時傳入的 position 就是第一個子 View 的位置漩符,所以和第一次 layout 的布局順序是差不多的一喘。獲取并設(shè)置子 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 不同的是凸克,由于之前已經(jīng)把舊的子 View 存到了 ActiveViews议蟆,所以可以直接從 ActiveViews 中獲取到子 View,無需再通過 inflate 方法加載子 View萎战。

小結(jié)

第一次 layout 時咐容,由于當(dāng)前子 View 數(shù)量為 0,且在 RecycleBin 的 ActiveViews 和 ScrapViews 都沒有緩存撞鹉,所以只能在 Adapter 的 getView 方法中疟丙,通過 LayoutInflate 的 inflate 方法加載子 View颖侄,相對來說比較耗時鸟雏,不過一開始只會加載第一屏的數(shù)據(jù),這樣就保證了 ListView 的內(nèi)容能夠迅速顯示在屏幕上览祖。

在某些手機版本中孝鹊,第一次顯示 ListView 時可能會發(fā)生兩次 layout。和第一次 layout 過程不同展蒂,在進行第二次 layout 時又活,子 View 數(shù)量不為 0,就可以先將所有子View 添加到 RecycleBin 的 ActiveViews 中保存起來锰悼。然后清除舊的子 View柳骄,之后再次設(shè)置新的子 View 時,由于之前已經(jīng)把舊的子 View 存到了 ActiveViews箕般,所以可以直接從 ActiveViews 中獲取到子 View耐薯,無需再通過 inflate 方法加載子 View。

(注:在 Android 9.0 版本中丝里,Button 顯示時調(diào)用了兩次 onMeasure曲初、一次 onLayout、兩次 onDraw杯聚;TextView 顯示時調(diào)用了兩次 onMeasure臼婆、一次 onLayout、一次 onDraw幌绍;ListView 會調(diào)用多次 onMeasure颁褂、一次 onLayout、多次 onDraw傀广。所以在 9.0 版本并不會發(fā)生第二次 layout颁独。)

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

上面 layout 過程分析的只是加載第一頁的數(shù)據(jù),如果有很多數(shù)據(jù)主儡,剩下的數(shù)據(jù)將會在滑動過程中加載奖唯。下面將分析一下滑動加載數(shù)據(jù)的過程。

該過程涉及到事件分發(fā)糜值,所以是從 AbsListView 的 onTouchEvent 方法開始丰捷,滑動對應(yīng) ACTION_MOVE坯墨,所以接下來調(diào)用 onTouchMove 方法,里面又有一個 switch 語句判斷 mTouchMode病往,這里對應(yīng) TOUCH_MODE_SCROLL捣染,所以接下來調(diào)用 scrollIfNeeded 方法,里面又繼續(xù)調(diào)用 trackMotionScroll 方法停巷。

下面看一下 trackMotionScroll 方法:

AbsListView#trackMotionScroll

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

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

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

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

        // 向下滑動
        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 都在屏幕上畔勤,退出循環(huán)
                if (child.getBottom() >= top) {
                    break;
                } 
                // 如果該子 View 的 bottom 值小于滑動的距離蕾各,說明該子 View 已經(jīng)不在屏幕上
                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 進行相應(yīng)的偏移式曲,達到內(nèi)容隨手指的拖動而滾動的效果
        offsetChildrenTopAndBottom(incrementalDeltaY);
        
        // 向下滑動時缸榛,更新 mFirstPosition(之后填充布局時會用到)
        if (down) {
            mFirstPosition += count;
        }

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

        return false;
    }

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

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

該方法根據(jù)滑動方向躯枢,調(diào)用 fillDown 或 fillUp 方法添加子 View,無論調(diào)用拿個方法锄蹂,最終都是調(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;
            }
        }

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

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

        return child;
    }

先從 RecycleBin 的 ActiveViews 中獲取,如果還沒有進行第二次 layout 的話敬扛,是可以獲取到的,如果已經(jīng)進行過第二次 layout谍珊,那么就獲取不到了,因為第二次 layout 的時候已經(jīng)從 ActiveViews 中拿到過子 View砌滞,而 ActiveViews 不能重復(fù)利用,所以就獲取不到了坏怪。

如果 ActiveViews 獲取不到贝润,就會調(diào)用 obtainView 方法獲取:

AbsListView#obtainView

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

        // 從 ScrapViews 中獲取一個 scrapView
        final View scrapView = mRecycler.getScrapView(position);
        // 從 Adapter 的 getView 方法獲取子 View陕悬,并將剛才得到的 scrapView 作為第二個參數(shù)傳入
        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 中捉超,所以現(xiàn)在就可以從 ScrapViews 中得到之前移除的子 View,并傳入 Adapter 的 getView 方法唯绍。用戶就可以利用這個緩存 View拼岳,不用再 inflate 一個子 View 了。

小結(jié)

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

Adapter 相關(guān)

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

ListView#setAdapter

    @Override
    public void setAdapter(ListAdapter adapter) {
        
        // 如果之前綁定過 Adapter亭引,先取消注冊 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 個數(shù)
            mItemCount = mAdapter.getCount();
            checkFocus();

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

            mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());

            //...
        } // ...

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

在該方法中焙蚓,ListView 綁定傳入的 adapter,并為 adapter 注冊 AdapterDataSetObserver,用于通知數(shù)據(jù)源的改變购公。 最后調(diào)用 requestLayout 方法赵哲,該方法最終會調(diào)用 ListView 的 onLayout,來到第一次 onLayout 的過程枫夺。

如果數(shù)據(jù)源發(fā)生了改變橡庞,想要更新 ListView 的時候扒最,我們會調(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 的父類 Observable 中:

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

mObservers 的元素是從哪里來的呢?要從 setAdapter 的這一句說起:

    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 注冊的 AdapterDataSetObserver 添加進了 mObservers 中俯渤。

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

AdapterDataSetObserver#onChanged

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

在該方法中,首先將 mDataChanged 屬性設(shè)置為 true抡四,并更新 item 數(shù)量床嫌,最后進行視圖重繪厌处,在 onLayout 中更新子 View阔涉。

寫在最后

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

無論是設(shè)置 Adapter 還是通知 Adapter 更新數(shù)據(jù)的過程些举,最終都會回到視圖重繪户魏,也就是 onLayout叼丑。而 onLayout 過程也會根據(jù)是第一次 layout幢码、第二次 layout 還是數(shù)據(jù)源改變的情況從不同途徑獲取子 View症副,是通過 inflate 加載還是從 RecycleBin 的 ActiveViews 或 ScrapViews 獲取贞铣。

RecycleBin 對子 View 的回收也是 ListView 的一個重點或是巧妙之處辕坝,在第二次 layout 時酱畅,會把子 View 添加到 RecycleBin 的 ActiveViews 中纺酸,之后獲取新的子 View 時就可以直接從 ActiveViews 獲取餐蔬。在滑動過程中樊诺,滑出屏幕的子 View 又會被添加到 RecycleBin 的 ScrapViews 中词爬,在之后填充布局時重新利用顿膨。

參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市顾画,隨后出現(xiàn)的幾起案子研侣,更是在濱河造成了極大的恐慌庶诡,老刑警劉巖末誓,帶你破解...
    沈念sama閱讀 211,561評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件喇澡,死亡現(xiàn)場離奇詭異晴玖,居然都是意外死亡呕屎,警方通過查閱死者的電腦和手機敬察,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評論 3 385
  • 文/潘曉璐 我一進店門琅催,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人侠碧,你說我怎么就攤上這事〈墒剑” “怎么了贸典?”我有些...
    開封第一講書人閱讀 157,162評論 0 348
  • 文/不壞的土叔 我叫張陵据过,是天一觀的道長绳锅。 經(jīng)常有香客問我鳞芙,道長原朝,這世上最難降的妖魔是什么竿拆? 我笑而不...
    開封第一講書人閱讀 56,470評論 1 283
  • 正文 為了忘掉前任煌贴,我火速辦了婚禮锥忿,結(jié)果婚禮上敬鬓,老公的妹妹穿的比我還像新娘。我一直安慰自己杈抢,他們只是感情好仑性,可當(dāng)我...
    茶點故事閱讀 65,550評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著豹储,像睡著了一般淘这。 火紅的嫁衣襯著肌膚如雪朦乏。 梳的紋絲不亂的頭發(fā)上氧骤,一...
    開封第一講書人閱讀 49,806評論 1 290
  • 那天刽锤,我揣著相機與錄音,去河邊找鬼宋彼。 笑死输涕,一個胖子當(dāng)著我的面吹牛莱坎,可吹牛的內(nèi)容都是我干的碴卧。 我是一名探鬼主播住册,決...
    沈念sama閱讀 38,951評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼烫葬!你這毒婦竟也來了界弧?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,712評論 0 266
  • 序言:老撾萬榮一對情侶失蹤搭综,失蹤者是張志新(化名)和其女友劉穎垢箕,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體兑巾,經(jīng)...
    沈念sama閱讀 44,166評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡条获,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,510評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蒋歌。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片修档。...
    茶點故事閱讀 38,643評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤六水,帶...
    沈念sama閱讀 34,306評論 4 330
  • 正文 年R本政府宣布啡莉,位于F島的核電站轨帜,受9級特大地震影響苟弛,放射性物質(zhì)發(fā)生泄漏荔睹。R本人自食惡果不足惜吨拗,卻給世界環(huán)境...
    茶點故事閱讀 39,930評論 3 313
  • 文/蒙蒙 一像鸡、第九天 我趴在偏房一處隱蔽的房頂上張望蛔钙。 院中可真熱鬧,春花似錦、人聲如沸孵班。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽礁苗。三九已至吱抚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間袱讹,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人籽前。 一個月前我還...
    沈念sama閱讀 46,351評論 2 360
  • 正文 我出身青樓齐婴,卻偏偏與公主長得像柠偶,于是被迫代替她去往敵國和親涎嚼。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,509評論 2 348

推薦閱讀更多精彩內(nèi)容

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