Android ListView工作原理完全解析,帶你從原始碼的角度徹底理解

發表於2015-10-17

在Android所有常用的原生控制元件當中,用法最複雜的應該就是ListView了,它專門用於處理那種內容元素很多,手機螢幕無法展示出所有內容的情況。ListView可以使用列表的形式來展示內容,超出螢幕部分的內容只需要通過手指滑動就可以移動到螢幕內了。

另外ListView還有一個非常神奇的功能,我相信大家應該都體驗過,即使在ListView中載入非常非常多的資料,比如達到成百上千條甚至更多,ListView都不會發生OOM或者崩潰,而且隨著我們手指滑動來瀏覽更多資料時,程式所佔用的記憶體竟然都不會跟著增長。那麼ListView是怎麼實現這麼神奇的功能的呢?當初我就抱著學習的心態花了很長時間把ListView的原始碼通讀了一遍,基本瞭解了它的工作原理,在感嘆Google大神能夠寫出如此精妙程式碼的同時我也有所敬畏,因為ListView的程式碼量比較大,複雜度也很高,很難用文字表達清楚,於是我就放棄了把它寫成一篇部落格的想法。那麼現在回想起來這件事我已經腸子都悔青了,因為沒過幾個月時間我就把當初梳理清晰的原始碼又忘的一乾二淨。於是現在我又重新定下心來再次把ListView的原始碼重讀了一遍,那麼這次我一定要把它寫成一篇部落格,分享給大家的同時也當成我自己的筆記吧。

首先我們先來看一下ListView的繼承結構,如下圖所示:

20150704111744498

可以看到,ListView的繼承結構還是相當複雜的,它是直接繼承自的AbsListView,而AbsListView有兩個子實現類,一個是ListView,另一個就是GridView,因此我們從這一點就可以猜出來,ListView和GridView在工作原理和實現上都是有很多共同點的。然後AbsListView又繼承自AdapterView,AdapterView繼承自ViewGroup,後面就是我們所熟知的了。先把ListView的繼承結構瞭解一下,待會兒有助於我們更加清晰地分析程式碼。

 

Adapter的作用

 

Adapter相信大家都不會陌生,我們平時使用ListView的時候一定都會用到它。那麼話說回來大家有沒有仔細想過,為什麼需要Adapter這個東西呢?總感覺正因為有了Adapter,ListView的使用變得要比其它控制元件複雜得多。那麼這裡我們就先來學習一下Adapter到底起到了什麼樣的一個作用。

其實說到底,控制元件就是為了互動和展示資料用的,只不過ListView更加特殊,它是為了展示很多很多資料用的,但是ListView只承擔互動和展示工作而已,至於這些資料來自哪裡,ListView是不關心的。因此,我們能設想到的最基本的ListView工作模式就是要有一個ListView控制元件和一個資料來源。

不過如果真的讓ListView和資料來源直接打交道的話,那ListView所要做的適配工作就非常繁雜了。因為資料來源這個概念太模糊了,我們只知道它包含了很多資料而已,至於這個資料來源到底是什麼樣型別,並沒有嚴格的定義,有可能是陣列,也有可能是集合,甚至有可能是資料庫表中查詢出來的遊標。所以說如果ListView真的去為每一種資料來源都進行適配操作的話,一是擴充套件性會比較差,內建了幾種適配就只有幾種適配,不能動態進行新增。二是超出了它本身應該負責的工作範圍,不再是僅僅承擔互動和展示工作就可以了,這樣ListView就會變得比較臃腫。

那麼顯然Android開發團隊是不會允許這種事情發生的,於是就有了Adapter這樣一個機制的出現。顧名思義,Adapter是介面卡的意思,它在ListView和資料來源之間起到了一個橋樑的作用,ListView並不會直接和資料來源打交道,而是會藉助Adapter這個橋樑來去訪問真正的資料來源,與之前不同的是,Adapter的介面都是統一的,因此ListView不用再去擔心任何適配方面的問題。而Adapter又是一個介面(interface),它可以去實現各種各樣的子類,每個子類都能通過自己的邏輯來去完成特定的功能,以及與特定資料來源的適配操作,比如說ArrayAdapter可以用於陣列和List型別的資料來源適配,SimpleCursorAdapter可以用於遊標型別的資料來源適配,這樣就非常巧妙地把資料來源適配困難的問題解決掉了,並且還擁有相當不錯的擴充套件性。簡單的原理示意圖如下所示:

20150719202924532

