Android ListView 源碼淺析

OverView

Android ListView 是Android中常用的長(zhǎng)列表組件扣猫,其繼承層次如下:


image.png

用法

通常在業(yè)務(wù)代碼中使用ListView的常用姿勢(shì)是:

  • 創(chuàng)建1個(gè)ListView
  • 創(chuàng)建1個(gè)BaseAdapter的子類(lèi)养叛,實(shí)現(xiàn)getCount/getItem/getItemId/getView這4個(gè)方法洲押,有時(shí)候還會(huì)實(shí)現(xiàn)getItemViewType/getViewTypeCount方法來(lái)滿(mǎn)足有多種ItemView樣式的需求
  • 將BaseAdatper的子類(lèi)實(shí)例通過(guò)ListView的setAdapter()方法,設(shè)置給ListView實(shí)例

常用優(yōu)化

通常的ListView在View的復(fù)用上有2種優(yōu)化:

  • public View getView(int position, View convertView, ViewGroup parent)這個(gè)方法在實(shí)現(xiàn)是,首先判斷一下傳入的convertView是否為null,不為null即可復(fù)用断楷,無(wú)需調(diào)用inflate或者new來(lái)新創(chuàng)建1個(gè)View
  • 可以通過(guò)ViewHoloder的方法,將convertView的子View直接存一個(gè)引用在ViewHolder中崭别,然后將ViewHolder通過(guò)convertView的setTag方法存儲(chǔ)在convertView上冬筒;這種做法的好處在于,通過(guò)對(duì)子View的直接引用訪問(wèn)茅主,避免了findViewById的耗時(shí)操作

源碼淺析

ListView的源碼比較長(zhǎng)舞痰,暫時(shí)先把精力放在理解Adapter的6個(gè)方法(getViewTypeCount/getItemViewType/getCount/getItem/getItemId/getView)被調(diào)用的時(shí)機(jī)上,更詳細(xì)的源碼分析文章已經(jīng)很多了诀姚,比較經(jīng)典的有郭霖前輩的https://blog.csdn.net/sinyu890807/article/details/44996879

int getViewTypeCount()

在源碼中搜索getViewTypeCount()被引用的位置响牛,得到的和ListView相關(guān)的結(jié)果是在setAdapter(ListAdapter adapter)方法中有1行:

mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());

這個(gè)mRecycler成員是RecycleBin類(lèi)型,RecycleBin的定義在AbsListView中赫段,其作用顧名思義呀打,就是起到1個(gè)回收的垃圾箱作用,其setViewTypeCount(int viewTypeCount)方法的實(shí)現(xiàn)為:

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

RecycleBin回收廢棄View的實(shí)現(xiàn)是通過(guò)其scrapViews數(shù)組實(shí)現(xiàn)的糯笙,而傳入的viewTypeCount決定了這個(gè)數(shù)組的長(zhǎng)度贬丛,注意scrapViews的每一個(gè)成員是一個(gè)ArrayList<View>;在這里我的理解是给涕,viewTypeCount決定了有多少種View會(huì)被回收豺憔,而每1個(gè)被回收的View會(huì)根據(jù)viewType進(jìn)入到對(duì)應(yīng)的ArrayList<View>中去额获,方便在復(fù)用時(shí)從正確的類(lèi)型中取出對(duì)應(yīng)的View來(lái)進(jìn)行復(fù)用。

int getItemViewType(int position)

在源碼中搜索該函數(shù)焕阿,有好幾處調(diào)用的地方咪啡,但多數(shù)調(diào)用都是得到viewType之后設(shè)置到AbsListView.LayoutParams的viewType屬性上使用首启,這種使用并不是非常重要暮屡,真正關(guān)鍵的調(diào)用在AbsListView的getScrapView(position)函數(shù)中:

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

getScrapView(int position)函數(shù)的作用在于,從廢棄的View中獲取一個(gè)View毅桃,準(zhǔn)備復(fù)用褒纲,從實(shí)現(xiàn)上可以看出,getItemViewType的作用在于钥飞,得到正確的ViewType莺掠,從而從對(duì)應(yīng)的mScrapViews數(shù)組中取出1個(gè)ScrapView

int getCount()

