Android源碼分析之ListView源碼

系列文章

  1. Android源碼分析之ListView源碼
  2. Android源碼分析之RecyclerView源碼分析(一)——繪制流程
  3. Android源碼分析之RecyclerView源碼分析(二)——緩存機制

前言

ListView 是用來展示大量數(shù)據(jù)的控件沪铭,且不會因為展示大量數(shù)據(jù)而出現(xiàn)內(nèi)存溢出的現(xiàn)象喘蟆,其原因是相關(guān)緩存機制保證了內(nèi)存的合理使用。

ListView的使用也相對比較簡單蚁吝,大家也都會慨飘,現(xiàn)在官方基本都推薦使用RecyclerView去替代ListView某残,二者之間有相似之處,也有不同之處蹈丸,本文先分析ListView的源碼成黄,重點是緩存的實現(xiàn)原理,后續(xù)再補充RecyclerView的原理分析逻杖,并將二者進行對比討論奋岁。

ListView的使用可以參考ListView簡單實用

ListView繼承自AbsListViewAbsListView又繼承自AdapterView荸百,AdapterView繼承自ViewGroup闻伶。

ListView繼承關(guān)系.png

RecycleBin機制

在ListView的緩存機制中,有一個類我們必須提前了解:RecycleBin够话,它是ListView緩存的核心機制蓝翰。RecycleBin是AbsListView的一個內(nèi)部類光绕,而ListView繼承AbsListView,所以ListView可以使用這個機制畜份。下面是RecycleBin的部分關(guān)鍵源碼:

class RecycleBin {
        private RecyclerListener mRecyclerListener;

        private int mFirstActivePosition;
        
        // 存儲View
        private View[] mActiveViews = new View[0];
        
        // 存儲廢棄View
        private ArrayList<View>[] mScrapViews;

        private int mViewTypeCount;
        
        // 存儲廢棄View
        private ArrayList<View> mCurrentScrap;
        
        // Adapter當中可以重寫一個getViewTypeCount()來表示ListView中有幾種類型的數(shù)據(jù)項
        // 而setViewTypeCount()方法的作用就是為每種類型的數(shù)據(jù)項都單獨啟用一個RecycleBin緩存機制
        public void setViewTypeCount(int viewTypeCount) {
            if (viewTypeCount < 1) {
                throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
            }
            //noinspection unchecked
            ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
            for (int i = 0; i < viewTypeCount; i++) {
                scrapViews[i] = new ArrayList<View>();
            }
            mViewTypeCount = viewTypeCount;
            mCurrentScrap = scrapViews[0];
            mScrapViews = scrapViews;
        }
        
        // 第一個參數(shù)表示要存儲的view的數(shù)量诞帐,第二個參數(shù)表示ListView中第一個可見元素的position值
        // 根據(jù)傳入的參數(shù)來將ListView中的指定元素存儲到mActiveViews數(shù)組當中。
        void fillActiveViews(int childCount, int firstActivePosition) {
            if (mActiveViews.length < childCount) {
                mActiveViews = new View[childCount];
            }
            mFirstActivePosition = firstActivePosition;

            final View[] activeViews = mActiveViews;
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);
                AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
                if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                    activeViews[i] = child;
                    lp.scrappedFromPosition = firstActivePosition + i;
                }
            }
        }
        
        // 從mActiveViews數(shù)組當中獲取數(shù)據(jù)
        // 該方法接收一個position參數(shù)爆雹,表示元素在ListView當中的位置
        // 方法內(nèi)部會自動將position值轉(zhuǎn)換成mActiveViews數(shù)組對應的下標值
        // mActiveViews當中所存儲的View停蕉,一旦被獲取了之后就會從mActiveViews當中移除
        // 下次獲取同樣位置的View將會返回null,也就是說mActiveViews不能被重復利用
        View getActiveView(int position) {
            int index = position - mFirstActivePosition;
            final View[] activeViews = mActiveViews;
            if (index >=0 && index < activeViews.length) {
                final View match = activeViews[index];
                activeViews[index] = null;
                return match;
            }
            return null;
        }
        
        // 從廢棄緩存中取出一個View钙态,這些廢棄緩存中的View是沒有順序可言的
        // 因此getScrapView()方法中的算法也非常簡單
        // 就是直接從mCurrentScrap當中獲取尾部的一個scrap view進行返回
        View getScrapView(int position) {
            final int whichScrap = mAdapter.getItemViewType(position);
            if (whichScrap < 0) {
                return null;
            }
            if (mViewTypeCount == 1) {
                return retrieveFromScrap(mCurrentScrap, position);
            } else if (whichScrap < mScrapViews.length) {
                return retrieveFromScrap(mScrapViews[whichScrap], position);
            }
            return null;
        }

        // 將一個廢棄的View進行緩存
        // 該方法接收一個View參數(shù)谷徙,當有某個View確定要廢棄掉的時候(比如滾動出了屏幕)
        // 應該調(diào)用這個方法來對View進行緩存
        void addScrapView(View scrap, int position) {
            final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
            if (lp == null) {
                return;
            }

            lp.scrappedFromPosition = position;

            final int viewType = lp.viewType;
            if (!shouldRecycleViewType(viewType)) {
                if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                    getSkippedScrap().add(scrap);
                }
                return;
            }

            scrap.dispatchStartTemporaryDetach();

            notifyViewAccessibilityStateChangedIfNeeded(
                    AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);

            final boolean scrapHasTransientState = scrap.hasTransientState();
            if (scrapHasTransientState) {
                if (mAdapter != null && mAdapterHasStableIds) {
                    if (mTransientStateViewsById == null) {
                        mTransientStateViewsById = new LongSparseArray<>();
                    }
                    mTransientStateViewsById.put(lp.itemId, scrap);
                } else if (!mDataChanged) {
                    if (mTransientStateViews == null) {
                        mTransientStateViews = new SparseArray<>();
                    }
                    mTransientStateViews.put(position, scrap);
                } else {
                    getSkippedScrap().add(scrap);
                }
            } else {
                if (mViewTypeCount == 1) {
                    mCurrentScrap.add(scrap);
                } else {
                    mScrapViews[viewType].add(scrap);
                }

                if (mRecyclerListener != null) {
                    mRecyclerListener.onMovedToScrapHeap(scrap);
                }
            }
        }
    }