當然Adapter的作用不僅僅只有資料來源適配這一點,還有一個非常非常重要的方法也需要我們在Adapter當中去重寫,就是getView()方法,這個在下面的文章中還會詳細講到。

RecycleBin機制

那麼在開始分析ListView的原始碼之前,還有一個東西是我們提前需要了解的,就是RecycleBin機制,這個機制也是ListView能夠實現成百上千條資料都不會OOM最重要的一個原因。其實RecycleBin的程式碼並不多,只有300行左右,它是寫在AbsListView中的一個內部類,所以所有繼承自AbsListView的子類,也就是ListView和GridView,都可以使用這個機制。那我們來看一下RecycleBin中的主要程式碼,如下所示:

這裡的RecycleBin程式碼並不全,我只是把最主要的幾個方法提了出來。那麼我們先來對這幾個方法進行簡單解讀,這對後面分析ListView的工作原理將會有很大的幫助。

  • fillActiveViews() 這個方法接收兩個引數,第一個參數列示要儲存的view的數量,第二個參數列示ListView中第一個可見元素的position值。RecycleBin當中使用mActiveViews這個陣列來儲存View,呼叫這個方法後就會根據傳入的引數來將ListView中的指定元素儲存到mActiveViews陣列當中。
  • getActiveView() 這個方法和fillActiveViews()是對應的,用於從mActiveViews陣列當中獲取資料。該方法接收一個position引數,表示元素在ListView當中的位置,方法內部會自動將position值轉換成mActiveViews陣列對應的下標值。需要注意的是,mActiveViews當中所儲存的View,一旦被獲取了之後就會從mActiveViews當中移除,下次獲取同樣位置的View將會返回null,也就是說mActiveViews不能被重複利用。
  • addScrapView() 用於將一個廢棄的View進行快取,該方法接收一個View引數,當有某個View確定要廢棄掉的時候(比如滾動出了螢幕),就應該呼叫這個方法來對View進行快取,RecycleBin當中使用mScrapViews和mCurrentScrap這兩個List來儲存廢棄View。
  • getScrapView 用於從廢棄快取中取出一個View,這些廢棄快取中的View是沒有順序可言的,因此getScrapView()方法中的演算法也非常簡單,就是直接從mCurrentScrap當中獲取尾部的一個scrap view進行返回。
  • setViewTypeCount() 我們都知道Adapter當中可以重寫一個getViewTypeCount()來表示ListView中有幾種型別的資料項,而setViewTypeCount()方法的作用就是為每種型別的資料項都單獨啟用一個RecycleBin快取機制。實際上,getViewTypeCount()方法通常情況下使用的並不是很多,所以我們只要知道RecycleBin當中有這樣一個功能就行了。

瞭解了RecycleBin中的主要方法以及它們的用處之後,下面就可以開始來分析ListView的工作原理了,這裡我將還是按照以前分析原始碼的方式來進行,即跟著主線執行流程來逐步閱讀並點到即止,不然的話要是把ListView所有的程式碼都貼出來,那麼本篇文章將會很長很長了。

第一次Layout

不管怎麼說,ListView即使再特殊最終還是繼承自View的,因此它的執行流程還將會按照View的規則來執行,對於這方面不太熟悉的朋友可以參考我之前寫的 Android檢視繪製流程完全解析,帶你一步步深入瞭解View(二) 。

View的執行流程無非就分為三步,onMeasure()用於測量View的大小,onLayout()用於確定View的佈局,onDraw()用於將View繪製到介面上。而在ListView當中,onMeasure()並沒有什麼特殊的地方,因為它終歸是一個View,佔用的空間最多並且通常也就是整個螢幕。onDraw()在ListView當中也沒有什麼意義,因為ListView本身並不負責繪製,而是由ListView當中的子元素來進行繪製的。那麼ListView大部分的神奇功能其實都是在onLayout()方法中進行的了,因此我們本篇文章也是主要分析的這個方法裡的內容。

如果你到ListView原始碼中去找一找,你會發現ListView中是沒有onLayout()這個方法的,這是因為這個方法是在ListView的父類AbsListView中實現的,程式碼如下所示:

可以看到,onLayout()方法中並沒有做什麼複雜的邏輯操作,主要就是一個判斷,如果ListView的大小或者位置發生了變化,那麼changed變數就會變成true,此時會要求所有的子佈局都強制進行重繪。除此之外倒沒有什麼難理解的地方了,不過我們注意到,在第16行呼叫了layoutChildren()這個方法,從方法名上我們就可以猜出這個方法是用來進行子元素佈局的,不過進入到這個方法當中你會發現這是個空方法,沒有一行程式碼。這當然是可以理解的了,因為子元素的佈局應該是由具體的實現類來負責完成的,而不是由父類完成。那麼進入ListView的layoutChildren()方法,程式碼如下所示:

這段程式碼比較長,我們挑重點的看。首先可以確定的是,ListView當中目前還沒有任何子View,資料都還是由Adapter管理的,並沒有展示到介面上,因此第19行getChildCount()方法得到的值肯定是0。接著在第81行會根據dataChanged這個布林型的值來判斷執行邏輯,dataChanged只有在資料來源發生改變的情況下才會變成true,其它情況都是false,因此這裡會進入到第90行的執行邏輯,呼叫RecycleBin的fillActiveViews()方法。按理來說,呼叫fillActiveViews()方法是為了將ListView的子View進行快取的,可是目前ListView中還沒有任何的子View,因此這一行暫時還起不了任何作用。

接下來在第114行會根據mLayoutMode的值來決定佈局模式,預設情況下都是普通模式LAYOUT_NORMAL,因此會進入到第140行的default語句當中。而下面又會緊接著進行兩次if判斷,childCount目前是等於0的,並且預設的佈局順序是從上往下,因此會進入到第145行的fillFromTop()方法,我們跟進去瞧一瞧:


從這個方法的註釋中可以看出,它所負責的主要任務就是從mFirstPosition開始,自頂至底去填充ListView。而這個方法本身並沒有什麼邏輯,就是判斷了一下mFirstPosition值的合法性,然後呼叫fillDown()方法,那麼我們就有理由可以猜測,填充ListView的操作是在fillDown()方法中完成的。進入fillDown()方法,程式碼如下所示:

可以看到,這裡使用了一個while迴圈來執行重複邏輯,一開始nextTop的值是第一個子元素頂部距離整個ListView頂部的畫素值,pos則是剛剛傳入的mFirstPosition的值,而end是ListView底部減去頂部所得的畫素值,mItemCount則是Adapter中的元素數量。因此一開始的情況下nextTop必定是小於end值的,並且pos也是小於mItemCount值的。那麼每執行一次while迴圈,pos的值都會加1,並且nextTop也會增加,當nextTop大於等於end時,也就是子元素已經超出當前螢幕了,或者pos大於等於mItemCount時,也就是所有Adapter中的元素都被遍歷結束了,就會跳出while迴圈。

那麼while迴圈當中又做了什麼事情呢?值得讓人留意的就是第18行呼叫的makeAndAddView()方法,進入到這個方法當中,程式碼如下所示:

這裡在第19行嘗試從RecycleBin當中快速獲取一個active view,不過很遺憾的是目前RecycleBin當中還沒有快取任何的View,所以這裡得到的值肯定是null。那麼取得了null之後就會繼續向下執行,到第28行會呼叫obtainView()方法來再次嘗試獲取一個View,這次的obtainView()方法是可以保證一定返回一個View的,於是下面立刻將獲取到的View傳入到了setupChild()方法當中。那麼obtainView()內部到底是怎麼工作的呢?我們先進入到這個方法裡面看一下:


obtainView()方法中的程式碼並不多,但卻包含了非常非常重要的邏輯,不誇張的說,整個ListView中最重要的內容可能就在這個方法裡了。那麼我們還是按照執行流程來看,在第19行程式碼中呼叫了RecycleBin的getScrapView()方法來嘗試獲取一個廢棄快取中的View,同樣的道理,這裡肯定是獲取不到的,getScrapView()方法會返回一個null。這時該怎麼辦呢?沒有關係,程式碼會執行到第33行,呼叫mAdapter的getView()方法來去獲取一個View。那麼mAdapter是什麼呢?當然就是當前ListView關聯的介面卡了。而getView()方法又是什麼呢?還用說嗎,這個就是我們平時使用ListView時最最經常重寫的一個方法了,這裡getView()方法中傳入了三個引數,分別是position,null和this。

那麼我們平時寫ListView的Adapter時,getView()方法通常會怎麼寫呢?這裡我舉個簡單的例子:

getView()方法接受的三個引數,第一個引數position代表當前子元素的的位置,我們可以通過具體的位置來獲取與其相關的資料。第二個引數convertView,剛才傳入的是null,說明沒有convertView可以利用,因此我們會呼叫LayoutInflater的inflate()方法來去載入一個佈局。接下來會對這個view進行一些屬性和值的設定,最後將view返回。

