listView復(fù)用解析

1.ListView第一次加載

時(shí)序圖:
https://www.processon.com/view/link/5bd7b047e4b0fef7882c2fda

ListView第一次加載界面.png

第一次加載時(shí),因?yàn)闆](méi)有緩存view,所以通過(guò)adapter的getItem來(lái)獲得要加載的view咬摇,ScrapView緩存為空罪郊,所以convertView為空反璃。

2.第二次Layout

即使是一個(gè)再簡(jiǎn)單的View取董,在展示到界面上之前都會(huì)經(jīng)歷至少兩次onMeasure()和兩次onLayout()的過(guò)程盐茎,這就意味著layoutChildren()過(guò)程會(huì)執(zhí)行兩次者冤。

與第二次不同的是肤视,ListView已經(jīng)有第一次加載的緩存,直接讀取緩存加載界面涉枫。

時(shí)序圖:
https://www.processon.com/view/link/5bd7d149e4b056d0cf9004c6

ListView第二次Layout.png

@Override
    protected void layoutChildren() {
        ...
        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();
        ...
        //選擇加載view方案
         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;
            }
         }
        //  將所有沒(méi)有被重用到的view從mActiveViews轉(zhuǎn)移到mScrapViews中去
            recycleBin.scrapActiveViews();
    }
  1. 當(dāng)數(shù)據(jù)沒(méi)有變化時(shí)邢滑,調(diào)用fillActiveViews用來(lái)緩存所有的子View,然后調(diào)用detachAllViewsFromParent來(lái)清空view愿汰,在后面調(diào)用makeAndAddView時(shí)調(diào)用mRecycler.getActiveView(position)來(lái)獲取緩存困后。如果mActiveViews還有剩余的view沒(méi)有復(fù)用,最后都移到ScrapViews中去衬廷。
  2. 當(dāng)數(shù)據(jù)變化時(shí)摇予,調(diào)用recycleBin.addScrapView(getChildAt(i), firstPosition+i);將view都存在mCurrentScrap中(mViewTypeCount默認(rèn)為1,默認(rèn)存mCurrentScrap吗跋,對(duì)不同的mViewTypeCount有mScrapViews數(shù)組存儲(chǔ))

在addScrapView方法中侧戴,如果view正在執(zhí)行動(dòng)畫(huà)操作,設(shè)定setHasTransientView(true),將把view添加入mTransientStateViews中救鲤。在obtainView中會(huì)重新復(fù)用久窟。

  1. 第一次加載沒(méi)有數(shù)據(jù)所以忽略。

在選擇選擇加載view方案時(shí)

  1. 在第一次調(diào)用加載方法時(shí)本缠,因childCount == 0且不位于底部斥扛,走的fillFromTop方法。
  2. 第二次調(diào)用方法時(shí)丹锹,childCount大于0稀颁,根據(jù)點(diǎn)擊的item位置和顯示的第一個(gè)item位置來(lái)構(gòu)造加載view的位置。
  3. 最終都是循環(huán)調(diào)用makeAndAddView來(lái)加載單個(gè)view來(lái)顯示界面楣黍。復(fù)用view中匾灶,getActiveView優(yōu)先最高,其次是obtainView中的transientView租漂,最后是scrapView阶女。

3.滾動(dòng)加載view

onTouchEvent ——》 onTouchMove -》 scrollIfNeeded - 》 trackMotionScroll -》 fillGap -》 fillDown/fillUp -》 makeAndAddView - 》 obtainView -》 getScrapView

這里關(guān)注點(diǎn)主要在界面復(fù)用和加載上。

boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
     if (down) { //判斷是否向下滾動(dòng)
        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();
    }
    
    offsetChildrenTopAndBottom(incrementalDeltaY);
    
    if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
        fillGap(down);
    }
}
  • 如果是下劃哩治,頂部的item會(huì)先滑出屏幕秃踩,循環(huán)已加載的view判斷底部是否移出的屏幕,移出了就調(diào)用mRecycler.addScrapView加入緩存中业筏。
  • 同理憔杨,上劃屏幕,底部的item如果移出的屏幕也加入緩存蒜胖。
  • 將移出屏幕的view調(diào)用detachViewsFromParent移出消别,并清空SkippedScrap。
  • fillGap調(diào)用條件是台谢,有新的view移入屏幕寻狂,需要加載了。fillGap就是實(shí)現(xiàn)新view加載的方法对碌。而fillGap里根據(jù)滑動(dòng)方向分別調(diào)用了fillDownfillUp荆虱,這個(gè)我們?cè)诘诙蝜ayout已經(jīng)很熟悉了。
 View obtainView(int position, boolean[] isScrap) {
    ...
    final View scrapView = mRecycler.getScrapView(position);
    final View child = mAdapter.getView(position, scrapView, this);
    ...
 }

與第二次layout不同的是朽们,這是滾動(dòng)中加載view怀读,ActiveView為空,而ScrapView有緩存骑脱,所以調(diào)用Adapter.getView方法菜枷,ScrapView的緩存view作為convertView參數(shù)傳入進(jìn)去。

