天天看點

ListView工作原理分析

引言

ListView我們平時經常用到,可是對它的了解又有多少呢,今天我就來分析一下ListView的工作原理。

首先來看一下ListView的繼承體系:

ListView工作原理分析

ListView的繼承結構還是相當複雜的,它是直接繼承自的AbsListView,而AbsListView有兩個子實作類,一個是ListView,另一個就是GridView,是以我們從這一點就可以猜出來,ListView和GridView在工作原理和實作上都是有很多共同點的。然後AbsListView又繼承自AdapterView,AdapterView繼承自ViewGroup,後面就是我們所熟知的了。先把ListView的繼承結構了解一下,待會兒有助于我們更加清晰地分析代碼。

RecycleBin機制

那麼在開始分析ListView的源碼之前,還有一個東西是我們提前需要了解的,就是RecycleBin機制,這個機制也是ListView能夠實作成百上千條資料都不會OOM最重要的一個原因。其實RecycleBin的代碼并不多,在API23中隻有500行左右,它是寫在AbsListView中的一個内部類,是以所有繼承自AbsListView的子類,也就是ListView和GridView,都可以使用這個機制。下面來看其中的幾個方法:

1.setViewTypeCount()

我們都知道Adapter當中可以重寫一個getViewTypeCount()來表示ListView中有幾種類型的資料項,而setViewTypeCount()方法的作用就是為每種類型的View都單獨啟用一個RecycleBin緩存機制。

public void setViewTypeCount(int viewTypeCount) {
            if (viewTypeCount < ) {
                throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
            }
            //noinspection unchecked
            ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
            for (int i = ; i < viewTypeCount; i++) {
                scrapViews[i] = new ArrayList<View>();
            }
            mViewTypeCount = viewTypeCount;
            mCurrentScrap = scrapViews[];
            mScrapViews = scrapViews;
        }
           

2.fillActiveViews()

這個方法接收兩個參數,第一個參數表示要存儲的view的數量,第二個參數表示ListView中第一個可見元素的position值。RecycleBin當中使用mActiveViews這個數組來存儲View,調用這個方法後就會根據傳入的參數來将ListView中的指定元素存儲到mActiveViews數組當中。

void fillActiveViews(int childCount, int firstActivePosition) {
            if (mActiveViews.length < childCount) {
                mActiveViews = new View[childCount];
            }
            mFirstActivePosition = firstActivePosition;

            //noinspection MismatchedReadAndWriteOfArray
            final View[] activeViews = mActiveViews;
            for (int i = ; i < childCount; i++) {
                View child = getChildAt(i);
                AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
                // Don't put header or footer views into the scrap heap
                if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                    // Note:  We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views.
                    //        However, we will NOT place them into scrap views.
                    activeViews[i] = child;
                    // Remember the position so that setupChild() doesn't reset state.
                    lp.scrappedFromPosition = firstActivePosition + i;
                }
            }
        }
           

3.getActiveView()

這個方法和fillActiveViews()是對應的,用于從mActiveViews數組當中擷取資料。該方法接收一個position參數,表示元素在ListView當中的位置,方法内部會自動将position值轉換成mActiveViews數組對應的下标值。需要注意的是,mActiveViews當中所存儲的View,一旦被擷取了之後就會從mActiveViews當中移除,下次擷取同樣位置的View将會傳回null,也就是說mActiveViews不能被重複利用。

View getActiveView(int position) {
            int index = position - mFirstActivePosition;
            final View[] activeViews = mActiveViews;
            if (index >= && index < activeViews.length) {
                final View match = activeViews[index];
                activeViews[index] = null;
                return match;
            }
            return null;
        }
           

4.addScrapView()

用于将一個廢棄的View進行緩存,該方法接收一個View參數,當有某個View确定要廢棄掉的時候(比如滾動出了螢幕),就應該調用這個方法來對View進行緩存。根據注釋可以知道對于是否回收某一個View有一些列判斷的規則,最後如果要回收的話會先判斷,是不是View的種類唯一,如果是的話就把View存儲到mCurrentScrap這個集合中;如果不是就把View存儲到mScrapViews數組對應下标所代表的集合中。