那麼這個View也會作為obtainView()的結果進行返回,並最終傳入到setupChild()方法當中。其實也就是說,第一次layout過程當中,所有的子View都是呼叫LayoutInflater的inflate()方法載入出來的,這樣就會相對比較耗時,但是不用擔心,後面就不會再有這種情況了,那麼我們繼續往下看:

setupChild()方法當中的程式碼雖然比較多,但是我們只看核心程式碼的話就非常簡單了,剛才呼叫obtainView()方法獲取到的子元素View,這裡在第40行呼叫了addViewInLayout()方法將它新增到了ListView當中。那麼根據fillDown()方法中的while迴圈,會讓子元素View將整個ListView控制元件填滿然後就跳出,也就是說即使我們的Adapter中有一千條資料,ListView也只會載入第一屏的資料,剩下的資料反正目前在螢幕上也看不到,所以不會去做多餘的載入工作,這樣就可以保證ListView中的內容能夠迅速展示到螢幕上。

那麼到此為止,第一次Layout過程結束。

第二次Layout

 

雖然我在原始碼中並沒有找出具體的原因,但如果你自己做一下實驗的話就會發現,即使是一個再簡單的View,在展示到介面上之前都會經歷至少兩次onMeasure()和兩次onLayout()的過程。其實這只是一個很小的細節,平時對我們影響並不大,因為不管是onMeasure()或者onLayout()幾次,反正都是執行的相同的邏輯,我們並不需要進行過多關心。但是在ListView中情況就不一樣了,因為這就意味著layoutChildren()過程會執行兩次,而這個過程當中涉及到向ListView中新增子元素,如果相同的邏輯執行兩遍的話,那麼ListView中就會存在一份重複的資料了。因此ListView在layoutChildren()過程當中做了第二次Layout的邏輯處理,非常巧妙地解決了這個問題,下面我們就來分析一下第二次Layout的過程。

其實第二次Layout和第一次Layout的基本流程是差不多的,那麼我們還是從layoutChildren()方法開始看起:

同樣還是在第19行,呼叫getChildCount()方法來獲取子View的數量,只不過現在得到的值不會再是0了,而是ListView中一屏可以顯示的子View數量,因為我們剛剛在第一次Layout過程當中向ListView新增了這麼多的子View。下面在第90行呼叫了RecycleBin的fillActiveViews()方法,這次效果可就不一樣了,因為目前ListView中已經有子View了,這樣所有的子View都會被快取到RecycleBin的mActiveViews陣列當中,後面將會用到它們。

接下來將會是非常非常重要的一個操作,在第113行呼叫了detachAllViewsFromParent()方法。這個方法會將所有ListView當中的子View全部清除掉,從而保證第二次Layout過程不會產生一份重複的資料。那有的朋友可能會問了,這樣把已經載入好的View又清除掉,待會還要再重新載入一遍,這不是嚴重影響效率嗎?不用擔心,還記得我們剛剛呼叫了RecycleBin的fillActiveViews()方法來快取子View嗎,待會兒將會直接使用這些快取好的View來進行載入,而並不會重新執行一遍inflate過程,因此效率方面並不會有什麼明顯的影響。

那麼我們接著看,在第141行的判斷邏輯當中,由於不再等於0了,因此會進入到else語句當中。而else語句中又有三個邏輯判斷,第一個邏輯判斷不成立,因為預設情況下我們沒有選中任何子元素,mSelectedPosition應該等於-1。第二個邏輯判斷通常是成立的,因為mFirstPosition的值一開始是等於0的,只要adapter中的資料大於0條件就成立。那麼進入到fillSpecific()方法當中,程式碼如下所示:


fillSpecific()這算是一個新方法了,不過其實它和fillUp()、fillDown()方法功能也是差不多的,主要的區別在於,fillSpecific()方法會優先將指定位置的子View先載入到螢幕上,然後再載入該子View往上以及往下的其它子View。那麼由於這裡我們傳入的position就是第一個子View的位置,於是fillSpecific()方法的作用就基本上和fillDown()方法是差不多的了,這裡我們就不去關注太多它的細節,而是將精力放在makeAndAddView()方法上面。再次回到makeAndAddView()方法,程式碼如下所示:

仍然還是在第19行嘗試從RecycleBin當中獲取Active View,然而這次就一定可以獲取到了,因為前面我們呼叫了RecycleBin的fillActiveViews()方法來快取子View。那麼既然如此,就不會再進入到第28行的obtainView()方法,而是會直接進入setupChild()方法當中,這樣也省去了很多時間,因為如果在obtainView()方法中又要去infalte佈局的話,那麼ListView的初始載入效率就大大降低了。

