1. 程式人生 > >RecycleBin快取機制

RecycleBin快取機制

在學習Android開發的過程中,大家應該或多或少的會接觸到ListView這個控制元件。那麼,大家有沒有思考過,一個螢幕只能顯示一定的item,每次下滑後,消失的item去哪裡了?當再次回到原來的位置的時候,加載出來的item是重新獲取的還是從快取空間裡提取的呢?接下來,我們通過分析RecycleBin機制來探尋ListView的快取機制。

RecycleBin的基本原理

RecycleBin中有兩個陣列,mActiveViews和mScrapViews分別儲存可以直接複用的view(處於可見狀態的view)和間接複用的view(處於不可見狀態的view)。當螢幕向下滑動的時候,頂部view不可見時,會將其回收至RecycleBin中的mScrapViews陣列中進行儲存,底部需要顯示一個新的View時,會從mScrapViews中取出一個View傳至convertView進行復用。

在ListView中,重寫addView方法,當呼叫時,會丟擲異常。ListView是一幀一幀繪製的,會經歷measure->layout->draw方法。在ListView佈局的時候,會呼叫layoutChildren方法繪製子View。當剛開始執行layout的時候,ListView的children是上一幀中需要繪製的view的集合,當layout執行完畢時,children變成當前幀需要繪製的子View的集合。

當一個view不可見時,首先會將該view移至RecycleBin。會根據資料是否發生變化呼叫不同的方法。如果資料發生變化,會將所有字View移至RecycleBin的mScrapView陣列中進行儲存,資料未發生改變在將當前View放入mActiveViews陣列中。

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

然後會將不可見的View清楚,呼叫detachAllViewFromParent()方法,將該view設定為null。

protected void detachAllViewsFromParent() {
    final int count = mChildrenCount;
    if (count <= 0) {
        return;
    }

    final View[] children = mChildren;
    mChildrenCount = 0;

    for (int i = count - 1; i >= 0; i--) {
        children[i].mParent = null;
        children[i] = null;
    }

當需要將一個view從RecycleBin中傳至ListView時,會根據mLayoutMode的不同情況呼叫fillUp,fillDown等方法。

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;
 

我們以fillDown方法為例子,該方法傳入兩個引數,一個是view對應adapter資料來源中的位置,和下一個view在ListView中的起點位置。當nextTop小於ListView的高度並且pos小於資料來源的資料總數時,將當前view新增進ListView並改變nextTop和pos的資料,實現pos的自增和下一個view的起點。

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

接下來我們看一下makeAndAddView方法是怎麼將view新增進ListView的
在makeAndAddView中,如果資料來源沒有發生改變,我們會首先通過getActiveView方法在mActiveViews中查詢是否存在該View,如果不存在,通過呼叫obtainView方法從mScrapViews陣列中查詢是否存在。最後呼叫setupChild對viwe進行定位和量算。

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
            boolean selected) {
        View child;
        if (!mDataChanged) {
            child = mRecycler.getActiveView(position);
            if (child != null) {
                setupChild(child, position, y, flow, childrenLeft, selected, true);
                return child;
            }
        }   
        child = obtainView(position, mIsScrap);      
        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
        return child;
    }