void addScrapView(View scrap, int position) {
            final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
            if (lp == null) {
                // Can't recycle, but we don't know anything about the view.
                // Ignore it completely.
                return;
            }

            lp.scrappedFromPosition = position;

            // Remove but don't scrap header or footer views, or views that
            // should otherwise not be recycled.
            final int viewType = lp.viewType;
            if (!shouldRecycleViewType(viewType)) {
                // Can't recycle. If it's not a header or footer, which have
                // special handling and should be ignored, then skip the scrap
                // heap and we'll fully detach the view later.
                if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                    getSkippedScrap().add(scrap);
                }
                return;
            }

            scrap.dispatchStartTemporaryDetach();

            // The the accessibility state of the view may change while temporary
            // detached and we do not allow detached views to fire accessibility
            // events. So we are announcing that the subtree changed giving a chance
            // to clients holding on to a view in this subtree to refresh it.
            notifyViewAccessibilityStateChangedIfNeeded(
                    AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);

            // Don't scrap views that have transient state.
            final boolean scrapHasTransientState = scrap.hasTransientState();
            if (scrapHasTransientState) {
                if (mAdapter != null && mAdapterHasStableIds) {
                    // If the adapter has stable IDs, we can reuse the view for
                    // the same data.
                    if (mTransientStateViewsById == null) {
                        mTransientStateViewsById = new LongSparseArray<>();
                    }
                    mTransientStateViewsById.put(lp.itemId, scrap);
                } else if (!mDataChanged) {
                    // If the data hasn't changed, we can reuse the views at
                    // their old positions.
                    if (mTransientStateViews == null) {
                        mTransientStateViews = new SparseArray<>();
                    }
                    mTransientStateViews.put(position, scrap);
                } else {
                    // Otherwise, we'll have to remove the view and start over.
                    getSkippedScrap().add(scrap);
                }
            } else {
                if (mViewTypeCount == ) {
                    mCurrentScrap.add(scrap);
                } else {
                    mScrapViews[viewType].add(scrap);
                }

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

5.getScrapView()

用于從廢棄緩存中取出一個View,這些廢棄緩存中的View是沒有順序可言的,是以getScrapView()方法中的算法也非常簡單。如果View的種類唯一,就從mCurrentScrap當中擷取一個scrap view進行傳回;如果不是,就從mScrapViews數組對應下标的元素所代表的集合中擷取一個scrap view進行傳回。實際上從setViewTypeCount()中可以看到,當typeCount大于1時mCurrentScrap實際上就是mScrapViews數組對應下标為0的元素所代表的集合。

/**
         * @return A view from the ScrapViews collection. These are unordered.
         */
        View getScrapView(int position) {
            final int whichScrap = mAdapter.getItemViewType(position);
            if (whichScrap < ) {
                return null;
            }
            if (mViewTypeCount == ) {
                return retrieveFromScrap(mCurrentScrap, position);
            } else if (whichScrap < mScrapViews.length) {
                return retrieveFromScrap(mScrapViews[whichScrap], position);
            }
            return null;
        }
           

6.retrieveFromScrap()

這個方法就是getScrapView()中調用的傳回scrap view的方法。它先判斷了傳入的參數position所代表的scrap view是否還存在,如果存在就傳回,如果不存在就傳回存儲scrap view集合中的最後一個元素。

private View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
            final int size = scrapViews.size();
            if (size > ) {
                // See if we still have a view for this position or ID.
                for (int i = ; i < size; i++) {
                    final View view = scrapViews.get(i);
                    final AbsListView.LayoutParams params =
                            (AbsListView.LayoutParams) view.getLayoutParams();

                    if (mAdapterHasStableIds) {
                        final long id = mAdapter.getItemId(position);
                        if (id == params.itemId) {
                            return scrapViews.remove(i);
                        }
                    } else if (params.scrappedFromPosition == position) {
                        final View scrap = scrapViews.remove(i);
                        clearAccessibilityFromScrap(scrap);
                        return scrap;
                    }
                }
                final View scrap = scrapViews.remove(size - );
                clearAccessibilityFromScrap(scrap);
                return scrap;
            } else {
                return null;
            }
        }
           

第一次Layout

不管怎麼說,ListView即使再特殊最終還是繼承自View的,是以它的執行流程還将會按照View的規則來執行。View的執行流程無非就分為三步,onMeasure()用于測量View的大小,onLayout()用于确定View的布局,onDraw()用于将View繪制到界面上。而在ListView當中,onMeasure()并沒有什麼特殊的地方,因為它終歸是一個View,占用的空間最多并且通常也就是整個螢幕。onDraw()在ListView當中也沒有什麼意義,因為ListView本身并不負責繪制,而是由ListView當中的子元素來進行繪制的。那麼ListView大部分的神奇功能其實都是在onLayout()方法中進行的了,是以我們本篇文章也是主要分析的這個方法裡的内容。

如果你到ListView源碼中去找一找,你會發現ListView中是沒有onLayout()這個方法的,這是因為這個方法是在ListView的父類AbsListView中實作的,代碼如下所示:

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 = ; i < childCount; i++) {
                getChildAt(i).forceLayout();
            }
            mRecycler.markChildrenDirty();
        }

        layoutChildren();
        mInLayout = false;

        mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;

        // TODO: Move somewhere sane. This doesn't belong in onLayout().
        if (mFastScroll != null) {
            mFastScroll.onItemCountChanged(getChildCount(), mItemCount);
        }
    }
           