注意在第23行,setupChild()方法的最後一個引數傳入的是true,這個參數列明當前的View是之前被回收過的,那麼我們再次回到setupChild()方法當中:


可以看到,setupChild()方法的最後一個引數是recycled,然後在第32行會對這個變數進行判斷,由於recycled現在是true,所以會執行attachViewToParent()方法,而第一次Layout過程則是執行的else語句中的addViewInLayout()方法。這兩個方法最大的區別在於,如果我們需要向ViewGroup中新增一個新的子View,應該呼叫addViewInLayout()方法,而如果是想要將一個之前detach的View重新attach到ViewGroup上,就應該呼叫attachViewToParent()方法。那麼由於前面在layoutChildren()方法當中呼叫了detachAllViewsFromParent()方法,這樣ListView中所有的子View都是處於detach狀態的,所以這裡attachViewToParent()方法是正確的選擇。

經歷了這樣一個detach又attach的過程,ListView中所有的子View又都可以正常顯示出來了,那麼第二次Layout過程結束。

滑動載入更多資料

經歷了兩次Layout過程,雖說我們已經可以在ListView中看到內容了,然而關於ListView最神奇的部分我們卻還沒有接觸到,因為目前ListView中只是載入並顯示了第一屏的資料而已。比如說我們的Adapter當中有1000條資料,但是第一屏只顯示了10條,ListView中也只有10個子View而已,那麼剩下的990是怎樣工作並顯示到介面上的呢?這就要看一下ListView滑動部分的原始碼了,因為我們是通過手指滑動來顯示更多資料的。

由於滑動部分的機制是屬於通用型的,即ListView和GridView都會使用同樣的機制,因此這部分程式碼就肯定是寫在AbsListView當中的了。那麼監聽觸控事件是在onTouchEvent()方法當中進行的,我們就來看一下AbsListView中的這個方法:


這個方法中的程式碼就非常多了,因為它所處理的邏輯也非常多,要監聽各種各樣的觸屏事件。但是我們目前所關心的就只有手指在螢幕上滑動這一個事件而已,對應的是ACTION_MOVE這個動作,那麼我們就只看這部分程式碼就可以了。

可以看到,ACTION_MOVE這個case裡面又巢狀了一個switch語句,是根據當前的TouchMode來選擇的。那這裡我可以直接告訴大家,當手指在螢幕上滑動時,TouchMode是等於TOUCH_MODE_SCROLL這個值的,至於為什麼那又要牽扯到另外的好幾個方法,這裡限於篇幅原因就不再展開講解了,喜歡尋根究底的朋友們可以自己去原始碼裡找一找原因。

這樣的話,程式碼就應該會走到第78行的這個case裡面去了,在這個case當中並沒有什麼太多需要注意的東西,唯一一點非常重要的就是第92行呼叫的trackMotionScroll()方法,相當於我們手指只要在螢幕上稍微有一點點移動,這個方法就會被呼叫,而如果是正常在螢幕上滑動的話,那麼這個方法就會被呼叫很多次。那麼我們進入到這個方法中瞧一瞧,程式碼如下所示:


這個方法接收兩個引數,deltaY表示從手指按下時的位置到當前手指位置的距離,incrementalDeltaY則表示據上次觸發event事件手指在Y方向上位置的改變數,那麼其實我們就可以通過incrementalDeltaY的正負值情況來判斷使用者是向上還是向下滑動的了。如第34行程式碼所示,如果incrementalDeltaY小於0,說明是向下滑動,否則就是向上滑動。

下面將會進行一個邊界值檢測的過程,可以看到,從第43行開始,當ListView向下滑動的時候,就會進入一個for迴圈當中,從上往下依次獲取子View,第47行當中,如果該子View的bottom值已經小於top值了,就說明這個子View已經移出螢幕了,所以會呼叫RecycleBin的addScrapView()方法將這個View加入到廢棄快取當中,並將count計數器加1,計數器用於記錄有多少個子View被移出了螢幕。那麼如果是ListView向上滑動的話,其實過程是基本相同的,只不過變成了從下往上依次獲取子View,然後判斷該子View的top值是不是大於bottom值了,如果大於的話說明子View已經移出了螢幕,同樣把它加入到廢棄快取中,並將計數器加1。

