Android ListView與RecyclerView對(duì)比淺析

前言

RecyclerView是谷歌官方出的一個(gè)用于大量數(shù)據(jù)展示的新控件,可以用來(lái)代替?zhèn)鹘y(tǒng)的ListView,更加強(qiáng)大和靈活。

弄清楚RecyclerView是否有足夠的吸引力替換掉ListView,我從性能這一角度出發(fā),研究RecyclerView和ListView二者的緩存機(jī)制,并得到了一些較有益的”結(jié)論”,待我慢慢道來(lái)。

同時(shí)也希望能通過(guò)本文,讓大家快速了解RecyclerView與ListView在緩存機(jī)制上的一些區(qū)別,在使用上也更加得心應(yīng)手吧。

ListView與RecyclerView緩存機(jī)制原理大致相似,如下圖所示:


這里寫圖片描述

過(guò)程中,離屏的ItemView即被回收至緩存,入屏的ItemView則會(huì)優(yōu)先從緩存中獲取,只是ListView與RecyclerView的實(shí)現(xiàn)細(xì)節(jié)有差異.(這只是緩存使用的其中一個(gè)場(chǎng)景,還有如刷新等)


比較

緩存機(jī)制對(duì)比

層級(jí)不同:

RecyclerView比ListView多兩級(jí)緩存,支持多個(gè)離ItemView緩存,支持開發(fā)者自定義緩存處理邏輯,支持所有RecyclerView共用同一個(gè)RecyclerViewPool(緩存池)。

具體來(lái)說(shuō):
ListView(兩級(jí)緩存):


這里寫圖片描述

RecyclerView(四級(jí)緩存):

這里寫圖片描述

ListView和RecyclerView緩存機(jī)制基本一致

  • mActiveViews和mAttachedScrap功能相似,意義在于快速重用屏幕上可見的列表項(xiàng)ItemView,而不需要重新createView和bindView;

  • mScrapView和mCachedViews + mReyclerViewPool功能相似,意義在于緩存離開屏幕的ItemView,目的是讓即將進(jìn)入屏幕的ItemView重用.

  • RecyclerView的優(yōu)勢(shì)在于a.mCacheViews的使用,可以做到屏幕外的列表項(xiàng)ItemView進(jìn)入屏幕內(nèi)時(shí)也無(wú)須bindView快速重用;b.mRecyclerPool可以供多個(gè)RecyclerView共同使用,在特定場(chǎng)景下,如viewpaper+多個(gè)列表頁(yè)下有優(yōu)勢(shì).客觀來(lái)說(shuō),RecyclerView在特定場(chǎng)景下對(duì)ListView的緩存機(jī)制做了補(bǔ)強(qiáng)和完善。

緩存不同:

  • RecyclerView緩存RecyclerView.ViewHolder,抽象可理解為:
    View + ViewHolder(避免每次createView時(shí)調(diào)用findViewById) + flag(標(biāo)識(shí)狀態(tài));

  • ListView緩存View。

緩存不同,二者在緩存的使用上也略有差別,具體來(lái)說(shuō):
ListView獲取緩存的流程:

這里寫圖片描述

RecyclerView獲取緩存的流程:

這里寫圖片描述

RecyclerView中mCacheViews(屏幕外)獲取緩存時(shí),是通過(guò)匹配pos獲取目標(biāo)位置的緩存,這樣做的好處是,當(dāng)數(shù)據(jù)源數(shù)據(jù)不變的情況下,無(wú)須重新bindView,而同樣是離屏緩存,ListView從mScrapViews根據(jù)pos獲取相應(yīng)的緩存,但是并沒(méi)有直接使用,而是重新getView(即必定會(huì)重新bindView),相關(guān)代碼如下:

//AbsListView源碼:line2345
//通過(guò)匹配pos從mScrapView中獲取緩存
final View scrapView = mRecycler.getScrapView(position);
//無(wú)論是否成功都直接調(diào)用getView,導(dǎo)致必定會(huì)調(diào)用createView
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
    if (child != scrapView) {
        mRecycler.addScrapView(scrapView, position);
    } else {
        ...
    }
}

ListView中通過(guò)pos獲取的是view,即pos-->view;
RecyclerView中通過(guò)pos獲取的是viewholder,即pos --> (view,viewHolder,flag);