下面就上述幾個關(guān)鍵變量和方法做一些說明:

  • mActiveViews:用來存放正在展示在屏幕上的view,從顯示在屏幕山上的第一個view到最后一個view
  • mScrapViews存放可以由適配器用作convert view的view驯绎,是一個數(shù)組完慧,數(shù)組的每個元素類型為ArrayList<View>
  • mCurrentScrap是mScrapViews的第0個元素,當view種類數(shù)量為1時存放廢棄view
  • fillActiveViews():這個方法接收兩個參數(shù)剩失,第一個參數(shù)表示mActiveViews數(shù)組最小要保存的View數(shù)量屈尼,第二個參數(shù)表示ListView中第一個可見元素的position值。根據(jù)傳入的參數(shù)來將ListView中的指定元素存儲到mActiveViews數(shù)組當中拴孤。
  • getActiveView()從mActiveViews數(shù)組當中取出特定元素脾歧,position參數(shù)表示元素在ListView當中的位置,方法內(nèi)部會自動將position值轉(zhuǎn)換成mActiveViews數(shù)組對應的下標值演熟,mActiveViews當中所存儲的View鞭执,一旦被獲取了之后就會從mActiveViews當中移除,下次獲取同樣位置的View將會返回null芒粹,也就是說mActiveViews不能被重復利用兄纺。如果在mActiveViews數(shù)組中沒有找到,則返回null化漆。
  • addScrapView()將一個廢棄的View進行緩存估脆,該方法接收一個View參數(shù),當有某個View確定要廢棄掉的時候(比如滾動出了屏幕)座云,應該調(diào)用這個方法來對View進行緩存疙赠,當view類型為1時則用mCurrentScrap存儲廢棄view,否則使用mScrapViews添加廢棄view朦拖。
  • getScrapView(): 從廢棄緩存中取出一個View圃阳,這些廢棄緩存中的View是沒有順序可言,就是直接從mCurrentScrap當中獲取尾部的一個scrap view進行返回
  • setViewTypeCount():Adapter當中可以重寫一個getViewTypeCount()來表示ListView中有幾種類型的數(shù)據(jù)項璧帝,而setViewTypeCount()方法的作用就是為每種類型的數(shù)據(jù)項都單獨啟用一個RecycleBin緩存機制

RecycleBin類的核心方法和變量的解釋暫且放在此處捍岳,后續(xù)講解時會用到此處信息。

ListView的繪制流程

ListView本質(zhì)上還是一個View,因此繪制的過程還是分為三步:onMeasure祟同、onLayout作喘、onDraw,onMeasure測出其占用屏幕空間晕城,最大為整個屏幕泞坦,而onDraw用于將ListView內(nèi)容繪制到屏幕上,在ListView中無實際意義砖顷,因為ListView本身只是提供了一種布局方式贰锁,真正的繪制是ListView中的子View完成的,因此onLayout方法是最為關(guān)鍵的滤蝠。

onLayout方法

ListView的OnLayout實現(xiàn)在AbsListView中豌熄,具體源碼如下:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    super.onLayout(changed, l, t, r, b);

    mInLayout = true;

    final int childCount = getChildCount();
    if (changed) {
        for (int i = 0; i < childCount; i++) {
            getChildAt(i).forceLayout();
        }
        mRecycler.markChildrenDirty();
    }

    layoutChildren();
    mInLayout = false;
    
    ...
}