接下來在第76行,會根據當前計數器的值來進行一個detach操作,它的作用就是把所有移出螢幕的子View全部detach掉,在ListView的概念當中,所有看不到的View就沒有必要為它進行儲存,因為螢幕外還有成百上千條資料等著顯示呢,一個好的回收策略才能保證ListView的高效能和高效率。緊接著在第78行呼叫了offsetChildrenTopAndBottom()方法,並將incrementalDeltaY作為引數傳入,這個方法的作用是讓ListView中所有的子View都按照傳入的引數值進行相應的偏移,這樣就實現了隨著手指的拖動,ListView的內容也會隨著滾動的效果。

然後在第84行會進行判斷,如果ListView中最後一個View的底部已經移入了螢幕,或者ListView中第一個View的頂部移入了螢幕,就會呼叫fillGap()方法,那麼因此我們就可以猜出fillGap()方法是用來載入螢幕外資料的,進入到這個方法中瞧一瞧,如下所示:

OK,AbsListView中的fillGap()是一個抽象方法,那麼我們立刻就能夠想到,它的具體實現肯定是在ListView中完成的了。回到ListView當中,fillGap()方法的程式碼如下所示:

down引數用於表示ListView是向下滑動還是向上滑動的,可以看到,如果是向下滑動的話就會呼叫fillDown()方法,而如果是向上滑動的話就會呼叫fillUp()方法。那麼這兩個方法我們都已經非常熟悉了,內部都是通過一個迴圈來去對ListView進行填充,所以這兩個方法我們就不看了,但是填充ListView會通過呼叫makeAndAddView()方法來完成,又是makeAndAddView()方法,但這次的邏輯再次不同了,所以我們還是回到這個方法瞧一瞧:


不管怎麼說,這裡首先仍然是會嘗試呼叫RecycleBin的getActiveView()方法來獲取子佈局,只不過肯定是獲取不到的了,因為在第二次Layout過程中我們已經從mActiveViews中獲取過了資料,而根據RecycleBin的機制,mActiveViews是不能夠重複利用的,因此這裡返回的值肯定是null。

既然getActiveView()方法返回的值是null,那麼就還是會走到第28行的obtainView()方法當中,程式碼如下所示:

這裡在第19行會呼叫RecyleBin的getScrapView()方法來嘗試從廢棄快取中獲取一個View,那麼廢棄快取有沒有View呢?當然有,因為剛才在trackMotionScroll()方法中我們就已經看到了,一旦有任何子View被移出了螢幕,就會將它加入到廢棄快取中,而從obtainView()方法中的邏輯來看,一旦有新的資料需要顯示到螢幕上,就會嘗試從廢棄快取中獲取View。所以它們之間就形成了一個生產者和消費者的模式,那麼ListView神奇的地方也就在這裡體現出來了,不管你有任意多條資料需要顯示,ListView中的子View其實來來回回就那麼幾個,移出螢幕的子View會很快被移入螢幕的資料重新利用起來,因而不管我們載入多少資料都不會出現OOM的情況,甚至記憶體都不會有所增加。

那麼另外還有一點是需要大家留意的,這裡獲取到了一個scrapView,然後我們在第22行將它作為第二個引數傳入到了Adapter的getView()方法當中。那麼第二個引數是什麼意思呢?我們再次看一下一個簡單的getView()方法示例:

第二個引數就是我們最熟悉的convertView呀,難怪平時我們在寫getView()方法是要判斷一下convertView是不是等於null,如果等於null才呼叫inflate()方法來載入佈局,不等於null就可以直接利用convertView,因為convertView就是我們之間利用過的View,只不過被移出螢幕後進入到了廢棄快取中,現在又重新拿出來使用而已。然後我們只需要把convertView中的資料更新成當前位置上應該顯示的資料,那麼看起來就好像是全新載入出來的一個佈局一樣,這背後的道理你是不是已經完全搞明白了?

之後的程式碼又都是我們熟悉的流程了,從快取中拿到子View之後再呼叫setupChild()方法將它重新attach到ListView當中,因為快取中的View也是之前從ListView中detach掉的,這部分程式碼就不再重複進行分析了。

為了方便大家理解,這裡我再附上一張圖解說明:

20150719213754421

那麼到目前為止,我們就把ListView的整個工作流程程式碼基本分析結束了,文章比較長,希望大家可以理解清楚,下篇文章中會講解我們平時使用ListView時遇到的問題,感興趣的朋友請繼續閱讀 Android ListView非同步載入圖片亂序問題,原因分析及解決方案 

相關文章