可以看到,onLayout()方法中并沒有做什麼複雜的邏輯操作,主要就是一個判斷,如果ListView的大小或者位置發生了變化,那麼changed變量就會變成true,此時會要求所有的子布局都強制進行重繪。除此之外倒沒有什麼難了解的地方了,不過我們注意到,其中調用了layoutChildren()這個方法,從方法名上我們就可以猜出這個方法是用來進行子元素布局的,不過進入到這個方法當中你會發現這是個空方法,沒有一行代碼。這當然是可以了解的了,因為子元素的布局應該是由具體的實作類來負責完成的,而不是由父類完成。那麼進入ListView的layoutChildren()方法,部分代碼如下所示:

protected void layoutChildren() {
        ……
            switch (mLayoutMode) {
            case LAYOUT_SET_SELECTION:
                if (newSel != null) {
                    sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
                } else {
                    sel = fillFromMiddle(childrenTop, childrenBottom);
                }
                break;
            case LAYOUT_SYNC:
                sel = fillSpecific(mSyncPosition, mSpecificTop);
                break;
            case LAYOUT_FORCE_BOTTOM:
                sel = fillUp(mItemCount - , childrenBottom);
                adjustViewsUpOrDown();
                break;
            case LAYOUT_FORCE_TOP:
                mFirstPosition = ;
                sel = fillFromTop(childrenTop);
                adjustViewsUpOrDown();
                break;
            case LAYOUT_SPECIFIC:
                sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
                break;
            case LAYOUT_MOVE_SELECTION:
                sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
                break;
            default:
                if (childCount == ) {
                    if (!mStackFromBottom) {
                        final int position = lookForSelectablePosition(, true);
                        setSelectedPositionInt(position);
                        sel = fillFromTop(childrenTop);
                    } else {
                        final int position = lookForSelectablePosition(mItemCount - , false);
                        setSelectedPositionInt(position);
                        sel = fillUp(mItemCount - , childrenBottom);
                    }
                } else {
                    if (mSelectedPosition >=  && 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(, childrenTop);
                    }
                }
                break;
            }
        ……
    }
           

根據mLayoutMode的值來決定布局模式,預設情況下都是普通模式LAYOUT_NORMAL,是以會進入到default語句當中。而下面又會緊接着進行兩次if判斷,childCount目前是等于0的,并且預設的布局順序是從上往下,是以會進入fillFromTop()方法,我們跟進去瞧一瞧:

/**
     * Fills the list from top to bottom, starting with mFirstPosition
     *
     * @param nextTop The location where the top of the first item should be
     *        drawn
     *
     * @return The view that is currently selected
     */
    private View fillFromTop(int nextTop) {
        mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
        mFirstPosition = Math.min(mFirstPosition, mItemCount - );
        if (mFirstPosition < ) {
            mFirstPosition = ;
        }
        return fillDown(mFirstPosition, nextTop);
    }
           

從這個方法的注釋中可以看出,它所負責的主要任務就是從mFirstPosition開始,自頂至底去填充ListView。而這個方法本身并沒有什麼邏輯,就是判斷了一下mFirstPosition值的合法性,然後調用fillDown()方法,那麼我們就有理由可以猜測,填充ListView的操作是在fillDown()方法中完成的。進入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) {
            // 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() - );
        return selectedView;
    }
           

可以看到,這裡使用了一個while循環來執行重複邏輯,一開始nextTop的值是第一個子元素頂部距離整個ListView頂部的像素值,pos則是剛剛傳入的mFirstPosition的值,而end是ListView底部減去頂部所得的像素值,mItemCount則是Adapter中的元素數量。是以一開始的情況下nextTop必定是小于end值的,并且pos也是小于mItemCount值的。那麼每執行一次while循環,pos的值都會加1,并且nextTop也會增加,當nextTop大于等于end時,也就是子元素已經超出目前螢幕了,或者pos大于等于mItemCount時,也就是所有Adapter中的元素都被周遊結束了,就會跳出while循環。

那麼while循環當中又做了什麼事情呢?值得讓人留意的就是makeAndAddView()方法,進入到這個方法當中,代碼如下所示:

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
            boolean selected) {
        View child;


        if (!mDataChanged) {
            // Try to use an existing view for this position
            child = mRecycler.getActiveView(position);
            if (child != null) {
                // Found it -- we're using an existing child
                // This just needs to be positioned
                setupChild(child, position, y, flow, childrenLeft, selected, true);

                return child;
            }
        }

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

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

        return child;
    }
           