從上面代碼可以看出,首先調(diào)用了父類的onLayout方法物咳,再判斷ListView是否發(fā)生了變化(大小锣险,位置),如果ListView發(fā)生了變化览闰,則changed變量為true芯肤,changed為true則強制每個子布局都進行重新繪制,還需要注意一點的是同時還進行了mRecycler.markChildrenDirty()這個操作压鉴,其中mRecycler就是一個RecycleBin的對象崖咨,而markChildrenDirty()方法會為每一個scrap view調(diào)用forceLayout();判斷完changed變量后又調(diào)用了layoutChildren()方法油吭,點開此方法可以發(fā)現(xiàn)他是一個空方法击蹲,因為每個子元素的布局實現(xiàn)應該由自己來實現(xiàn),所以它的具體實現(xiàn)在ListView中婉宰。

layoutChildren方法

 @Override
 protected void layoutChildren() {
    ...
    
    final int childCount = getChildCount();
    
    ...
    
    boolean dataChanged = mDataChanged;
      
    ...
    
    if (dataChanged) {
        for (int i = 0; i < childCount; i++) {
            recycleBin.addScrapView(getChildAt(i), firstPosition+i);
        }
    } else {
        recycleBin.fillActiveViews(childCount, firstPosition);
    }

    ...
    
    switch (mLayoutMode) {
        ...
        
        default:
            if (childCount == 0) {
                if (!mStackFromBottom) {
                    final int position = lookForSelectablePosition(0, true);
                    setSelectedPositionInt(position);
                    sel = fillFromTop(childrenTop);
                } else {
                    final int position = lookForSelectablePosition(mItemCount - 1,false);
                    setSelectedPositionInt(position);
                    sel = fillUp(mItemCount - 1, childrenBottom);
                }
            } else {
                if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
                    sel = fillSpecific(mSelectedPosition,
                            oldSel == null ? childrenTop : oldSel.getTop());
                } else if (mFirstPosition < mItemCount) {
                    sel = fillSpecific(mFirstPosition,
                            oldFirst == null ? childrenTop : oldFirst.getTop());
                } else {
                    sel = fillSpecific(0, childrenTop);
                }
            }
            break;
    }

    ...
    
 }

它的方法過長歌豺,只貼出來一部分,此方法中首先會獲取子元素的數(shù)量芍阎,但此時ListView中還沒有任何子View世曾,因為數(shù)據(jù)都是由Adapter管理的,還沒有展示到界面上谴咸。接著又會判斷dataChanged這個值,如果數(shù)據(jù)源發(fā)生變化則該值變?yōu)閠rue骗露,緊接著調(diào)用了RecycleBin的fillActiveViews()方法岭佳;可是這時ListView中還沒有子View,因此fillActiveViews的緩存功能無法起作用萧锉。

那么我們再接著往下分析珊随,接下來又會判斷mLayoutMode的值,默認情況下該值都是LAYOUT_NORMAL,此模式下會直接進入default語句中叶洞,其中有多次if條件判斷鲫凶,因為當前ListView中還沒有任何子View所以當前childCount數(shù)量為0,mStackFromBottom變量代表的是布局的順序衩辟,默認的布局順序是從上至下螟炫,因此會進入fillFromTop方法中

fillFromTop方法

此方法具體代碼如下:

private View fillFromTop(int nextTop) {
    mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
    mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
    if (mFirstPosition < 0) {
        mFirstPosition = 0;
    }
    return fillDown(mFirstPosition, nextTop);
}

private View fillDown(int pos, int nextTop) {
    View selectedView = null;

    int end = (mBottom - mTop);
    if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
        end -= mListPadding.bottom;
    }

    while (nextTop < end && pos < mItemCount) {
        // is this the selected item?
        boolean selected = pos == mSelectedPosition;
        View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);

        nextTop = child.getBottom() + mDividerHeight;
        if (selected) {
            selectedView = child;
        }
        pos++;
    }

    setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
    return selectedView;
}

fillFromTop首先計算出了mFirstPosition的值,并從mFirstPosition開始自頂至下調(diào)用fillDown填充艺晴。

fillDown中采用了while循環(huán)來填充昼钻,一開始時nextTop的值是第一個子元素頂部距離整個ListView頂部的像素值pos是傳入的mFirstPosition的值封寞,end是ListView底部減去頂部所得的像素值然评,mItemCount是Adapter中的元素數(shù)量,因此nextTop是小于end的狈究,pos也小于mItemCount碗淌,每次執(zhí)行while循環(huán)時,pos加1抖锥,nextTop也會累加當nextTop大于end時贯莺,也就是子元素超出屏幕了,或者pos大于mItemCount時宁改,即Adapter中所有元素都被遍歷了缕探,出現(xiàn)以上兩種情況中一種便會跳出while循環(huán)。

