1. 程式人生 > >ListView 快取機制原始碼淺析

ListView 快取機制原始碼淺析

      ListView的快取機制是通過RecycleBin實現對View的快取實現的。快取機制大大的提高了View的複用率,這也是為什麼ListView可以載入大量的Item也不會OOM的原因。當然了,為了進一步提高效能我們都會自定義一個ViewHolder來避免不必要的findViewById操作,這也是RecycleView與ListView的區別之一<RecycleView快取的是ViewHolder>。RecycleBin是ListView/GridView的父類AbsListView的內部類,這說明ListView/GridView的快取機制大致是相同的。

一、概覽


當item view 在螢幕之外,RecycleBin將其快取起來,當需要新的itemView,除非RecycleBin中沒有快取,否則就直接從快取中取,還記得在getView中有一個引數convertView就是從RecycleBin中取出來的。

二、RecycleBin


由上圖我們可以看大,RecycleBin主要通過兩個集合:View[] mActiveViews和ArrayList<View>[] mScrapViews來實現的。

下面看一下RecycleBin中需要重點關注的部分:

private View[] mActiveViews:快取當前螢幕可以看到的item view;這個陣列是為了防止多次layout帶來的效能即資料重複問題而存在的。因為多次layout會導致ListView重複多次載入當前的螢幕顯示的item views,為了避免資料重複以及多次例項化item views帶來的資源浪費,有必要將其快取起來。
private int mViewTypeCount:item view的種類;
private ArrayList<View>[] mScrapViews:注意到這是一個ArrayList<View>陣列,之所以是一個數組,是因為item view的種類可能大於一,而mScrapViews:就是快取每種item View的。還記得BaseAdapter裡面有一個getViewTypeCount()方法嗎?這個方法的返回值就是mViewTypeCount。
private ArrayList<View> mCurrentScrap:當前種類的item view的快取。

public void setViewTypeCount(int viewTypeCount):設定列表item的種類; 
void fillActiveViews(int childCount, int firstActivePosition):將當前螢幕可以看到的item view新增到mActiveViews中;
View getActiveView(int position):根據position獲取mActiveViews中的item view;
void addScrapView(View scrap, int position):將廢棄的(通常是滾出螢幕外的)item view快取起來;簡單的看一下addSrapView的原始碼:

void addScrapView(View scrap, int position){
                .......
                if (mViewTypeCount == 1) {
                    mCurrentScrap.add(scrap);
                } else {
                    mScrapViews[viewType].add(scrap);
                }


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

這驗證了我們對mScrapViews的分析;
View getScrapView(int position):根據position的值獲取一個廢棄的item view用於重用;

三、詳細快取流程

ListView是ViewGroup的子類,遵循自定義View的基本步驟,measure、layout、draw。快取部分我們重點分析layout過程。下面看一下ListView的layoutChildren部分原始碼:

  // Pull all children into the RecycleBin.
            // These views will be reused if possible
            final int firstPosition = mFirstPosition;
            final RecycleBin recycleBin = mRecycler;
            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();
            recycleBin.removeSkippedScrap();

            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 - 1, childrenBottom);
                adjustViewsUpOrDown();
                break;
            case LAYOUT_FORCE_TOP:
                mFirstPosition = 0;
                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 == 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;
            }

            // Flush any cached views that did not get reused above
            recycleBin.scrapActiveViews();
第一次layout的時候,由於資料還沒有顯示,仍然有Adapter管理,並沒有填充到ListView中,因此,childCount等於0。datasetchanged只有在資料來源發生變化的情況下才會是true,其他的都是false。因此,會執行recycleBin.fillActiveViews(childCount, firstPosition);,但是此時childCount等於0,因此並不會向mActiveViews新增任何東西。

繼續向下看,mLayoutMode代表了佈局模式,預設情況下是LAYOUT_NORMAL,因此會走default語句中。因為childCount是0,並且預設的順序從上到下的,因此會執行fillFromTop -- 執行該函式就可以將Adapter中的資料填充到ListView中了,填充完成之後,childCount就大於0了。下面看一下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);
    }
可以看到fillFromTop呼叫了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() - 1);
        return selectedView;
    }
可以看到View的產生是通過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[0]);

        return child;
    }
由於mRecycler.getActiveView返回null,因此,最終makeAndAddView又呼叫了obtainView生成View的。下面看一下obtainView的部分原始碼:
......
        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();
            }
        }
......
return child;
看到這一句是不是很熟悉,final View child = mAdapter.getView(position, scrapView, this);沒錯,這就是我們自定義adapter的時候,重寫的getView方法。由於第一次layout的時候,mRecycleView還沒有快取,因此scrapView是null,也就是說此時我們填充ListView都是通過getView產生的。

下面看一下第二次layout的時候,會發生什麼

// Pull all children into the RecycleBin.
            // These views will be reused if possible
            final int firstPosition = mFirstPosition;
            final RecycleBin recycleBin = mRecycler;
            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();
            recycleBin.removeSkippedScrap();

            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 - 1, childrenBottom);
                adjustViewsUpOrDown();
                break;
            case LAYOUT_FORCE_TOP:
                mFirstPosition = 0;
                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 == 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;
            }

            // Flush any cached views that did not get reused above
            recycleBin.scrapActiveViews();
第二次layout開始,會執行我們所說的fillActiveViews過程,因此此時childCount不再是0,所以當前螢幕顯示的item view會被快取到mActiveViews中。緊接著執行了detachAllViewsFromParent()方法,該方法清空了所有的view,這是為了避免向下執行填充之後出現數據重複。之後的執行邏輯與前面分析的相似了,mActiveViews中快取的View會在makeAndAddView中使用。
滑動過程中,會產生大量的Item View,RecycleBin也發揮了重要的作用。這也是ListView不會OOM的原因。