ListView學習(二)-ListView緩存機制

打開ListView源碼查看ListView的繼承結構,發現ListView是繼承自AbsListView



ListView作爲列表,可以顯示成百上千個item。如果有多少數據,就創建多少個item,會佔用很大內存,但是大多數item並沒有顯示在屏幕上,造成了內存浪費,所以移除屏幕的view就可以緩存起來,以便下次重用。ListView爲我們提供了convertView,來複用view,這裏介紹下ListView的緩存機制。

1. RecycleBin類
查看ListView源碼並沒有緩存的相關代碼,那一定是在父類中了。查看AbsListView的成員變量,有個mRecycler,看註釋:用來存儲不使用的view,其將被下次layout的時候重新使用,以避免創建新的實例。

/**
 * The data set used to store unused views that should be reused during the next layout
 * to avoid creating new ones
 */
final RecycleBin mRecycler = new RecycleBin();

看下RecycleBin這個類型,點進去查看,原來RecycleBin是AbsListView的內部類。

RecycleBin:大意就是通過兩級緩存來緩存view。(RecycleBin在layout的過程中便於view重用,RecycleBin有兩級存儲:ActiveViews和ScrapViews。ActiveViews存儲的是layout開始的時候屏幕上那些view。layout結束後,所有ActiveViews中的view被移動到ScrapViews中。ScrapViews中的views是那些可能被adapter重新用到的view,以避免重新創建不必要的view。)

1.1 RecycleBin成員變量

貼出RecycleBin的成員變量

/**
 * 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.
 */
class RecycleBin {
    private RecyclerListener mRecyclerListener;
    /**
     * The position of the first view stored in mActiveViews.
     */
    private int mFirstActivePosition;
    /**
     * Views that were on screen at the start of layout. This array is populated at the start of
     * layout, and at the end of layout all view in mActiveViews are moved to mScrapViews.
     * Views in mActiveViews represent a contiguous range of Views, with position of the first
     * view store in mFirstActivePosition.
     */
    private View[] mActiveViews = new View[0];
    /**
     * Unsorted views that can be used by the adapter as a convert view.
     */
    private ArrayList<View>[] mScrapViews;
    private int mViewTypeCount;
    private ArrayList<View> mCurrentScrap;
    private ArrayList<View> mSkippedScrap;
    private SparseArray<View> mTransientStateViews;
    private LongSparseArray<View> mTransientStateViewsById;
}

mActiveViews:一級緩存,顧名思義活動等view,這些view是佈局過程開始屏幕上的view。layout開始時這個數組被填充,layout結束,mActiveViews中的view移動到mScrapViews。mActiveViews代表了一個連續範圍的views,其第一個view的位置存儲在mFirstActivePosition變量中。


mScrapViews :二級緩存,顧名思義廢棄的view,無序的被adapter的convertView使用的view的集合
mScrapViews是多個list組成的數組,數組的長度爲viewTypeCount,每個item是個list,所以每個list緩存不同類型item佈局的view,所以mScrapViews應該是下圖的樣子。


 
mCurrentScrap:是個List,當ListView的item佈局只有一種的時候使用該變量緩存view。所以mCurrentScrap是個簡化的mScrapViews。

1.2 RecycleBin的方法

看下RecycleBin與上述變量有關的方法,總體是對兩個變量內元素添加移動刪除等操作:(請對照源碼閱讀)

void fillActiveViews(int childCount, int firstActivePosition)    ListView中的子view緩存到mActiveViews中
用ListView的所有子view填充ActiveViews,其中childCount是mActiveViews應該保存的最少的view數,firstActivePosition是mActiveViews中存儲的首個view的位置。從代碼看該方法的處理邏輯爲將當前ListView的0-childCount個view中的非header、footer添加到mActiveViews數組中。當Adapter中的數據個數未發生變化時,此時用戶可能只是滾動,或點擊等操作,ListView中item的個數會發生變化,因此,需要將可視的item加入到mActiveView中來管理。

View getActiveView(int position)   獲取一個mActiveViews
獲取mActiveViews中指定位置的view,如果找到會將該view從mActiveViews中移除。position是adapter中的絕對下標值,mFirstActivePosition前面說過了,是當前可視區域的下標值,對應在adapter中的絕對值,如果找到,則返回找到的View,並將mActiveView對應的位置設置爲null。

void scrapActiveViews()  
將mActiveViews 中剩餘的view放入mScrapViews。實際上就是將mActiveView中未使用的view回收(因爲,此時已經移出可視區域了)。會調用mRecyclerListener.onMovedToScrapHeap(scrap);

private void pruneScrapViews()  確保mScrapViews 的數目不會超過mActiveViews的數目
 (This can happen if an adapter does not recycle its views)。mScrapView中每個ScrapView數組大小不應該超過mActiveView的大小,如果超過,系統認爲程序並沒有複用convertView,而是每次都是創建一個新的view,爲了避免產生大量的閒置內存且增加OOM的風險,系統會在每次回收後,去檢查一下,將超過的部分釋放掉,節約內存降低OOM風險。