在此while循環(huán)中还蹲,我們注意到調(diào)用了makeAddView這個方法爹耗,下面具體分析下。

makeAddView方法

首先貼下代碼:

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
        boolean selected) {
    if (!mDataChanged) {
        // Try to use an existing view for this position.
        final View activeView = mRecycler.getActiveView(position);
        if (activeView != null) {
            // Found it. We're reusing an existing child, so it just needs
            // to be positioned like a scrap view.
            setupChild(activeView, position, y, flow, childrenLeft, selected, true);
            return activeView;
        }
    }

    // Make a new view for this position, or convert an unused view if
    // possible.
    final View child = obtainView(position, mIsScrap);

    // This needs to be positioned and measured.
    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

    return child;
}

Adapter的數(shù)據(jù)源未發(fā)生變化時谜喊,會從RecycleBin中獲取一個activeView潭兽,但是目前RecycleBin中還沒有緩存任何的View,因此這里得到的child為null斗遏,接著又調(diào)用了obtainView方法來獲取一個View山卦,再來看看這個方法吧,源碼如下:

obtainView方法

View obtainView(int position, boolean[] outMetadata) {

    outMetadata[0] = false;
    
    ...
    
    final View scrapView = mRecycler.getScrapView(position);
    final View child = mAdapter.getView(position, scrapView, this);
    if (scrapView != null) {
        if (child != scrapView) {
            // Failed to re-bind the data, return scrap to the heap.
            mRecycler.addScrapView(scrapView, position);
        } else if (child.isTemporarilyDetached()) {
            outMetadata[0] = true;

            // Finish the temporary detach started in addScrapView().
            child.dispatchFinishTemporaryDetach();
        }
    }

    ...
    
    return child;
}

首先調(diào)用了RecycleBingetScrapView方法來嘗試獲取一個廢棄緩存中的View诵次,但是這里是獲取不到的账蓉;接著又調(diào)用了getView方法,即自定的Adapter中的getView方法逾一,getView方法接收三個參數(shù)铸本,第一個是當前子元素位置,第二個參數(shù)是convertView遵堵,上面?zhèn)魅氲氖莕ull箱玷,說明沒有covertView可以利用怨规,因此在Adapter中判斷convertView為null時可以調(diào)用LayoutInflater的inflate方法去加載一個布局,并將此view返回锡足。

同時我們可以看到波丰,這個view最終也會作為obtainView方法的返回結(jié)果,并傳入makeAddView方法中后續(xù)調(diào)用setupChild()方法中舶得,上面過程可以說明第一次layout過程中掰烟,所有子View都是調(diào)用LayoutInflater的inflate方法動態(tài)加載對應布局而產(chǎn)生的,解析布局的過程肯定是耗時的扩灯,但是在后續(xù)過程中媚赖,這種情況不會出現(xiàn)了。接下來珠插,繼續(xù)看下setupChild方法源碼:

private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
        boolean selected, boolean isAttachedToWindow) {
    ...
    
    addViewInLayout(child, flowDown ? -1 : 0, p, true);
    
    ....
}

setupChild方法中會調(diào)用addViewInLayout方法將它添加到ListView中惧磺,那么回到fillDown方法,其中的while循環(huán)就會讓子元素View將整個ListView控件填滿然后跳出捻撑,也就是說即使Adapter中有很多條數(shù)據(jù)磨隘,ListView也只會加載第一屏數(shù)據(jù)。下圖是第一次onLayout的過程:

ListView_onLayout1.png

第二次onLayout

即使是一個再簡單的View顾患,在展示到界面上之前都會經(jīng)歷至少兩次onMeasure()和兩次onLayout()的過程番捂,自然ListView的繪制過程也不例外,同樣我們關(guān)注的重點還是onLayout過程江解。

首先還是從layoutChildren()方法看起:

layoutChildren方法

再來看一遍該方法源碼:

 @Override
 protected void layoutChildren() {
    ...
    
    final int childCount = getChildCount();
    
    ...
    
    boolean dataChanged = mDataChanged;
      
    ...
    
    if (dataChanged) {
        for (int i = 0; i < childCount; i++) {
            recycleBin.addScrapView(getChildAt(i), firstPosition+i);
        }
    } else {
        recycleBin.fillActiveViews(childCount, firstPosition);
    }

    ...
    
    // Clear out old views
    detachAllViewsFromParent();
    
    switch (mLayoutMode) {
        ...
        
        default:
            if (childCount == 0) {
                if (!mStackFromBottom) {
                    final int position = lookForSelectablePosition(0, true);
                    setSelectedPositionInt(position);
                    sel = fillFromTop(childrenTop);
                } else {
                    final int position = lookForSelectablePosition(mItemCount - 1,false);
                    setSelectedPositionInt(position);
                    sel = fillUp(mItemCount - 1, childrenBottom);
                }
            } else {
                if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
                    sel = fillSpecific(mSelectedPosition,
                            oldSel == null ? childrenTop : oldSel.getTop());
                } else if (mFirstPosition < mItemCount) {
                    sel = fillSpecific(mFirstPosition,
                            oldFirst == null ? childrenTop : oldFirst.getTop());
                } else {
                    sel = fillSpecific(0, childrenTop);
                }
            }
            break;
    }

    ...
    
 }