4. notifyDataSetChanged

public void notifyDataSetChanged() {
    mDataSetObservable.notifyChanged();
}

DataSetObservable.java
public void notifyChanged() {
    synchronized(mObservers) {
        for (int i = mObservers.size() - 1; i >= 0; i--) {
            mObservers.get(i).onChanged();
        }
    }
}

mDataSetObservable是在AbsListView中的AdapterDataSetObserver類(lèi)中實(shí)現(xiàn)叁丧,它繼承自AdapterView<ListAdapter>.AdapterDataSetObserver

 @Override
public void onChanged() {
    mDataChanged = true;
    mOldItemCount = mItemCount;
    mItemCount = getAdapter().getCount();

    // Detect the case where a cursor that was previously invalidated has
    // been repopulated with new data.
    if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
            && mOldItemCount == 0 && mItemCount > 0) {
        AdapterView.this.onRestoreInstanceState(mInstanceState);
        mInstanceState = null;
    } else {
        rememberSyncState();
    }
    checkFocus();
    requestLayout();
}

最后一行啤誊,調(diào)用requestLayout()岳瞭,onLayout要被調(diào)用,開(kāi)始重新布局蚊锹。

5.總結(jié)

  1. 作為AbsListView的內(nèi)部類(lèi)RecycleBin瞳筏,view重用主要靠它實(shí)現(xiàn)。
  2. mActiveViews在數(shù)據(jù)沒(méi)有改變而進(jìn)行重繪時(shí)保存(例如多次重繪牡昆;調(diào)用notifyDataSetChanged而數(shù)據(jù)沒(méi)有改變等)姚炕,不用多次調(diào)用adapter的getView;

調(diào)用RecycleBin的fillActiveViews將view緩存到mActiveViews丢烘。

  1. mScrapViews/mCurrentScrap來(lái)緩存已經(jīng)移除屏幕的view(例如listview滾動(dòng)柱宦;數(shù)據(jù)有改變時(shí)調(diào)用notifyDataSetChanged),作為adapter的getView中convertView的參數(shù)播瞳;

RecycleBin的addScrapView或直接mRecycler.addScrapView加入緩存

  1. mTransientStateViews/mTransientStateViewsById來(lái)緩存處于transient的view(例如動(dòng)畫(huà)未執(zhí)行結(jié)束掸刊,hasTransientState為true,設(shè)置了setHasTransientView(true)赢乓,而沒(méi)有設(shè)置false)忧侧。
  2. 三者的優(yōu)先級(jí)依次,mActiveViews在makeAndAddView中調(diào)用牌芋,后面兩個(gè)在obtainView方法中被調(diào)用苍柏。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市姜贡,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌棺棵,老刑警劉巖楼咳,帶你破解...
    沈念sama閱讀 219,589評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異烛恤,居然都是意外死亡母怜,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門(mén)缚柏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)苹熏,“玉大人,你說(shuō)我怎么就攤上這事币喧」煊颍” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,933評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵杀餐,是天一觀的道長(zhǎng)干发。 經(jīng)常有香客問(wèn)我,道長(zhǎng)史翘,這世上最難降的妖魔是什么枉长? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,976評(píng)論 1 295
  • 正文 為了忘掉前任冀续,我火速辦了婚禮,結(jié)果婚禮上必峰,老公的妹妹穿的比我還像新娘洪唐。我一直安慰自己,他們只是感情好吼蚁,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,999評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布凭需。 她就那樣靜靜地躺著,像睡著了一般桂敛。 火紅的嫁衣襯著肌膚如雪功炮。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,775評(píng)論 1 307
  • 那天术唬,我揣著相機(jī)與錄音薪伏,去河邊找鬼。 笑死粗仓,一個(gè)胖子當(dāng)著我的面吹牛嫁怀,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播借浊,決...
    沈念sama閱讀 40,474評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼塘淑,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了蚂斤?” 一聲冷哼從身側(cè)響起存捺,我...
    開(kāi)封第一講書(shū)人閱讀 39,359評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎曙蒸,沒(méi)想到半個(gè)月后捌治,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,854評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡纽窟,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,007評(píng)論 3 338
  • 正文 我和宋清朗相戀三年肖油,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片臂港。...
    茶點(diǎn)故事閱讀 40,146評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡森枪,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出审孽,到底是詐尸還是另有隱情县袱,我是刑警寧澤,帶...
    沈念sama閱讀 35,826評(píng)論 5 346
  • 正文 年R本政府宣布佑力,位于F島的核電站显拳,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏搓萧。R本人自食惡果不足惜杂数,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,484評(píng)論 3 331
  • 文/蒙蒙 一宛畦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧揍移,春花似錦次和、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,029評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至罕邀,卻和暖如春畅形,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背诉探。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,153評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工日熬, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人肾胯。 一個(gè)月前我還...
    沈念sama閱讀 48,420評(píng)論 3 373
  • 正文 我出身青樓竖席,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親敬肚。 傳聞我的和親對(duì)象是個(gè)殘疾皇子毕荐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,107評(píng)論 2 356

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