public void setViewTypeCount(int viewTypeCount)
設置子view的類型數目,這裏會初始化mScrapViews數組

public void markChildrenDirty()
爲每個子類調用forceLayout()。將mScrapView中回收回來的View設置一樣標誌,在下次被複用到ListView中時,告訴viewroot重新layout該view。forceLayout()方法只是設置標誌,並不會通知其parent來重新layout。

void clear()
清空廢棄view堆,並將這些View從窗口中分離。

View getScrapView(int position)
從廢棄堆中獲取一個view,並從堆中移除

void reclaimScrapViews(List<View> views)
mScrapView中所有的緩存view全部添加到指定的view list

2.ListView的緩存原理

2.1 從ListView繪製說起

看ListView的繪製過程代碼,主要看layout的過程,ListView沒有實現onLayout,因爲AbsListView中實現了onLayout,這個方法調用了一個layoutChildre方法,看註釋。

/**
 * Subclasses must override this method to layout their children.
 */
protected void layoutChildren() {
}

子類必須重寫該方法來layout它們的子元素。所以ListView實現了layoutChildren方法。該方法較長,刪除了些代碼,由於分支較多,只分析第一次layout的路徑,留下了與RecycleBin有關的些代碼。重要看有註釋位置:

@Override
protected void layoutChildren() {
    final boolean blockLayoutRequests = mBlockLayoutRequests;
    if (blockLayoutRequests) {
        return;
    }

    mBlockLayoutRequests = true;

    try {
        super.layoutChildren();

        ......

        // Pull all children into the RecycleBin.
        // These views will be reused if possible
        final int firstPosition = mFirstPosition;
        final RecycleBin recycleBin = mRecycler;
        if (dataChanged) {
            //如果數據發生變化,把ListView的子view緩存到mScrapView中
            for (int i = 0; i < childCount; i++) {
                recycleBin.addScrapView(getChildAt(i), firstPosition+i);
            }
        } else {
            //數據沒有變化緩存到mActiveViews中
            recycleBin.fillActiveViews(childCount, firstPosition);
        }

        // Clear out old views
        //清空分離listView中的view
        detachAllViewsFromParent();

        recycleBin.removeSkippedScrap();

        ......

        switch (mLayoutMode) {
            ......
            default:
                if (childCount == 0) {
                    if (!mStackFromBottom) {
                        final int position = lookForSelectablePosition(0, true);
                        setSelectedPositionInt(position);
                        //mLayoutMode默認爲LAYOUT_NORMAL,第一次執行到這裏
                        //該方法向下調用到makeAndAddView方法中
                        //數據沒有變化最終會mRecycler.getActiveView(position)
                        //數據有變化最後會調用,mRecycler.getScrapView(position)或者adapter.getView()獲取一個view
                        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;
        }

        // Flush any cached views that did not get reused above
        //把mActiveViews中剩餘的view移動到ScrapViews中
        recycleBin.scrapActiveViews();

        ......

    } finally {
        if (!blockLayoutRequests) {
            mBlockLayoutRequests = false;
        }
    }
}

從源碼看繪製是下邊的樣子:



由於父ViewGroup的原因可能導致ListView layout多次
第一次由於ListView裏沒有item了所以與RecycleBin無關,最後調用adapter.getView獲得要layout的view。
第二次之後在layout的時候就是執行以下步驟,使用緩存的時候可能執行下邊兩個分支。

在makeAndAddView方法(重要方法)中,顧名思義就是make一個view添加到list上去。怎麼make呢,有三種情況:一是從mActiveViews中獲取,二是mAdapter中獲取,三是從mScrapViews中獲取了,最後在調用setupChild方法把make的view添加到ListView上去。


layoutChildren中有個mDataChanged,從單詞的意思我們就可以瞭解,這裏的優化規則就是基於數據是否有變化,(當Adapter中的數據個數未發生變化時,此時用戶可能只是滾動,或點擊等操作,ListView中item的個數會發生變化,因此,需要將可視的item加入到mActiveView中來管理。)
當爲false,走分支1用mActiveViews緩存:



mDataChange爲true走分支2,使用mScrapViews緩存




2.2 ListView滾動

ListView滾動的時候如何緩存view呢

當滑動的時候添加view,緩存view的操作是在ListView的scrollListItemsBy方法中做的,該方法邏輯比較清晰,就是向上滑動與向下滑動的邏輯處理,amount爲偏移量。

/**
 * Scroll the children by amount, adding a view at the end and removing
 * views that fall off as necessary.
 *
 * @param amount The amount (positive or negative) to scroll.
 */
private void scrollListItemsBy(int amount) {

    //按指定像素垂直方向上移動所有子view
    offsetChildrenTopAndBottom(amount);

    final int listBottom = getHeight() - mListPadding.bottom;
    final int listTop = mListPadding.top;
    final AbsListView.RecycleBin recycleBin = mRecycler;

    if (amount < 0) {//向上滑動
        // shifted items up

        // may need to pan views into the bottom space
        int numChildren = getChildCount();
        View last = getChildAt(numChildren - 1);
        //當最下邊的一個人view的底部縱座標小於ListView的底部縱座標的時候 需要在ListView底部添加一個view
        while (last.getBottom() < listBottom) {
            final int lastVisiblePosition = mFirstPosition + numChildren - 1;
            if (lastVisiblePosition < mItemCount - 1) {
                last = addViewBelow(last, lastVisiblePosition);
                numChildren++;
            } else {
                break;
            }
        }

        // may have brought in the last child of the list that is skinnier
        // than the fading edge, thereby leaving space at the end.  need
        // to shift back
        //如果最後一個與ListView最下邊有空隙,移動所有子view向下
        if (last.getBottom() < listBottom) {
            offsetChildrenTopAndBottom(listBottom - last.getBottom());
        }

        // top views may be panned off screen
        //最上邊的view可能移除了屏幕 添加到scrapView中
        View first = getChildAt(0);
        while (first.getBottom() < listTop) {
            AbsListView.LayoutParams layoutParams = (AbsListView.LayoutParams) first.getLayoutParams();
            if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {
                recycleBin.addScrapView(first, mFirstPosition);
            }
            detachViewFromParent(first);
            first = getChildAt(0);
            mFirstPosition++;
        }
    } else {//向下滑動
        // shifted items down
        View first = getChildAt(0);

        // may need to pan views into top
        //可能需要在最上邊添加view
        while ((first.getTop() > listTop) && (mFirstPosition > 0)) {
            first = addViewAbove(first, mFirstPosition);
            mFirstPosition--;
        }

        // may have brought the very first child of the list in too far and
        // need to shift it back
        if (first.getTop() > listTop) {
            offsetChildrenTopAndBottom(listTop - first.getTop());
        }

        int lastIndex = getChildCount() - 1;
        View last = getChildAt(lastIndex);

        // bottom view may be panned off screen
        //最下邊的view可能需要緩存起來
        while (last.getTop() > listBottom) {
            AbsListView.LayoutParams layoutParams = (AbsListView.LayoutParams) last.getLayoutParams();
            if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {
                recycleBin.addScrapView(last, mFirstPosition+lastIndex);
            }
            detachViewFromParent(last);
            last = getChildAt(--lastIndex);
        }
    }
}

向上滑動的



向下滑動同理。

這只是一個類型的item,多類型item對於的item會存入對應的ScrapViews列表數組中。需要adapter實現getItemViewType,getViewTypeCount兩個方法,來判斷存入哪個列表。

看上述方法中的 addViewAbove,addViewBelow分別是在上邊下邊添加view的方法,看他是怎麼從scrapView獲得一個view的

private View addViewAbove(View theView, int position) {
    int abovePosition = position - 1;
    View view = obtainView(abovePosition, mIsScrap);
    int edgeOfNewChild = theView.getTop() - mDividerHeight;
    setupChild(view, abovePosition, edgeOfNewChild, false, mListPadding.left,
            false, mIsScrap[0]);
    return view;
}

private View addViewBelow(View theView, int position) {
    int belowPosition = position + 1;
    View view = obtainView(belowPosition, mIsScrap);
    int edgeOfNewChild = theView.getBottom() + mDividerHeight;
    setupChild(view, belowPosition, edgeOfNewChild, true, mListPadding.left,
            false, mIsScrap[0]);
    return view;
}

看他們內部都是調用obtainView方法獲取一個view
獲取一個view,使它顯示與位置關聯的數據。當這個方法被調用時,說明Recycle bin中的view已經不可用了,那麼,現在唯一的方法就是,convert一個老的view,或者構造一個新的view。
position: 要顯示的位置
isScrap: 是個boolean數組, 如果view從scrap heap獲取,isScrap [0]爲true,否則爲false。

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

    isScrap[0] = 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[0] = 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[0] = true;

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

    if (mCacheColorHint != 0) {
        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;
}

其中有個mRecycler.getScrapView方法,看他的內部

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

對佈局是一個類型還是多類型做了不同處理,都調用了retrieveFromScrap方法。

private View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
    final int size = scrapViews.size();
    if (size > 0) {
        // See if we still have a view for this position or ID.
        for (int i = 0; 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 - 1);
        clearAccessibilityFromScrap(scrap);
        return scrap;
    } else {
        return null;
    }
}

大意就是優先獲取相同Id的view,然後是獲取本來這個位置的view,最後就是獲取scrapView的最後一個view。至此滑動時候ListView如何緩存分析完了,其中還有一些細節有待分析比如transientView。初寫博客有錯誤歡迎大家指正。

參考:
Android6.0.1_r10源碼
發佈了30 篇原創文章 · 獲贊 62 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章