getCount()的調(diào)用在源碼中的搜索結(jié)果就實(shí)在是太多了,粗略瀏覽的一下读宙,把覺(jué)得比較關(guān)鍵的點(diǎn)記錄下來(lái)

  • 首先是ListView的setAdatper()函數(shù)中有這么一句:
 mItemCount = mAdapter.getCount();

這個(gè)mItemCount是ListView的祖先類(lèi)AdapterView的成員彻秆,設(shè)置到這上面之后就能更加方便地使用了

  • 然后是ListView的layoutChildren()函數(shù)中有這么一段:
            if (mItemCount == 0) {
                resetList();
                invokeOnItemScrollListener();
                return;
            } else if (mItemCount != mAdapter.getCount()) {
                throw new IllegalStateException("The content of the adapter has changed but "
                        + "ListView did not receive a notification. Make sure the content of "
                        + "your adapter is not modified from a background thread, but only from "
                        + "the UI thread. Make sure your adapter calls notifyDataSetChanged() "
                        + "when its content changes. [in ListView(" + getId() + ", " + getClass()
                        + ") with Adapter(" + mAdapter.getClass() + ")]");
            }

在layoutChildren()過(guò)程中需要檢查mItemCount和mAdapter.getCount()是否一致,如果不一致结闸,證明數(shù)據(jù)源被改變了卻沒(méi)有調(diào)用notifyDataSetChanged()通知觀察方

Object getItem(int position)

getItem(int position)方法主要被調(diào)用的地方在AdapterView的getItemAtPosition(int position)函數(shù)中:

    public Object getItemAtPosition(int position) {
        T adapter = getAdapter();
        return (adapter == null || position < 0) ? null : adapter.getItem(position);
    }

除此之外唇兑,在源碼中再?zèng)]找到getItem(int position)的相關(guān)調(diào)用,這也可以理解桦锄,因?yàn)間etItem(int position)更主要的使用場(chǎng)景是我們?cè)跇I(yè)務(wù)代碼中調(diào)用扎附,通過(guò)該方法,能夠從Adapter里拿出數(shù)據(jù)項(xiàng)结耀,而不需要直接跟數(shù)據(jù)源接觸留夜。

long getItemId(int position)

getItemId(int postion)函數(shù)在源碼中搜索,ListView中的調(diào)用已經(jīng)被標(biāo)注為@Deprecate图甜,其余主要的調(diào)用都在AbsListView中碍粥,選擇其中一處來(lái)看下這個(gè)方法的作用

   public void setItemChecked(int position, boolean value) {
        if (mChoiceMode == CHOICE_MODE_NONE) {
            return;
        }

        // Start selection mode if needed. We don't need to if we're unchecking something.
        if (value && mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode == null) {
            if (mMultiChoiceModeCallback == null ||
                    !mMultiChoiceModeCallback.hasWrappedCallback()) {
                throw new IllegalStateException("AbsListView: attempted to start selection mode " +
                        "for CHOICE_MODE_MULTIPLE_MODAL but no choice mode callback was " +
                        "supplied. Call setMultiChoiceModeListener to set a callback.");
            }
            mChoiceActionMode = startActionMode(mMultiChoiceModeCallback);
        }

        final boolean itemCheckChanged;
        if (mChoiceMode == CHOICE_MODE_MULTIPLE || mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) {
            boolean oldValue = mCheckStates.get(position);
            mCheckStates.put(position, value);
            if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
                if (value) {
                    mCheckedIdStates.put(mAdapter.getItemId(position), position);
                } else {
                    mCheckedIdStates.delete(mAdapter.getItemId(position));
                }
            }
            itemCheckChanged = oldValue != value;
            if (itemCheckChanged) {
                if (value) {
                    mCheckedItemCount++;
                } else {
                    mCheckedItemCount--;
                }
            }
            if (mChoiceActionMode != null) {
                final long id = mAdapter.getItemId(position);
                mMultiChoiceModeCallback.onItemCheckedStateChanged(mChoiceActionMode,
                        position, id, value);
            }
        } else {
           ……
            }
            // this may end up selecting the value we just cleared but this way
            // we ensure length of mCheckStates is 1, a fact getCheckedItemPosition relies on
            if (value) {
                mCheckStates.put(position, true);
                if (updateIds) {
                    mCheckedIdStates.put(mAdapter.getItemId(position), position);
                }
                mCheckedItemCount = 1;
            } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) {
                mCheckedItemCount = 0;
            }
        }

        // Do not generate a data change while we are in the layout phase or data has not changed
        if (!mInLayout && !mBlockLayoutRequests && itemCheckChanged) {
            mDataChanged = true;
            rememberSyncState();
            requestLayout();
        }
    }