嘗試從RecycleBin當中快速擷取一個active view,不過很遺憾的是目前RecycleBin當中還沒有緩存任何的View,是以這裡得到的值肯定是null。那麼取得了null之後就會繼續向下運作,會調用obtainView()方法來再次嘗試擷取一個View,這次的obtainView()方法是可以保證一定傳回一個View的,于是下面立刻将擷取到的View傳入到了setupChild()方法當中。那麼obtainView()内部到底是怎麼工作的呢?我們先進入到這個方法裡面看一下:

View obtainView(int position, boolean[] isScrap) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");

        isScrap[] = false;

        // Check whether we have a transient state view. Attempt to re-bind the
        // data and discard the view if we fail.
        final View transientView = mRecycler.getTransientStateView(position);
        if (transientView != null) {
            final LayoutParams params = (LayoutParams) transientView.getLayoutParams();

            // If the view type hasn't changed, attempt to re-bind the data.
            if (params.viewType == mAdapter.getItemViewType(position)) {
                final View updatedView = mAdapter.getView(position, transientView, this);

                // If we failed to re-bind the data, scrap the obtained view.
                if (updatedView != transientView) {
                    setItemViewLayoutParams(updatedView, position);
                    mRecycler.addScrapView(updatedView, position);
                }
            }

            isScrap[] = true;

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

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

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

        if (mCacheColorHint != ) {
            child.setDrawingCacheBackgroundColor(mCacheColorHint);
        }

        if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
            child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
        }

        setItemViewLayoutParams(child, position);

        if (AccessibilityManager.getInstance(mContext).isEnabled()) {
            if (mAccessibilityDelegate == null) {
                mAccessibilityDelegate = new ListItemAccessibilityDelegate();
            }
            if (child.getAccessibilityDelegate() == null) {
                child.setAccessibilityDelegate(mAccessibilityDelegate);
            }
        }

        Trace.traceEnd(Trace.TRACE_TAG_VIEW);

        return child;
    }
           

obtainView()方法中的代碼并不多,但卻包含了非常非常重要的邏輯,不誇張的說,整個ListView中最重要的内容可能就在這個方法裡了。那麼我們還是按照執行流程來看,首先看到調用了RecycleBin的getTransientStateView()。 從單詞的意思裡面我們可以得知這是擷取一個瞬間狀态的view,這裡就有個疑問什麼是瞬間狀态的view?通過對源碼的層層分析終于在View 類的hasTransientState()方法裡面找到描述:

/**
     * Indicates whether the view is currently tracking transient state that the
     * app should not need to concern itself with saving and restoring, but that
     * the framework should take special note to preserve when possible.
     *
     * <p>A view with transient state cannot be trivially rebound from an external
     * data source, such as an adapter binding item views in a list. This may be
     * because the view is performing an animation, tracking user selection
     * of content, or similar.</p>
     *
     * @return true if the view has transient state
     */
    @ViewDebug.ExportedProperty(category = "layout")
    public boolean hasTransientState() {
        return (mPrivateFlags2 & PFLAG2_HAS_TRANSIENT_STATE) == PFLAG2_HAS_TRANSIENT_STATE;
    }
           

從注釋中我們得知這個方法是用來标記這個view的瞬時狀态,用來告訴app無需關心其儲存和恢複。從注釋中,官方告訴我這種具有瞬時狀态的view,用于在view動畫播放等情況中。

我們接着看obtainView(),之後調用了RecycleBin的getScrapView()方法來嘗試擷取一個廢棄緩存中的View,同樣的道理,這裡肯定是擷取不到的,getScrapView()方法會傳回一個null。這時該怎麼辦呢?沒有關系,之後又調用了mAdapter的getView()方法來去擷取一個View。那麼mAdapter是什麼呢?當然就是目前ListView關聯的擴充卡了。而getView()方法又是什麼呢?還用說嗎,這個就是我們平時使用ListView時最最經常重寫的一個方法了,這裡getView()方法中傳入了三個參數,分别是position,null和this。

那麼我們平時寫ListView的Adapter時,getView()方法通常會怎麼寫呢?這裡我舉個簡單的例子:

@Override
    public View getView(int position, View convertView, ViewGroup parent) {
        Fruit fruit = getItem(position);
        View view;
        if (convertView == null) {
            view = LayoutInflater.from(getContext()).inflate(resourceId, null);
        } else {
            view = convertView;
        }
        ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
        TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
        fruitImage.setImageResource(fruit.getImageId());
        fruitName.setText(fruit.getName());
        return view;
    }
           

getView()方法接受的三個參數,第一個參數position代表目前子元素的的位置,我們可以通過具體的位置來擷取與其相關的資料。第二個參數convertView,剛才傳入的是null,說明沒有convertView可以利用,是以我們會調用LayoutInflater的inflate()方法來去加載一個布局。接下來會對這個view進行一些屬性和值的設定,最後将view傳回。

