現(xiàn)在開發(fā)中列表控件基本都是使用recyclerview控件,recyclerview在結(jié)構(gòu)上使用了跟Listview差不多的view以及adapter之外,還使用了LayoutMananger,LayoutManager主要負責測量以及布局擺放,也正是因為LayoutManager負責了擺放,所以LayoutManager是最清楚知道View擺放位置的一層,對應的涉及到view位置更改的操作也都是由它處理的,比如Item的拖拽。
recyclerview跟Listview的區(qū)別
1.ListView是由自己來測量以及布局的,recyclerview增加了LayoutManager, 交由Manager來測量以及布局,職責更加分明
2.ListView的復用是通過ViewHolder中轉(zhuǎn)來復用View,RecyclerView是通過復用ViewHolder來實現(xiàn)View緩存的
3.ListView是兩級緩存,具體可見RecyclerBin該類的注釋。ListView緩存的載體為 ActiveViews(數(shù)組) 以及mScrapViews(Array數(shù)組),這兩個數(shù)組中存儲的都是View對象,正因為這兩個數(shù)組,所以LIstview中的type類型都是0,1,2,3這種連續(xù)的Int型數(shù)組。RecyclerView緩存主要是Recycler這個類來處理,載體跟LIstview差不多,多了一個RecyclerViewPool,但是不同的是,存儲的都是ViewHolder;更好的一個點是,同一個上下文中的Recylerview可以共用RecyclerViewPool,所以可以提高內(nèi)存的使用效率。
4.ListView的弊端在于,如果給某一個View設置動畫,沒有辦法特別精細的去操作某一個View,只能整體重新測量布局然后刷新,極大的損失了資源。并不是無法實現(xiàn),而是很麻煩。Recyclerview則可以局部刷新。
5.Recyclerview有一個DividerItemDeCoration,可以很方便的設置邊界線,并且很好的執(zhí)行動畫,并且DividerItemDeCoration并不是只能在Item的邊界去繪制,范圍是整個Recyclerview。
ListView的運作機制如下:
通過adapter去創(chuàng)建view,屏幕內(nèi)沒有顯示view時也就沒有緩存,直接創(chuàng)建,當View放滿屏幕時,再滑動,劃出屏幕的view會被存儲起來,即將劃入屏幕的View會優(yōu)先從內(nèi)存中取出來復用,對應的就是咱們經(jīng)常寫的 Viewholder緩存代碼,其中g(shù)etView中的第二個參數(shù)converView,就是從內(nèi)存中取出的View。
ListView緩存的載體為 ActiveViews(數(shù)組) 以及mScrapViews(Array數(shù)組),這兩個數(shù)組中存儲的都是View對象
這里需要注意,mScrapViews,該數(shù)組的索引即為adapter的type類型,回想咱們再編寫ListViewAdapter的時候,是不是需要用一個int型去創(chuàng)建type類型?正因為這個數(shù)組,所以Listview中的type類型都是0,1,2,3這種連續(xù)的Int型類型。mScrapViews的索引代表類型,每一個索引對應的ArrayList中存的就是當前type的View緩存。
那這個緩存是如何運作的呢,兩種情況
- 當調(diào)用notifyDataSetChange時,會將屏幕上顯示的View全都放到mScrapViews中緩存起來,但時數(shù)據(jù)會被清空,而后刷新完成后,重新將緩存取出來,再調(diào)用Adapter的onBind方法重新綁定數(shù)據(jù)。
2.當滑動的時候,會先從緩存中取View,取出來的View也就對應了getView()的convertView,但是緩存中可能沒有,所以咱們在寫adapter時,需要判斷這個View是否為null,如果為null需要重新創(chuàng)建。 - 那ActiveViews有什么作用呢?實際上當adapter的數(shù)據(jù)沒有發(fā)生改變,但是觸發(fā)了onLayout方法,如:requestLayout的時候,會使用ActiveViews存起來,并且重新讀取時并不會觸發(fā)adapter的getView以及onBind,那這個速度就很快了,但是ListView并沒有很好的運用這一層緩存機制。那可能有人問了,我可以在data不改變的情況下notifydatasetChange,這不也沒改變數(shù)據(jù),很遺憾,只要調(diào)用notifydatasetChange,ListView就會判定數(shù)據(jù)發(fā)生改變了,所以說ListView并沒有很好的運用這一層緩存機制。
Recyclerview的運行流程大概如下:
需求方Recyclerview需要顯示View,會通知LayoutManager, LayoutManager并不會自己創(chuàng)建view,也不會直接找Adapter拿View,而是會通過Recycler這個類,先判斷緩存中有無,(類似于圖片的三級緩存,只不過沒有本地文件緩存,直接從內(nèi)存中找,沒有就創(chuàng)建)無的話找Adapter拿,調(diào)用的是Adapter的onCreatViewHolder方法,如果有也會找Adapter重新綁定數(shù)據(jù),對應的是onBindViewHolder,然后拿到ViewHolder之后返回給LayoutManager,然后布局擺放,最后展示出來。
Recyclerview的緩存運作大概如下:
-
Recyclerview中有一個mCachedViews集合,類型為Arraylist<ViewHolder>,該集合的默認Size=2。假設當前一屏只夠展示10條item,從0到9從上往下依次擺放,當手指繼續(xù)向上滑動時,0和1對應的item會劃出屏幕,完全滑出屏幕后,緩存開始生效,將這兩條存入mCachedViews, 此時如果改變方向手指向下滑動,滑到1完全展示時,會依據(jù)position從緩存中取出1,說明這時的ViewHolder是保留position的(不像Listview一樣,先根據(jù)position取出type對應的Array)。那到這里就會產(chǎn)生一個問題,如果在item中展示的某些控件,需要在item劃出屏幕后去釋放回收資源怎么辦?這不是就有問題了嗎?其實谷歌已經(jīng)考慮到該問題,增加了兩個回調(diào)
recyclerview-3.png
可以在這里處理View劃出屏幕之后的邏輯。
- 那如果存入的緩存數(shù)量>mCachedViews的size,存入時間最早的緩存就會被取出,放入到RecyclerViewPool中,這個池子中包含了一個SparseArray<ScrapData>ScrapViews
RecyclerViewPool為Recycler的一個內(nèi)部類,ScrapData中包了一個ArrayList<ViewHolder>類型的mScrapHeap對象,最終的數(shù)據(jù)格式為 SparseArray<ArrayList<ViewHolder>>。這種格式的優(yōu)勢在于,key可以是不連續(xù)的Int值。
RecyclerViewPool中有一個DEFAULT_MAX_SCRAP=5,表示每種ViewType中包含的數(shù)據(jù)條數(shù)默認為5條。若某些場景下需要優(yōu)化該緩存數(shù)量,可以調(diào)用setMaxRecycledViews(int viewtype,int max)來優(yōu)化緩存機制。
說回緩存機制,放入到RecyclerViewPool中后,如果用戶想滑回去重新展示已經(jīng)放到RecyclerViewPool中的數(shù)據(jù),就可以直接拿到緩存使用,并且該緩存是已經(jīng)保存了數(shù)據(jù)的,所以不需要調(diào)用綁定數(shù)據(jù)的方法。需要注意的是,保存了數(shù)據(jù),不代表數(shù)據(jù)就是正確的。在某些場景下,綁定數(shù)據(jù)可能會耗時,那這時就有很好的優(yōu)化效果了??梢韵饶玫娇丶系臄?shù)據(jù)判斷是否為目標數(shù)據(jù),一致的話就不需要更新了,否則再重新綁定數(shù)據(jù)。
如果觸發(fā)notifyDataSetChange,緩存會怎么運作呢?
觸發(fā)notifyDataSetChange時,跟Listview一樣,列表會判斷這是一次數(shù)據(jù)變更,會將這些數(shù)據(jù)放到緩存里,那放到RecyclerViewPool中呢還是mCachedViews中呢?其實答案已經(jīng)很明顯了,既然判斷是一次數(shù)據(jù)的大變更,那肯定需要重新綁定數(shù)據(jù)的,而mCachedViews中是直接拿來用的,不會重新綁定數(shù)據(jù)。所以是放入到RecyclerViewPool中,如果pool中的數(shù)量不夠,就會調(diào)用createViewHolder以及onBindViewHolder的方法,重新創(chuàng)建。這樣的話資源消耗就很大了,所以非必要的情況下,我們需要調(diào)用局部刷新功能以優(yōu)化資源消耗。如果非要調(diào)用notifyDataSetChange的情況下優(yōu)化,那可以再notifyDataSetChange之前,增大緩存池的容量,而后調(diào)用notifyDataSetChange,再縮小緩存池,只是該方式不夠優(yōu)雅。
以上兩層的優(yōu)化跟ListView的區(qū)別是不太大的。RecyclerView除了以上的兩層優(yōu)化,還有別的優(yōu)化。
- Recyclerview的另外一層優(yōu)化是局部刷新。局部刷新時可以節(jié)省大量的資源,這是顯而易見的。但是現(xiàn)在需要考慮一個問題,當調(diào)用notifyItemInsert時,插入item的后續(xù)item都需要往下移動,那必然會導致有些item不可見。那這些不可見的item去哪里呢,這里就引入了一個AttachedScrap,這就是第三個緩存(只是咱們這篇文章中介紹的第三個,從優(yōu)先級來說是第一級),放入到該緩存中,有需要會優(yōu)先從這里去取。AttachedScrap是一個暫存區(qū),當調(diào)用局部刷新開始時,先把多出來的ViewHolder存進去,當本次刷新調(diào)用直到布局完成后,會將AttachedScrap里剩余的ViewHolder拿出來存入RecycledViewPool,重點重點重點:完成一次布局之后。有點類似于垃圾回收中的老生代新生代機制。
- 當某個Item的數(shù)據(jù)發(fā)生改變,調(diào)用notifyItemChange時,該ViewHolder會被放入到ChangeScrap的ArryList中,ChangeScrap只是在布局開始到布局結(jié)束過程中臨時存放,布局完成之后就把緩存挪到AttachedScrap,使用時間很短。該暫存區(qū)是否可以算作一層緩存呢?值得思考
那以上四級緩存的優(yōu)先級是怎樣的呢?正常情況下,首先是AttachedScrap,然后是CachedViews,最后是RecycledViewPool。局部修改刷新時,第一層為ChangeScrap緩存,其他幾層同正常情況下的順序。
總結(jié)一下:
- AttachedScrap:之前在屏幕內(nèi),由于局部刷新被擠出屏幕,比如調(diào)用notifyItemInsert插入一條新數(shù)據(jù),就可能有一條數(shù)據(jù)被擠出屏幕。notifyItemRemove:可能會有一條數(shù)據(jù)被移除。這些數(shù)據(jù)會被判定為比較新的數(shù)據(jù),所以在緩存優(yōu)先級中最高。
- mCachedViews:屏幕內(nèi)的item被滑動出RecyclerView的邊界時,會根據(jù)position存入該緩存,但由于容量有限,較老的緩存會被放入RecycleViewPool,主要優(yōu)化滑動或者回滾時期的ViewHolder。不需要重新綁定數(shù)據(jù)。
- mViewScrapExtation 該緩存為自定義的緩存,極少使用。
- ReccycleViewPool,在調(diào)用notifyDataSetChange時會全部緩存進該緩存。根據(jù)ViewType來緩存ViewHolder,所以ViewHolder上的數(shù)據(jù)跟postion可能都是錯誤的,需要重新Bind數(shù)據(jù),在綁定數(shù)據(jù)較為耗時的情況下,可以直接判斷View上的數(shù)據(jù)和正確的數(shù)據(jù)是否一致,來優(yōu)化。每種ViewType的大小默認為5,開發(fā)者可修改。該回收池也可以自定義。
還沒完,Recyclerview還有一個非常核心的機制。
使用過RecycleView的都知道,Recyclerview的另外一大優(yōu)點是可以很輕松的給Item增加動畫,極大的提高了體驗。那思考一下,這個動畫是如何在更新列表的同時增加上去的呢。這里就需要引出另外一層核心的機制了:pre/post-layout。
這里先說一個示例。
- 假設現(xiàn)在有一屏僅能顯示10條item的Recycelrview,當我們調(diào)用notifyItemRemove的時候,移除index=9的item,默認的動畫為index=10的item會向上移動。那么問題來了,index=10的item執(zhí)行動畫的初始位置是怎么計算的?有的同學說,10不就在9的下方嗎?這是錯誤的,由于一屏展示10條,index=10的item理論上來說在屏幕外,但實際上未移除9之前,LayoutManager并未在屏幕上擺放index=10的item。
那如何計算呢?這里Recyclerview使用了預測性動畫,調(diào)用notifyItemRemove時,Recyclerview一看 屏幕內(nèi)沒有10,就會通知LayoutManager從緩存中拿或者創(chuàng)建第10條擺放到9的下面,這樣就有初始位置了,接下來執(zhí)行動畫。
這個理解之后,開始考慮下一個問題:當修改某一條Item時,默認動畫為 輕輕的閃爍一下。那這個動畫是如何實現(xiàn)的呢?為什么會閃爍一下。 - 當修改某一條Item時,調(diào)用notifyItemChange會閃爍,是因為Item執(zhí)行了淡出淡入的動畫。問題又來了動畫執(zhí)行前后是同一個View嗎?首先,動畫不是在同一個item對象上同時發(fā)生的,是兩個View一個在執(zhí)行出場動畫,一個執(zhí)行入場動畫。嗯?為什么是兩個View?這里就是pre/post-layout運作了。preLayout指的是 View change之前對應的layout,這個layout中的ViewHolder是舊的。postLayout指的是change之后對應的layout,這個ViewHolder是新的。那由于有緩存,舊的直接在緩存中取就可以,那新的呢?新的內(nèi)存中可能是沒有的,所以recyclerview的prelayout是從AttachedScrap以及ChangedScrap中取,postLayout只能從CachedViews以及RecyclerViewPool中取,取不到就去創(chuàng)建新的。
setSupportsChangeAnimations(false)可以禁用這個默認的動畫。那如果我只是想禁用閃爍的動畫,插入刪除的動畫還要用怎么辦?可以調(diào)用notifyItemChanged(position,"payload")。
