ListView 中的 RecycleBin 機制

發表於2016-07-20

在自定義Adapter時,我們常常會重寫Adapter的getView方法,該方法的簽名如下所示:

此處會傳入一個convertView變數,它的值有可能是null,也有可能不是null,如果不為null,我們就可以複用該convertView,對convertView裡面的一些控制元件賦值後可以將convertView作為getView的返回值返回,這麼做的目的是減少LayoutInflater.inflate()的呼叫次數,從而提升了效能(LayoutInflater.inflate()比較消耗效能)。

本文將介紹ListView中的RecycleBin機制,讓大家對ListView中的優化機制有個概括的瞭解,同時也說明convertView的來龍去脈。

首先,我們知道,Adapter是資料來源,AdapterView是展示資料來源的UI控制元件,Adapter是給AdapterView使用的,通過呼叫AdapterView的setAdapter方法就可以讓一個AdapterView繫結Adapter物件,從而AdapterView會將Adapter中的資料展示出來。

AdapterView的子類有AbsListView和AbsSpinner等,其中AbsListView的子類又有ListView、GridView等,所以ListView繼承自AdapterView。

如果Adapter中有10000條資料,將這個Adapter物件賦給ListView,如果ListView建立10000個子View,那麼App肯定崩潰了,因為Android沒有能力同時繪製這麼多的子View。而且,即便能同時繪製這10000個子View也沒什麼意義,因為手機的螢幕大小是有限的,有可能ListView的高度只能最多顯示10個子View。基於此,Android在設計ListView這個類的時候,引入了RecycleBin機制—–對子View進行回收利用,RecycleBin直譯過來就是回收站的意思。


RecycleBin基本原理

下面先簡要說一下RecycleBin中的工作原理,後面會結合原始碼詳細說明。

在某一時刻,我們看到ListView中有許多View呈現在UI上,這些View對我們來說是可見的,這些可見的View可以稱作OnScreen的View,即在螢幕中能看到的View,也可以叫做ActiveView,因為它們是在UI上可操作的。

當觸控ListView並向上滑動時,ListView上部的一些OnScreen的View位置上移,並移除了ListView的螢幕範圍,此時這些OnScreen的View就變得不可見了,不可見的View叫做OffScreen的View,即這些View已經不在螢幕可見範圍內了,也可以叫做ScrapView,Scrap表示廢棄的意思,ScrapView的意思是這些OffScreen的View不再處於可以互動的Active狀態了。ListView會把那些ScrapView(即OffScreen的View)刪除,這樣就不用繪製這些本來就不可見的View了,同時,ListView會把這些刪除的ScrapView放入到RecycleBin中存起來,就像把暫時無用的資源放到回收站一樣。

當ListView的底部需要顯示新的View的時候,會從RecycleBin中取出一個ScrapView,將其作為convertView引數傳遞給Adapter的getView方法,從而達到View複用的目的,這樣就不必在Adapter的getView方法中執行LayoutInflater.inflate()方法了。

RecycleBin中有兩個重要的View陣列,分別是mActiveViews和mScrapViews。這兩個陣列中所儲存的View都是用來複用的,只不過mActiveViews中儲存的是OnScreen的View,這些View很有可能被直接複用;而mScrapViews中儲存的是OffScreen的View,這些View主要是用來間接複用的。

上面對mActiveViews和mScrapViews的說明比較籠統,其實在細節上還牽扯到Adapter的資料來源發生變化的情況,具體細節後面會講解。


原始碼解析

AdapterView是繼承自ViewGroup的,ViewGroup中有addView方法可以向ViewGroup中新增子View,但是AdapterView重寫了addView方法,如下所示:

在AdapterView的addView方法中會丟擲異常,也就是說AdapterView禁用了addView方法。

在具體講解之前,我們還是先花一點時間簡要說一下View的每一幀的顯示流程,當然,ListView也肯定遵循此流程。一個View要想在介面上呈現出來,需要經過三個階段:measure->layout->draw。

View是一幀一幀繪製的,每一幀繪製都經歷了measure->layout->draw這三個階段,繪製完一幀之後,如果UI需要更新,比如使用者滾動了ListView,那麼又會繪製下一幀,再次經歷measure->layout->draw方法,如果對此不瞭解,可以參見另一篇博文《 Android中View的量算、佈局及繪圖機制》

我們上面說了,AdapterView把addView方法給禁用了,那麼ListView怎麼向其中新增child呢?奧祕就在layout中,在佈局的時候,ListView會執行layoutChildren方法,該方法是ListView對View進行新增以及回收的關鍵方法,RecycleBin的很多方法都在layoutChildren方法中被呼叫。在layoutChildren方法中實現對子View的增刪,經過layoutChildren方法之後,ListView中所有的子View都是在螢幕中可見的,也就是說layoutChildren方法為接下來的幀繪製把子View準備完善了,這就保證了在後面的draw方法的執行過程中能夠正確繪製ListView。

ListView的layoutChildren方法程式碼比較多,我們只研究和View增刪相關的關鍵程式碼,主要分以下三個階段:

  1. ListView的children->RecycleBin
  2. ListView清空children
  3. RecycleBin->ListView的children

在layout這個方法剛剛開始執行的時候,ListView中的children其實還是上一幀中需要繪製的子View的集合,在layout這個方法執行完成的時候,ListView中的children就變成了當前幀馬上要進行繪製的子View的集合。