那麼這個View也會作為obtainView()的結果進行傳回,并最終傳入到setupChild()方法當中。其實也就是說,第一次layout過程當中,所有的子View都是調用LayoutInflater的inflate()方法加載出來的,這樣就會相對比較耗時,但是不用擔心,後面就不會再有這種情況了,那麼我們繼續往下看:

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

setupChild()方法當中的代碼雖然比較多,但是我們隻看核心代碼的話就非常簡單了,剛才調用obtainView()方法擷取到的子元素View,這裡在第40行調用了addViewInLayout()方法将它添加到了ListView當中。那麼根據fillDown()方法中的while循環,會讓子元素View将整個ListView控件填滿然後就跳出,也就是說即使我們的Adapter中有一千條資料,ListView也隻會加載第一屏的資料,剩下的資料反正目前在螢幕上也看不到,是以不會去做多餘的加載工作,這樣就可以保證ListView中的内容能夠迅速展示到螢幕上。那麼到此為止,第一次Layout過程結束。

第二次Layout

雖然我在源碼中并沒有找出具體的原因,但如果你自己做一下實驗的話就會發現,即使是一個再簡單的View,在展示到界面上之前都會經曆至少兩次onMeasure()和兩次onLayout()的過程。其實這隻是一個很小的細節,平時對我們影響并不大,因為不管是onMeasure()或者onLayout()幾次,反正都是執行的相同的邏輯,我們并不需要進行過多關心。但是在ListView中情況就不一樣了,因為這就意味着layoutChildren()過程會執行兩次,而這個過程當中涉及到向ListView中添加子元素,如果相同的邏輯執行兩遍的話,那麼ListView中就會存在一份重複的資料了。是以ListView在layoutChildren()過程當中做了第二次Layout的邏輯處理,非常巧妙地解決了這個問題,下面我們就來分析一下第二次Layout的過程。

其實第二次Layout和第一次Layout的基本流程是差不多的,那麼我們還是從layoutChildren()方法開始看起:

protected void layoutChildren() {
            ……
            if (dataChanged) {
                for (int i = ; i < childCount; i++) {
                    recycleBin.addScrapView(getChildAt(i), firstPosition+i);
                }
            } else {
                recycleBin.fillActiveViews(childCount, firstPosition);
            }

            // Clear out old views
            detachAllViewsFromParent();
            ……
            switch (mLayoutMode) {
            default:
                if (childCount == ) {
                    if (!mStackFromBottom) {
                        final int position = lookForSelectablePosition(, true);
                        setSelectedPositionInt(position);
                        sel = fillFromTop(childrenTop);
                    } else {
                        final int position = lookForSelectablePosition(mItemCount - , false);
                        setSelectedPositionInt(position);
                        sel = fillUp(mItemCount - , childrenBottom);
                    }
                } else {
                    if (mSelectedPosition >=  && 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(, childrenTop);
                    }
                }
                break;
            }
           

首先調用了RecycleBin的fillActiveViews()方法,這次效果可就不一樣了,因為目前ListView中已經有子View了,這樣所有的子View都會被緩存到RecycleBin的mActiveViews數組當中,後面将會用到它們。

接下來将會是非常非常重要的一個操作,調用了detachAllViewsFromParent()方法。這個方法會将所有ListView當中的子View全部清除掉,進而保證第二次Layout過程不會産生一份重複的資料。那有的朋友可能會問了,這樣把已經加載好的View又清除掉,待會還要再重新加載一遍,這不是嚴重影響效率嗎?不用擔心,還記得我們剛剛調用了RecycleBin的fillActiveViews()方法來緩存子View嗎,待會兒将會直接使用這些緩存好的View來進行加載,而并不會重新執行一遍inflate過程,是以效率方面并不會有什麼明顯的影響。

那麼我們接着看,進入mLayoutMode的判斷邏輯當中,依舊進入default的case。由于childCount 不再等于0了,是以會進入到else語句當中。而else語句中又有三個邏輯判斷,第一個邏輯判斷不成立,因為預設情況下我們沒有選中任何子元素,mSelectedPosition應該等于-1。第二個邏輯判斷通常是成立的,因為mFirstPosition的值一開始是等于0的,隻要adapter中的資料大于0條件就成立。那麼進入到fillSpecific()方法當中,代碼如下所示:

/**
     * Put a specific item at a specific location on the screen and then build
     * up and down from there.
     *
     * @param position The reference view to use as the starting point
     * @param top Pixel offset from the top of this view to the top of the
     *        reference view.
     *
     * @return The selected view, or null if the selected view is outside the
     *         visible area.
     */
    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 - , temp.getTop() - dividerHeight);
            // This will correct for the top of the first view not touching the top of the list
            adjustViewsUpOrDown();
            below = fillDown(position + , temp.getBottom() + dividerHeight);
            int childCount = getChildCount();
            if (childCount > ) {
                correctTooHigh(childCount);
            }
        } else {
            below = fillDown(position + , temp.getBottom() + dividerHeight);
            // This will correct for the bottom of the last view not touching the bottom of the list
            adjustViewsUpOrDown();
            above = fillUp(position - , temp.getTop() - dividerHeight);
            int childCount = getChildCount();
            if (childCount > ) {
                 correctTooLow(childCount);
            }
        }

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