首先還是會獲取子元素的數(shù)量设预,不同于第一次onLayout,此時獲取到的子View數(shù)量不再為0犁河,而是ListView中顯示的子元素數(shù)量鳖枕;下面又調(diào)用了RecycleBinfillActiveViews()方法,目前ListView已經(jīng)有子View了桨螺,這樣所有的子View都會被緩存到RecycleBin中mActiveViews數(shù)組中宾符,后面會使用到他們。

接下來有一個重要的方法:detachAllViewsFromParent()灭翔,這個方法會將所有ListView當中的子View全部清除掉魏烫,從而保證第二次Layout過程不會產(chǎn)生一份重復的數(shù)據(jù),因為layoutChildren方法會向ListView中添加View肝箱,在第一次layout中已經(jīng)添加了一次哄褒,如果第二次layout繼續(xù)添加,那么必然會出現(xiàn)數(shù)據(jù)重復的問題狭园,因此這里先調(diào)用detachAllViewsFromParent方法將第一次添加的View清除掉读处。

這樣把已經(jīng)加載好的View又清除掉,待會還要再重新加載一遍唱矛,這不是嚴重影響效率嗎?不用擔心,我們剛剛調(diào)用了RecycleBinfillActiveViews()方法來緩存子View绎谦,等會將直接使用這些緩存好的View來進行添加子View管闷,而并不會重新執(zhí)行一遍inflate過程,因此效率方面并不會有什么明顯的影響窃肠。

摘自Android ListView工作原理完全解析包个,帶你從源碼的角度徹底理解

再進入判斷childCount是否為0的邏輯中,此時會走和第一次layout相反的else邏輯分支冤留,這其中又有三條邏輯分支碧囊,第一條一般不成立,因為開始時我們還沒選中任何子View纤怒,第二條一般成立糯而,mFirstPosition開始時為0,只要Adapter中數(shù)據(jù)量大于0即可泊窘,所以進入了fillSpecific方法:

fillSpecific方法

此方法源碼如下:

private View fillSpecific(int position, int top) {
    boolean tempIsSelected = position == mSelectedPosition;
    View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
    // Possibly changed again in fillUp if we add rows above this one.
    mFirstPosition = position;

    View above;
    View below;

    final int dividerHeight = mDividerHeight;
    if (!mStackFromBottom) {
        above = fillUp(position - 1, temp.getTop() - dividerHeight);
        // This will correct for the top of the first view not touching the top of the list
        adjustViewsUpOrDown();
        below = fillDown(position + 1, temp.getBottom() + dividerHeight);
        int childCount = getChildCount();
        if (childCount > 0) {
            correctTooHigh(childCount);
        }
    } else {
        below = fillDown(position + 1, temp.getBottom() + dividerHeight);
        // This will correct for the bottom of the last view not touching the bottom of the list
        adjustViewsUpOrDown();
        above = fillUp(position - 1, temp.getTop() - dividerHeight);
        int childCount = getChildCount();
        if (childCount > 0) {
             correctTooLow(childCount);
        }
    }

    if (tempIsSelected) {
        return temp;
    } else if (above != null) {
        return above;
    } else {
        return below;
    }
}

fillSpecific()方法的功能和fillUp熄驼,fillDown差不多,但是在fillSpecific()方法會優(yōu)先加載指定位置的View烘豹,再加載該View上下的其它子View瓜贾,由于這里我們傳入的position就是第一個子元素的位置,因此此時其效果和上述的fillDown()基本一致携悯。

此外可以看到祭芦,fillSpecific()方法中也調(diào)用了makeAndAddView()方法,因為我們之前調(diào)用detachAllViewsFromParent()方法把所有ListView當中的子View全部清除掉了憔鬼,這里肯定要重新再加上龟劲,在makeAndAddView()方法中:

makeAndAddView方法

再來看一遍此方法源碼:

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
        boolean selected) {
    if (!mDataChanged) {
        // Try to use an existing view for this position.
        final View activeView = mRecycler.getActiveView(position);
        if (activeView != null) {
            // Found it. We're reusing an existing child, so it just needs
            // to be positioned like a scrap view.
            setupChild(activeView, position, y, flow, childrenLeft, selected, true);
            return activeView;
        }
    }

    // Make a new view for this position, or convert an unused view if
    // possible.
    final View child = obtainView(position, mIsScrap);

    // This needs to be positioned and measured.
    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

    return child;
}