可以看到,主要是通過(guò)該方法黑毅,獲得對(duì)應(yīng)位置的id后嚼摩,能夠作為一個(gè)索引,用于增刪改查等快速操作博肋。

View getView(int position, View convertView, ViewGroup parent)

最后Adapter中最重要的方法低斋,getView方法的作用是返回某一項(xiàng)對(duì)應(yīng)的ItemView,在源碼中關(guān)鍵的調(diào)用是AbsListView中的obtainView()方法中的調(diào)用:

        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 if (child.isTemporarilyDetached()) {
                outMetadata[0] = true;

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

AbsListView的obtainView函數(shù)的作用就是構(gòu)建出某一position對(duì)應(yīng)的View匪凡,首先會(huì)從mRecycler中取出一個(gè)對(duì)應(yīng)的廢棄View膊畴,這個(gè)廢棄View就是傳入Adapter的getView()方法中的convertView,這里也就解釋了為什么需要判空——在首次布局時(shí)病游,實(shí)際上是還沒(méi)有廢棄的View可用的唇跨,而后面布局時(shí)就有廢棄的View可復(fù)用稠通,無(wú)需重新構(gòu)建了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末买猖,一起剝皮案震驚了整個(gè)濱河市改橘,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌玉控,老刑警劉巖飞主,帶你破解...
    沈念sama閱讀 212,454評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異高诺,居然都是意外死亡劲适,警方通過(guò)查閱死者的電腦和手機(jī)是整,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)驼卖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)葱椭,“玉大人,你說(shuō)我怎么就攤上這事牡拇】桑” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,921評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵惠呼,是天一觀的道長(zhǎng)导俘。 經(jīng)常有香客問(wèn)我,道長(zhǎng)罢杉,這世上最難降的妖魔是什么趟畏? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,648評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮滩租,結(jié)果婚禮上赋秀,老公的妹妹穿的比我還像新娘。我一直安慰自己律想,他們只是感情好猎莲,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,770評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著技即,像睡著了一般著洼。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上而叼,一...
    開(kāi)封第一講書(shū)人閱讀 49,950評(píng)論 1 291
  • 那天身笤,我揣著相機(jī)與錄音,去河邊找鬼葵陵。 笑死液荸,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的脱篙。 我是一名探鬼主播娇钱,決...
    沈念sama閱讀 39,090評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼伤柄,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了文搂?” 一聲冷哼從身側(cè)響起适刀,我...
    開(kāi)封第一講書(shū)人閱讀 37,817評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎煤蹭,沒(méi)想到半個(gè)月后笔喉,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,275評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡疯兼,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,592評(píng)論 2 327
  • 正文 我和宋清朗相戀三年然遏,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吧彪。...
    茶點(diǎn)故事閱讀 38,724評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖丢早,靈堂內(nèi)的尸體忽然破棺而出姨裸,到底是詐尸還是另有隱情,我是刑警寧澤怨酝,帶...
    沈念sama閱讀 34,409評(píng)論 4 333
  • 正文 年R本政府宣布傀缩,位于F島的核電站,受9級(jí)特大地震影響农猬,放射性物質(zhì)發(fā)生泄漏赡艰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,052評(píng)論 3 316
  • 文/蒙蒙 一斤葱、第九天 我趴在偏房一處隱蔽的房頂上張望慷垮。 院中可真熱鬧,春花似錦揍堕、人聲如沸料身。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,815評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)芹血。三九已至,卻和暖如春楞慈,著一層夾襖步出監(jiān)牢的瞬間幔烛,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,043評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工囊蓝, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留饿悬,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,503評(píng)論 2 361
  • 正文 我出身青樓慎颗,卻偏偏與公主長(zhǎng)得像乡恕,于是被迫代替她去往敵國(guó)和親言询。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,627評(píng)論 2 350