fillSpecific()算是一個新方法了,不過其實它和fillUp()、fillDown()方法功能也是差不多的,根據代碼注釋我們知道主要的差別在于:fillSpecific()方法會優先将指定位置的子View先加載到螢幕上,然後再加載該子View往上以及往下的其它子View。那麼由于這裡我們傳入的position就是第一個子View的位置,于是fillSpecific()方法的作用就基本上和fillDown()方法是差不多的了,這裡我們就不去關注太多它的細節,而是将精力放在makeAndAddView()方法上面。再次回到makeAndAddView()方法,代碼如下所示:

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
            boolean selected) {
        View child;


        if (!mDataChanged) {
            // Try to use an existing view for this position
            child = mRecycler.getActiveView(position);
            if (child != null) {
                // Found it -- we're using an existing child
                // This just needs to be positioned
                setupChild(child, position, y, flow, childrenLeft, selected, true);

                return child;
            }
        }

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

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

        return child;
    }
           

仍然還是嘗試從RecycleBin當中擷取Active View,然而這次就一定可以擷取到了,因為前面我們調用了RecycleBin的fillActiveViews()方法來緩存子View。那麼既然如此,就不會再進入obtainView()方法,而是會直接進入setupChild()方法當中,這樣也省去了很多時間,因為如果在obtainView()方法中又要去infalte布局的話,那麼ListView的初始加載效率就大大降低了。

注意在這裡setupChild()方法的最後一個參數傳入的是true,這個參數表明目前的View是之前被回收過的,那麼我們再次回到setupChild()方法當中:

private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
            boolean selected, boolean recycled) {
        ……
        if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter
                && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
            attachViewToParent(child, flowDown ? - : , p);
        } else {
            p.forceAdd = false;
            if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                p.recycledHeaderFooter = true;
            }
            addViewInLayout(child, flowDown ? - : , p, true);
        }
        ……
    } 
           

可以看到,setupChild()方法的最後一個參數是recycled,在方法中會對這個變量進行判斷,由于recycled現在是true,是以會執行attachViewToParent()方法,而第一次Layout過程則是執行的else語句中的addViewInLayout()方法。這兩個方法最大的差別在于,如果我們需要向ViewGroup中添加一個新的子View,應該調用addViewInLayout()方法,而如果是想要将一個之前detach的View重新attach到ViewGroup上,就應該調用attachViewToParent()方法。

那麼由于前面在layoutChildren()方法當中調用了detachAllViewsFromParent()方法,這樣ListView中所有的子View都是處于detach狀态的,是以這裡attachViewToParent()方法是正确的選擇。

經曆了這樣一個detach又attach的過程,ListView中所有的子View又都可以正常顯示出來了,那麼第二次Layout過程結束。

滑動加載更多資料

經曆了兩次Layout過程,雖說我們已經可以在ListView中看到内容了,然而關于ListView最神奇的部分我們卻還沒有接觸到,因為目前ListView中隻是加載并顯示了第一屏的資料而已。比如說我們的Adapter當中有1000條資料,但是第一屏隻顯示了10條,ListView中也隻有10個子View而已,那麼剩下的990是怎樣工作并顯示到界面上的呢?這就要看一下ListView滑動部分的源碼了,因為我們是通過手指滑動來顯示更多資料的。

由于滑動部分的機制是屬于通用型的,即ListView和GridView都會使用同樣的機制,是以這部分代碼就肯定是寫在AbsListView當中的了。那麼監聽觸控事件是在onTouchEvent()方法當中進行的,我們就來看一下AbsListView中的這個方法:

public boolean onTouchEvent(MotionEvent ev) {
        ……
        switch (actionMasked) {
            case MotionEvent.ACTION_DOWN: {
                onTouchDown(ev);
                break;
            }

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

這個方法中的代碼就非常多了,因為它所處理的邏輯也非常多,要監聽各種各樣的觸屏事件。但是我們目前所關心的就隻有手指在螢幕上滑動這一個事件而已,對應的是ACTION_MOVE這個動作,那麼我們就繼續進入onTouchMove():

private void onTouchMove(MotionEvent ev, MotionEvent vtev) {
        int pointerIndex = ev.findPointerIndex(mActivePointerId);
        if (pointerIndex == -) {
            pointerIndex = ;
            mActivePointerId = ev.getPointerId(pointerIndex);
        }

        if (mDataChanged) {
            // Re-sync everything if data has been changed
            // since the scroll operation can query the adapter.
            layoutChildren();
        }

        final int y = (int) ev.getY(pointerIndex);

        switch (mTouchMode) {
            case TOUCH_MODE_DOWN:
            case TOUCH_MODE_TAP:
            case TOUCH_MODE_DONE_WAITING:
                // Check if we have moved far enough that it looks more like a
                // scroll than a tap. If so, we'll enter scrolling mode.
                if (startScrollIfNeeded((int) ev.getX(pointerIndex), y, vtev)) {
                    break;
                }
                // Otherwise, check containment within list bounds. If we're
                // outside bounds, cancel any active presses.
                final View motionView = getChildAt(mMotionPosition - mFirstPosition);
                final float x = ev.getX(pointerIndex);
                if (!pointInView(x, y, mTouchSlop)) {
                    setPressed(false);
                    if (motionView != null) {
                        motionView.setPressed(false);
                    }
                    removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ?
                            mPendingCheckForTap : mPendingCheckForLongPress);
                    mTouchMode = TOUCH_MODE_DONE_WAITING;
                    updateSelectorState();
                } else if (motionView != null) {
                    // Still within bounds, update the hotspot.
                    final float[] point = mTmpPoint;
                    point[] = x;
                    point[] = y;
                    transformPointToViewLocal(point, motionView);
                    motionView.drawableHotspotChanged(point[], point[]);
                }
                break;
            case TOUCH_MODE_SCROLL:
            case TOUCH_MODE_OVERSCROLL:
                scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev);
                break;
        }
    }
           

其中滑動事件會進入TOUCH_MODE_OVERSCROLL的case,調用scrollIfNeeded(),進而會在scrollIfNeeded()中調用trackMotionScroll():

boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
        ……
        if (down) {
            int top = -incrementalDeltaY;
            if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
                top += listPadding.top;
            }
            for (int i = ; 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 - ; i >= ; i--) {
                final View child = getChildAt(i);
                if (child.getTop() <= bottom) {
                    break;
                } else {
                    start = i;
                    count++;
                    int position = firstPosition + i;
                    if (position >= headerViewsCount && position < footerViewsStart) {
                        // The view will be rebound to new data, clear any
                        // system-managed transient state.
                        child.clearAccessibilityFocus();
                        mRecycler.addScrapView(child, position);
                    }
                }
            }
        }

        mMotionViewNewTop = mMotionViewOriginalTop + deltaY;

        mBlockLayoutRequests = true;

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

        // invalidate before moving the children to avoid unnecessary invalidate
        // calls to bubble up from the children all the way to the top
        if (!awakenScrollBars()) {
           invalidate();
        }

        offsetChildrenTopAndBottom(incrementalDeltaY);

        if (down) {
            mFirstPosition += count;
        }

        final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
        if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
            fillGap(down);
        }
        ……
    }
           

這個方法接收兩個參數,deltaY表示從手指按下時的位置到目前手指位置的距離,incrementalDeltaY則表示據上次觸發event事件手指在Y方向上位置的改變量,那麼其實我們就可以通過incrementalDeltaY的正負值情況來判斷使用者是向上還是向下滑動的了。如果incrementalDeltaY小于0,說明是向下滑動,否則就是向上滑動。

下面将會進行一個邊界值檢測的過程,可以看到當ListView向下滑動的時候,就會進入一個for循環當中,從上往下依次擷取子View,如果該子View的bottom值已經小于top值了,就說明這個子View已經移出螢幕了,是以會調用RecycleBin的addScrapView()方法将這個View加入到廢棄緩存當中,并将count計數器加1,計數器用于記錄有多少個子View被移出了螢幕。那麼如果是ListView向上滑動的話,其實過程是基本相同的,隻不過變成了從下往上依次擷取子View,然後判斷該子View的top值是不是大于bottom值了,如果大于的話說明子View已經移出了螢幕,同樣把它加入到廢棄緩存中,并将計數器加1。

接下來會根據目前計數器的值來進行一個detach操作,它的作用就是把所有移出螢幕的子View全部detach掉,在ListView的概念當中,所有看不到的View就沒有必要為它進行儲存,因為螢幕外還有成百上千條資料等着顯示呢,一個好的回收政策才能保證ListView的高性能和高效率。緊接着調用了offsetChildrenTopAndBottom()方法,并将incrementalDeltaY作為參數傳入,這個方法的作用是讓ListView中所有的子View都按照傳入的參數值進行相應的偏移,這樣就實作了随着手指的拖動,ListView的内容也會随着滾動的效果。