首先還是會從RecycleBin中獲取ActiveView,不同于第一次layout逊彭,這次能獲取到了咸灿,那肯定就不會進入obtainView中了,而是直接調(diào)用setupChild()方法侮叮,此時setupChild()方法的最后一個參數(shù)是true避矢,表明當前的view是被回收過的,再來看看setupChild()方法源碼:

private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
        boolean selected, boolean isAttachedToWindow) {

    ...

    if ((isAttachedToWindow && !p.forceAdd) || (p.recycledHeaderFooter
            && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
        attachViewToParent(child, flowDown ? -1 : 0, p);

        ...
        
    } else {
    
        ...
        
    }

    ...
    
}

可以看到囊榜,setupChild()方法的最后一個參數(shù)是isAttachedToWindow审胸,方法執(zhí)行過程中會對這個變量進行判斷,由于isAttachedToWindow現(xiàn)在是true卸勺,所以會執(zhí)行attachViewToParent()方法砂沛,而第一次Layout過程則是執(zhí)行的else語句中的addViewInLayout()方法。

這兩個方法最大的區(qū)別在于曙求,如果我們需要向ViewGroup中添加一個新的子View碍庵,應該調(diào)用addViewInLayout()方法映企,而如果是想要將一個之前detach的View重新attach到ViewGroup上,就應該調(diào)用attachViewToParent()方法静浴。那么由于前面在layoutChildren()方法當中調(diào)用了detachAllViewsFromParent()方法堰氓,這樣ListView中所有的子View都是處于detach狀態(tài)的,所以這里attachViewToParent()方法是正確的選擇苹享。

經(jīng)歷了這樣一個detach又attach的過程双絮,ListView中所有的子View又都可以正常顯示出來了,那么第二次Layout過程結(jié)束得问。

摘自Android ListView工作原理完全解析囤攀,帶你從源碼的角度徹底理解

下圖展示了第二次onLayout的過程:


ListView_onLayout_2.png

ListView如何做到滑動加載更多子View?

onTouchEvent方法

通過以上兩次layout宫纬,我們已經(jīng)能看到ListView的第一屏內(nèi)容了焚挠,但是如果Adapter中有大量數(shù)據(jù),剩下的數(shù)據(jù)怎么加載呢哪怔?我們知道實際使用過程中宣蔚,ListView滑動的時候剩余的數(shù)據(jù)便顯示出來了,那滑動首先肯定要監(jiān)聽觸摸事件认境,相關(guān)代碼在AbsListView中的onTouchEvent中:

@Override
public boolean onTouchEvent(MotionEvent ev) {

    ...
    
    switch (actionMasked) {
    
        ...

        case MotionEvent.ACTION_MOVE: {
            onTouchMove(ev, vtev);
            break;
        }
        
        ...
        
    }

    ...
    
    return true;
}

我們主要關(guān)注ACTION_MOVE滑動事件胚委,因為ListView是隨著滑動而加載更多子View的,其中調(diào)用了onTouchMove方法:

private void onTouchMove(MotionEvent ev, MotionEvent vtev) {
    
    ...

    switch (mTouchMode) {
        case TOUCH_MODE_DOWN:
        case TOUCH_MODE_TAP:
        case TOUCH_MODE_DONE_WAITING:
            
            ...
            
        case TOUCH_MODE_SCROLL:
        case TOUCH_MODE_OVERSCROLL:
            scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev);
            break;
    }
}

此方法中判斷了mTouchMode叉信,當手指在屏幕上滑動時亩冬,TouchMode是等于TOUCH_MODE_SCROLL,接下來調(diào)用了scrollIfNeeded()方法:

scrollIfNeeded方法

private void scrollIfNeeded(int x, int y, MotionEvent vtev) {
   
    ...
    
    final int deltaY = rawDeltaY;
    int incrementalDeltaY =
            mLastY != Integer.MIN_VALUE ? y - mLastY + scrollConsumedCorrection : deltaY;
    int lastYCorrection = 0;

    if (mTouchMode == TOUCH_MODE_SCROLL) {
        
        ...
        
            if (incrementalDeltaY != 0) {
                atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
            }

            ...
            
        }
    } 
    
    ...
    
}

注意到其中會調(diào)用trackMotionScroll方法硼身,只要我們手指滑動了一點距離硅急,此方法就會被調(diào)用,自然如果手指在屏幕上滑動了很多佳遂,此方法就會被調(diào)用很多次营袜。