從流程圖中可以看出,標(biāo)志flag的作用是判斷view是否需要重新bindView,這也是RecyclerView實(shí)現(xiàn)局部刷新的一個(gè)核心。

局部刷新

由上文可知,RecyclerView的緩存機(jī)制確實(shí)更加完善,但還不算質(zhì)的變化,RecyclerView更大的亮點(diǎn)在于提供了局部刷新的接口,通過(guò)局部刷新,就能避免調(diào)用許多無(wú)用的bindView。

結(jié)合RecyclerView的緩存機(jī)制,看看局部刷新是如何實(shí)現(xiàn)的:
以RecyclerView中notifyItemRemoved(1)為例,最終會(huì)調(diào)用requestLayout(),使整個(gè)RecyclerView重新繪制,過(guò)程為:
onMeasure()-->onLayout()-->onDraw()

其中,onLayout()為重點(diǎn),分為三步:

  • dispathLayoutStep1():記錄RecyclerView刷新前列表項(xiàng)ItemView的各種信息,如Top,Left,Bottom,Right,用于動(dòng)畫的相關(guān)計(jì)算;
  • dispathLayoutStep2():真正測(cè)量布局大小,位置,核心函數(shù)為layoutChildren();
  • dispathLayoutStep3():計(jì)算布局前后各個(gè)ItemView的狀態(tài),如Remove,Add,Move,Update等,如有必要執(zhí)行相應(yīng)的動(dòng)畫.

其中,layoutChildren()流程圖:

這里寫圖片描述

這里寫圖片描述

當(dāng)調(diào)用notifyItemRemoved時(shí),會(huì)對(duì)屏幕內(nèi)ItemView做預(yù)處理,修改ItemView相應(yīng)的pos以及flag(流程圖中紅色部分):

這里寫圖片描述

當(dāng)調(diào)用fill()中RecyclerView.getViewForPosition(pos)時(shí),RecyclerView通過(guò)對(duì)pos和flag的預(yù)處理,使得bindview只調(diào)用一次.

需要指出,ListView和RecyclerView最大的區(qū)別在于數(shù)據(jù)源改變時(shí)的緩存的處理邏輯,ListView是"一鍋端",將所有的mActiveViews都移入了二級(jí)緩存mScrapViews,而RecyclerView則是更加靈活地對(duì)每個(gè)View修改標(biāo)志位,區(qū)分是否重新bindView。


RecyclerView 優(yōu)點(diǎn)

RecyclerView 相比 ListView 在基礎(chǔ)使用上的區(qū)別主要有如下幾點(diǎn):

  • ViewHolder 的編寫規(guī)范化了
  • RecyclerView 復(fù)用 Item 的工作 Google 全幫你搞定,不再需要像 ListView 那樣自己調(diào)用 setTag
  • RecyclerView 需要多出一步 LayoutManager 的設(shè)置工作

更加方便的實(shí)現(xiàn)自定義功能

Android 優(yōu)雅的為RecyclerView添加HeaderView和FooterView
Android 默認(rèn)提供的 RecyclerView 就能支持 線性布局、網(wǎng)格布局、瀑布流布局 三種(這里我們暫且不提代碼細(xì)節(jié),后文再說(shuō)),而且同時(shí)還能夠控制橫向還是縱向滾動(dòng)。怎樣,從效果上足以碾壓 ListView 有木有。

橫向滾動(dòng)的ListView開源控件是不是可以不用再找了?對(duì),你沒(méi)看錯(cuò)!
瀑布流效果的開源控件是不是可以不用再找了?對(duì),你沒(méi)看錯(cuò)!
連橫向滾動(dòng)的GridView都不用找了!對(duì),你沒(méi)看錯(cuò)!

而 LayoutManager 只是一個(gè)抽象類而已,系統(tǒng)已經(jīng)為我們提供了三個(gè)相關(guān)的實(shí)現(xiàn)類:

  • LinearLayoutManager(線性布局效果)
  • GridLayoutManager(網(wǎng)格布局效果)
  • StaggeredGridLayoutManager(瀑布流布局效果)

RecyclerView 基礎(chǔ)使用關(guān)鍵點(diǎn)同樣有兩點(diǎn):

  • 繼承重寫 RecyclerView.Adapter 和 RecyclerView.ViewHolder;
  • 設(shè)置布局管理器,控制布局效果