下面對以上這三個階段分別說明。

  1. ListView的children->RecycleBin
    該階段的關鍵程式碼如下所示:

  1. 再次強調一下,在上面的程式碼剛開始的時候,ListView的中的children還是上一幀需要繪製的子View。
    • 如果Adapter呼叫了notifyDataSetChanged方法,那麼AdapterView就會知道Adapter的資料來源發生了變化,此時dataChanged變數就為true,這種情況下,ListView會認為children中的View都是不合格的了,這時候會用getChildAt方法遍歷children中所有的child,並把這些child通過RecycleBin的addScrapView方法將其放入RecycleBin的mScrapViews陣列中。
    • 如果adapter的資料沒有發生變化,那麼會呼叫RecycleBin的fillActiveViews方法將所有的children都放入到RecycleBin的mActiveViews陣列中。

    經過上面的操作之後,ListView所有的子View都放入到了RecycleBin中,這就實現了ListView的children->RecycleBin的遷移過程,放到RecycleBin的目的是為了分類快取ListView中的children,以便在後續過程中對這些View進行復用。

  2. ListView清空children
    然後呼叫ViewGroup的detachAllViewsFromParent方法,該方法將所有的子View從ListView中分離,也就是清空了children,該方法原始碼如下所示:

3. RecycleBin->ListView的children

然後ListView會根據mLayoutMode進行判斷,原始碼如下所示:

在該switch程式碼段中,會根據不同情況增刪子View,這些方法的程式碼邏輯大部分最終呼叫了fillDown、fillUp等方法。
fillDown用子View從指定的position自上而下填充ListView,fillUp則是自下而上填充,我們以fillDown方法為例詳細說明。
fillDown方法的原始碼如下所示:

    • fillDown接收兩個引數,pos表示列表中第一個要繪製的item的position,其對應著Adapter中的索引,nextTop表示第一個要繪製的item在ListView中實際的位置, 即該item所對應的子View的頂部到ListView的頂部的畫素數。
    • 首先將mBottom – mTop的值作為end,end表示ListView的高度。
    • 然後在while迴圈中新增子View,我們先不看while迴圈的具體條件,先看一下迴圈體。在迴圈體中,將pos和nextTop傳遞給makeAndAddView方法,該方法返回一個View作為child,該方法會建立View,並把該View作為child新增到ListView的children陣列中。
    • 然後執行nextTop = child.getBottom() + mDividerHeight,child的bottom值表示的是該child的底部到ListView頂部的距離,將該child的bottom作為下一個child的top,也就是說nextTop一直儲存著下一個child的top值。
    • 最後呼叫pos++實現position指標下移。現在我們回過頭來看一下while迴圈的條件while (nextTop < end && pos < mItemCount)。
    • nextTop < end確保了我們只要將新增的子View能夠覆蓋ListView的介面就可以了,比如ListView的高度最多顯示10個子View,我們沒必要向ListView中加入11個子View。
    • pos < mItemCount確保了我們新增的子View在Adapter中都有對應的資料來源item,比如ListView的高度最多顯示10個子View,但是我們Adapter中一共才有5條資料,這種情況下只能向ListView中加入5個子View,從而不能填充滿ListView的全部高度。

經過了上面的while迴圈之後,ListView對子View的增刪就完成了,即children中存放的就是要在後面繪圖過程中即將渲染的子View的集合。

上面while迴圈的方法體中呼叫了makeAndAddView方法,通過該方法會獲得一個子View,並把該子View新增到ListView的children中。該方法的方法簽名如下所示:

其原始碼如下所示:

我們重點說一下前兩個引數position和y,position表示的是資料來源item在Adapter中的索引,y表示要生成的View的top值或bottom值。如果第三個引數flow是true,那麼y表示top值,否則表示bottom值。

  • 如果資料來源沒發生變化,那麼嘗試用該position從RecycleBin的mActiveViews中獲取可複用的View。RecycleBin的getActiveView方法接收一個position引數,可以在RecycleBin的mActiveViews陣列中查詢有沒有對應position的View,如果能找到就可以直接複用該View作為child了。舉一個例子,假設在某一時刻ListView中顯示了10個子View,position依次為從0到9。然後我們手指向上滑動,且向上滑動了一個子View的高度,ListView需要繪製下一幀。這時候ListView在layoutChildren方法中把這10個子View都放入到了RecycleBin的mActiveViews陣列中了,然後清空了children陣列,然後呼叫fillDown方法,向ListView中依次新增position1到10的子View,在新增position為1的子View的時候,由於在上一幀中position為1的子View已經被放到mActiveViews陣列中了,這次直接可以將其從mActiveViews陣列中取出來,這樣就是直接複用子View,所以說RecycleBin的mActiveViews陣列主要是用於直接複用的。在直接複用了子View後,我們需要呼叫setupChild方法,該方法會將child新增到ListView的children陣列中,並對child進行定位。
  • 如果沒能夠從mActivieViews中直接複用View,那麼就要呼叫obtainView方法獲取View,該方法嘗試間接複用RecycleBin中的mScrapViews中的View,如果不能間接複用,則建立新的View。在通過obtainView獲取了View之後,呼叫setupChild方法,該方法會將child新增到ListView的children陣列中,並對child進行定位和量算。

下面我們再來看一下obtainView方法,該方法的方法簽名如下所示:

該方法接收position引數,其關鍵的原始碼有以下兩行:

通過呼叫RecycleBin的getScrapView方法,從mScrapViews陣列中獲取一個View,該View是用來間接複用的,該View可能為null,也可能不為null,將其作為我們熟悉的convertView傳遞給Adapter的getView方法,這樣我們就可以在AdapterView的getView方法中通過判斷convertView是否為空進行間接複用了。

希望本文對大家理解ListView的RecycleBin機制有所幫助!

相關文章