trackMotionScroll方法

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

    final boolean down = incrementalDeltaY < 0;
    ...

    if (down) {
        int top = -incrementalDeltaY;
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            top += listPadding.top;
        }
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            if (child.getBottom() >= top) {
                break;
            } else {
                count++;
                int position = firstPosition + i;
                if (position >= headerViewsCount && position < footerViewsStart) {
                    // The view will be rebound to new data, clear any
                    // system-managed transient state.
                    child.clearAccessibilityFocus();
                    mRecycler.addScrapView(child, position);
                }
            }
        }
    } else {
        int bottom = getHeight() - incrementalDeltaY;
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            bottom -= listPadding.bottom;
        }
        for (int i = childCount - 1; i >= 0; i--) {
            final View child = getChildAt(i);
            if (child.getTop() <= bottom) {
                break;
            } else {
                start = i;
                count++;
                int position = firstPosition + i;
                if (position >= headerViewsCount && position < footerViewsStart) {
                    // The view will be rebound to new data, clear any
                    // system-managed transient state.
                    child.clearAccessibilityFocus();
                    mRecycler.addScrapView(child, position);
                }
            }
        }
    }

    ...

    if (count > 0) {
        detachViewsFromParent(start, count);
        mRecycler.removeSkippedScrap();
    }

    ...
    
    if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
        fillGap(down);
    }
        
    ...
    
    return false;
}

這個方法接收兩個參數(shù),一個是deltaY丑罪,表示手指從按下的位置到當前手指位置的Y方向的距離荚板,incrementalDeltaY則表示距離上次觸發(fā)event事件手指在Y方向上位置的改變量,可以通過incrementalDeltaY的正負知道用戶是往上還是往下滑動吩屹。

如果incrementalDeltaY<0跪另,說明是向下滑動,進入if (down) 分支中煤搜,其中有個for循環(huán)免绿,從上往下獲取子View,如果子View的bottom小于ListView的Top說明這個子View已經(jīng)移出屏幕了擦盾,則調(diào)用RecycleBin的addScrapView方法將其加入到廢棄緩存中嘲驾,并將計數(shù)器count+1淌哟,計數(shù)器用于記錄有多少個子View被移出了屏幕

那么如果是ListView向上滑動的話距淫,其實過程是基本相同的绞绒,只不過變成了從下往上依次獲取子View婶希,然后判斷該子View的top值是不是大于bottom值了榕暇,如果大于的話說明子View已經(jīng)移出了屏幕,同樣把它加入到廢棄緩存中喻杈,并將計數(shù)器加1彤枢。

接下來,如果count大于0筒饰,說明有子View被加入廢棄緩存了缴啡,則會調(diào)用detachViewsFromParent()方法將所有移出屏幕的子View全部detach掉。有View被移出瓷们,那么自然就需要添加新的View业栅,所以如果ListView中最后一個View的底部已經(jīng)移入了屏幕,或者ListView中第一個View的頂部移入了屏幕谬晕,就會調(diào)用fillGap()方法碘裕,fillGap()方法是用來加載屏幕外數(shù)據(jù)的,如下所示:

fillGap方法

此方法的實現(xiàn)在ListView中攒钳,

void fillGap(boolean down) {
    final int count = getChildCount();
    if (down) {
        int paddingTop = 0;
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            paddingTop = getListPaddingTop();
        }
        final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
                paddingTop;
        fillDown(mFirstPosition + count, startOffset);
        correctTooHigh(getChildCount());
    } else {
        int paddingBottom = 0;
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            paddingBottom = getListPaddingBottom();
        }
        final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :
                getHeight() - paddingBottom;
        fillUp(mFirstPosition - 1, startOffset);
        correctTooLow(getChildCount());
    }
}

fillGap接受一個down參數(shù)帮孔,此參數(shù)代表之前ListView是向下還是向上滑動,如果向下則調(diào)用fillDown()方法不撑,如果向上滑動則調(diào)用fillUp()方法文兢,這兩個方法之前已經(jīng)說過了,內(nèi)部有一個while循環(huán)來對ListView進行填充焕檬,填充的過程是通過makeAndAddView來實現(xiàn)的姆坚,好吧,再去makeAndAddView方法中看看实愚。

makeAndAddView方法

這是第三次來看此方法源碼了:

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
        boolean selected) {
    if (!mDataChanged) {
        // Try to use an existing view for this position.
        final View activeView = mRecycler.getActiveView(position);
        if (activeView != null) {
            // Found it. We're reusing an existing child, so it just needs
            // to be positioned like a scrap view.
            setupChild(activeView, position, y, flow, childrenLeft, selected, true);
            return activeView;
        }
    }

    // Make a new view for this position, or convert an unused view if
    // possible.
    final View child = obtainView(position, mIsScrap);

    // This needs to be positioned and measured.
    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

    return child;
}

首先還是從RecycleBin中獲取activeView兼呵,不過此時已經(jīng)獲取不到了,因為第二次layout過程中被獲取過了爆侣,所以只好調(diào)用obtainView方法了萍程。

obtainView方法

View obtainView(int position, boolean[] outMetadata) {

    ...

    final View scrapView = mRecycler.getScrapView(position);
    final View child = mAdapter.getView(position, scrapView, this);
    
    ...

    return child;
}