系統(tǒng)也為我們提供了兩個(gè)默認(rèn)的動(dòng)畫實(shí)現(xiàn):SimpleItemAnimator 和 DefaultItemAnimator。而 RecyclerView 在不手動(dòng)調(diào)用 setItemAnimator 的情況下,則默認(rèn)用了內(nèi)置的 DefaultItemAnimator 。

RecyclerView緩存機(jī)制總結(jié)

主要靠三個(gè)內(nèi)部類來(lái)完成,Recycler,ViewCacheExtension,RecyclerViewPool:

  • 首先通過(guò) recycler.getViewForPosition()方法,該方法返回ViewHolder對(duì)象,通過(guò)源碼可以知道,該方法會(huì)檢查mAttachedScrap和一級(jí)緩存列表mCachedViews,如果有則返回ViewHolder進(jìn)行復(fù)用。
  • 然后調(diào)用ViewCacheExtension.getViewForPositionAndType()方法,注意這個(gè)方法是抽象方法,需要開發(fā)者進(jìn)行重寫。
  • 最后檢查RecyclerViewPool是否有ViewHolder。

注意:上述的三個(gè)步驟中,只要有一個(gè)返回了ViewHolder,就不會(huì)在進(jìn)行后邊的步驟了。
最后:緩存的數(shù)量:默認(rèn)的一級(jí)緩存中,mCachedViews中可以緩存的ViewHolder的個(gè)數(shù)是2;默認(rèn)的緩存池中的緩存數(shù)量是 5;所以在緩存時(shí),會(huì)先檢測(cè)一級(jí)緩存是否滿了,如果沒(méi)滿就add進(jìn)去,如果滿了就加入到三級(jí)緩存Recyclerpool


ListView 優(yōu)化

如果頁(yè)面不是復(fù)雜,也不是需要太多功能,只需要簡(jiǎn)單的列表功能,那就可以繼續(xù)使用ListView, 畢竟實(shí)現(xiàn)起來(lái)比RecyclerView簡(jiǎn)單些。

如何優(yōu)化ListView的性能:

  • 盡最大可能避免GC
  • 滑動(dòng)的時(shí)候不加載圖片
  • 將ListView的scrollingCache和animateCache設(shè)置為false
  • convertView重用;
    利用好 convertView 來(lái)重用 View,切忌每次 getView() 都新建。ListView 的核心原理就是重用 View,如果重用 view 不改變寬高,重用View可以減少重新分配緩存造成的內(nèi)存頻繁分配/回收;
  • ViewHolder優(yōu)化;
    使用ViewHolder的原因是findViewById方法耗時(shí)較大,如果控件個(gè)數(shù)過(guò)多,會(huì)嚴(yán)重影響性能,而使用ViewHolder主要是為了可以省去這個(gè)時(shí)間。通過(guò)setTag,getTag直接獲取View。
  • 圖片加載優(yōu)化
    如果ListView需要加載顯示網(wǎng)絡(luò)圖片,我們盡量不要在ListView滑動(dòng)的時(shí)候加載圖片,那樣會(huì)使ListView變得卡頓,所以我們需要在監(jiān)聽器里面監(jiān)聽ListView的狀態(tài),如果ListView滑動(dòng)(SCROLL_STATE_TOUCH_SCROLL)或者被猛滑(SCROLL_STATE_FLING)的時(shí)候,停止加載圖片,如果沒(méi)有滑動(dòng)(SCROLL_STATE_IDLE),則開始加載圖片。
  • onClickListener處理(通過(guò)接口回傳)
  • 減少Item View的布局層級(jí)
    這是所有l(wèi)ayout都必須遵循的,布局層級(jí)過(guò)深會(huì)直接導(dǎo)致View的測(cè)量與繪制浪費(fèi)大量的時(shí)間
  • adapter中的getView方法盡量少使用邏輯
    不要在getView方法中做過(guò)于復(fù)雜的邏輯,可以想辦法抽離到別的地方,
  • adapter中的getView方法盡量少做耗時(shí)操作
  • adapter中的getView方法避免創(chuàng)建大量對(duì)象
  • 將ListView的scrollingCache和animateCache設(shè)置為false
  • 分頁(yè)加載數(shù)據(jù)

總結(jié)

是選擇ListView還是RecyclerView 可以根據(jù)項(xiàng)目中的功能來(lái)的,但是對(duì)于他們了解之后的優(yōu)化也是必要的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容