然後會進行判斷,如果ListView中最後一個View的底部已經移入了螢幕,或者ListView中第一個View的頂部移入了螢幕,就會調用fillGap()方法,那麼是以我們就可以猜出fillGap()方法是用來加載螢幕外資料的,進入到ListView中的fillGap():

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

down參數用于表示ListView是向下滑動還是向上滑動的,可以看到,如果是向下滑動的話就會調用fillDown()方法,而如果是向上滑動的話就會調用fillUp()方法。那麼這兩個方法我們都已經非常熟悉了,内部都是通過一個循環來去對ListView進行填充,是以這兩個方法我們就不看了,但是填充ListView會通過調用makeAndAddView()方法來完成,又是makeAndAddView()方法,但這次的邏輯再次不同了,是以我們還是回到這個方法瞧一瞧:

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
            boolean selected) {
        View child;


        if (!mDataChanged) {
            // Try to use an existing view for this position
            child = mRecycler.getActiveView(position);
            if (child != null) {
                // Found it -- we're using an existing child
                // This just needs to be positioned
                setupChild(child, position, y, flow, childrenLeft, selected, true);

                return child;
            }
        }

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

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

        return child;
    }
           

不管怎麼說,這裡首先仍然是會嘗試調用RecycleBin的getActiveView()方法來擷取子布局,隻不過肯定是擷取不到的了,因為在第二次Layout過程中我們已經從mActiveViews中擷取過了資料,而根據RecycleBin的機制,mActiveViews是不能夠重複利用的,是以這裡傳回的值肯定是null。

既然getActiveView()方法傳回的值是null,那麼就還是會走到obtainView()方法當中,代碼如下所示:

View obtainView(int position, boolean[] isScrap) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");

        isScrap[] = false;

        // Check whether we have a transient state view. Attempt to re-bind the
        // data and discard the view if we fail.
        final View transientView = mRecycler.getTransientStateView(position);
        if (transientView != null) {
            final LayoutParams params = (LayoutParams) transientView.getLayoutParams();

            // If the view type hasn't changed, attempt to re-bind the data.
            if (params.viewType == mAdapter.getItemViewType(position)) {
                final View updatedView = mAdapter.getView(position, transientView, this);

                // If we failed to re-bind the data, scrap the obtained view.
                if (updatedView != transientView) {
                    setItemViewLayoutParams(updatedView, position);
                    mRecycler.addScrapView(updatedView, position);
                }
            }

            isScrap[] = true;

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

這裡會調用RecyleBin的getScrapView()方法來嘗試從廢棄緩存中擷取一個View,那麼廢棄緩存有沒有View呢?當然有,因為剛才在trackMotionScroll()方法中我們就已經看到了,一旦有任何子View被移出了螢幕,就會将它加入到廢棄緩存中,而從obtainView()方法中的邏輯來看,一旦有新的資料需要顯示到螢幕上,就會嘗試從廢棄緩存中擷取View。是以它們之間就形成了一個生産者和消費者的模式,那麼ListView神奇的地方也就在這裡展現出來了,不管你有任意多條資料需要顯示,ListView中的子View其實來來回回就那麼幾個,移出螢幕的子View會很快被移入螢幕的資料重新利用起來,因而不管我們加載多少資料都不會出現OOM的情況,甚至記憶體都不會有所增加。

那麼另外還有一點是需要大家留意的,這裡擷取到了一個scrapView,然後我們在将它作為第二個參數傳入到了Adapter的getView()方法當中。這個參數就是我們最熟悉的convertView呀,難怪平時我們在寫getView()方法是要判斷一下convertView是不是等于null,如果等于null才調用inflate()方法來加載布局,不等于null就可以直接利用convertView,因為convertView就是我們之間利用過的View,隻不過被移出螢幕後進入到了廢棄緩存中,現在又重新拿出來使用而已。然後我們隻需要把convertView中的資料更新成目前位置上應該顯示的資料,那麼看起來就好像是全新加載出來的一個布局一樣,這背後的道理你是不是已經完全搞明白了?

之後的代碼又都是我們熟悉的流程了,從緩存中拿到子View之後再調用setupChild()方法将它重新attach到ListView當中,因為緩存中的View也是之前從ListView中detach掉的,這部分代碼就不再重複進行分析了。

流程圖:

1.第一次Layout方法調用順序:

ListView工作原理分析

2.第二次Layout方法調用順序:

ListView工作原理分析

3.滑動時方法調用順序:

ListView工作原理分析

示意圖:

ListView工作原理分析

參考:

1. Android ListView工作原理完全解析,帶你從源碼的角度徹底了解

2. Android AdapterView 源碼分析以及其相關回收機制的分析

3. ListView原理分析