這里會調(diào)用mRecycler.getScrapView方法來獲取廢棄的緩存View,而剛好我們前面在trackMotionScroll方法中處理已移出屏幕的View時將其加入廢棄緩存view中了兔仰,也就是說一旦有任何子View被移出了屏幕茫负,就會將它加入到廢棄緩存中,而從obtainView()方法中的邏輯來看乎赴,一旦有新的數(shù)據(jù)需要顯示到屏幕上忍法,就會嘗試從廢棄緩存中獲取View潮尝。

所以它們之間就形成了一個生產(chǎn)者和消費者的模式,那么ListView神奇的地方也就在這里體現(xiàn)出來了饿序,不管你有任意多條數(shù)據(jù)需要顯示勉失,ListView中的子View其實來來回回就那么幾個移出屏幕的子View會很快被移入屏幕的數(shù)據(jù)重新利用起來原探,因而不管我們加載多少數(shù)據(jù)都不會出現(xiàn)OOM的情況乱凿,甚至內(nèi)存都不會有所增加

摘自Android ListView工作原理完全解析咽弦,帶你從源碼的角度徹底理解

還有一點需要注意徒蟆,我們將獲取到的scrapView傳入了mAdapter.getView()方法中,那么這個參數(shù)具體是什么用呢型型,我們來看一個Adapter的getView例子:

public View getView(int position, View convertView, ViewGroup parent) {
    String url = getItem(position);
    View view;
    if (convertView == null) {
        view = LayoutInflater.from(getContext()).inflate(R.layout.image_item, null);
    } else {
        view = convertView;
    }
    ...
    return view;
}

第二個參數(shù)就是convertView段审,所以當這個參數(shù)不為null的時候,我們會直接復用此View闹蒜,然后更新下對應的數(shù)據(jù)即可寺枉,而為null時才去加載布局文件

再之后的代碼就是調(diào)用setupChild()方法绷落,將獲取到的view重新attach到ListView當中姥闪,因為廢棄緩存中的View也是之前從ListView中detach掉的

至此已經(jīng)基本將ListView的工作原理說清楚了嘱函,下圖是滑動時ListView工作原理:


ListView Move

總結(jié)一下ListView的緩存原理如下:

ListView cache

參考信息

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市往弓,隨后出現(xiàn)的幾起案子疏唾,更是在濱河造成了極大的恐慌,老刑警劉巖函似,帶你破解...
    沈念sama閱讀 211,948評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件槐脏,死亡現(xiàn)場離奇詭異,居然都是意外死亡撇寞,警方通過查閱死者的電腦和手機顿天,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蔑担,“玉大人牌废,你說我怎么就攤上這事∑∥眨” “怎么了鸟缕?”我有些...
    開封第一講書人閱讀 157,490評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我懂从,道長授段,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,521評論 1 284
  • 正文 為了忘掉前任番甩,我火速辦了婚禮侵贵,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘缘薛。我一直安慰自己窍育,他們只是感情好,可當我...
    茶點故事閱讀 65,627評論 6 386
  • 文/花漫 我一把揭開白布掩宜。 她就那樣靜靜地躺著蔫骂,像睡著了一般。 火紅的嫁衣襯著肌膚如雪牺汤。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,842評論 1 290
  • 那天浩嫌,我揣著相機與錄音檐迟,去河邊找鬼。 笑死码耐,一個胖子當著我的面吹牛追迟,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播骚腥,決...
    沈念sama閱讀 38,997評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼敦间,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了束铭?” 一聲冷哼從身側(cè)響起廓块,我...
    開封第一講書人閱讀 37,741評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎契沫,沒想到半個月后带猴,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,203評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡懈万,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,534評論 2 327
  • 正文 我和宋清朗相戀三年拴清,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片会通。...
    茶點故事閱讀 38,673評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡口予,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出涕侈,到底是詐尸還是另有隱情沪停,我是刑警寧澤,帶...
    沈念sama閱讀 34,339評論 4 330
  • 正文 年R本政府宣布驾凶,位于F島的核電站牙甫,受9級特大地震影響掷酗,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜窟哺,卻給世界環(huán)境...
    茶點故事閱讀 39,955評論 3 313
  • 文/蒙蒙 一泻轰、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧且轨,春花似錦浮声、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至至朗,卻和暖如春屉符,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背锹引。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評論 1 266
  • 我被黑心中介騙來泰國打工倦畅, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留哲嘲,地道東北人虐块。 一個月前我還...
    沈念sama閱讀 46,394評論 2 360
  • 正文 我出身青樓并村,卻偏偏與公主長得像,于是被迫代替她去往敵國和親腾啥。 傳聞我的和親對象是個殘疾皇子东涡,可洞房花燭夜當晚...
    茶點故事閱讀 43,562評論 2 349

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