ListView是咱們開發中最經常使用的組件之一,在以往的PC端組件開發中,列表控件也是至關重要的,可是從桌面端到移動端,狀況又有新的變化。數據庫
移動端的屏幕並不像桌面端那麼大,而且移動端不可能把全部的內容都一會兒展示出來,由於Android系統分配給一個應用的內存是有限的,而任何顯示在組件上面的內容都是加載在內存中的,若是一個Item包含相似圖片這樣的佔內存的內容,很容易就內存爆炸,也就是OOM,並且Android的界面渲染是在主線程中,若是耗時太長,用戶在屏幕上有其餘事件輸入,如點擊觸摸,超過5秒沒有響應就會ANR,渲染時間若是超過60毫秒,就會開始感受到卡頓了,也就是所謂的掉幀。設計模式
解決卡頓的問題,減小布局的層級提升渲染速度,還有就是不要在主線程中進行耗時操做,竭力避免內存抖動,頻繁的GC也會引發界面卡頓。緩存
很遺憾,ListView都有可能要面對上面全部問題。網絡
ListView首先要面對一個嚴峻的問題:數據來源的加載。數據結構
ListView綁定了一個數據源,而後根據數據源的個數,返回對應數目item的View進行渲染。數據來源多是數據庫查詢,也多是網絡獲取,這些耗時操做按理都不該該在主線程中進行,可是ListView綁定數據源的操做卻必定要在主線程進行,任何和View有關的操做都要在主線程。併發
這就涉及到跨線程通訊,咱們能夠用異步任務或者EventBus等各類方式來完成這個綁定數據源的動做。框架
解決這個問題後,咱們還得面對item在手機上顯示的問題。異步
咱們是不可能讓全部的item一會兒在屏幕上顯示出來,手機可以顯示的item數目有限,而且用戶是有很大的可能不會將全部的item都看完,所以在用戶看不見的地方顯示item是很不值得的行爲。ide
ListView對item進行了緩存,只緩存了一個屏幕可以顯示的數目。佈局
這個機制的實如今原生中是很簡單的,Adapter的getView中有一個convertView,它在一開始繪製的時候都是null,在繪製完第一屏的item後,它就不會是空的,存放的是第一屏item的內容,就算這些item滑出後,它都不會是空的。
ListView老是緩存一個屏幕可以顯示的item + 1的數目的View。
瞭解到這點後,咱們就沒有必要每次都從新建立convertView,只要將新的內容顯示在convertView緩存好的組件上,就能減小inflate的時間。
inflate實際上是很耗時的,由於inflate會涉及到組件寬高的計算,還有內容的顯示,一個item的infalte的時間可能不算多,但每次滑動都會inflate,尤爲是item的內容涉及到圖片加載等,就會形成在infalte的時候還要響應屏幕的滑動,這就會形成卡頓了,畢竟主線程就一個,屏幕滑動和控件繪製都是在主線程。
因此咱們要找到辦法來利用convertView的這個特性。
首先解決convertView重複建立的問題。
咱們能夠先判斷convertView是否爲null,若是爲null,再從新建立。
if(convertView == null){ convertView = LayoutInflater.from(context).inflate(R.layout.item_pratice, null); }
這解決了convertView重複建立的問題。
當咱們要使用佈局中的組件時,會先經過findViewById來聲明組件,這在通常的頁面中沒問題,但若是是一個列表,就有問題了。
findViewById是很浪費時間的。
findViewById要遍歷View的樹形結構來找到對應的id,並且這個遍歷是從頭至尾,因此若是該View的層級比價複雜,這個查詢就比較耗時了。
咱們在佈局文件中採用@+id的形式指定控件id,就會在R文件中生成一個id,也能夠採用@id的形式,經過在ids文件中聲明一個id。
這兩種形式的區別到底在哪裏呢?實際上,不管採用哪一種方式,findViewById的時間都是差很少的,可是@+id的形式,在代碼中能夠點擊跳轉到對應界面中的控件,而@id的形式只能跳轉到對應的ids文件,這在查看控件時候是很不方便的。
爲了解決這個問題,谷歌推薦咱們使用ViewHolder的形式。
ViewHolder自己並不神祕,它就是聲明瞭一個存儲了組件實例的類,而後要用的時候再取出來,這樣就不用每次都findViewById。
在Java中,對同一個引用進行操做,會修改該引用指向的對象,ViewHolder就是經過保存佈局中組件的引用,達到重複利用的目的,由於convertView中的組件引用和ViewHolder是相同的。
@Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder viewHolder; if(convertView == null){ convertView = LayoutInflater.from(context).inflate(R.layout.item_pratice, null); viewHolder = new ViewHolder(); viewHolder.tvName = (TextView)convertView.findViewById(R.id.tv_name); convertView.setTag(viewHolder); }else{ viewHolder = (ViewHolder)convertView.getTag(); } String name = nameList.get(position); viewHolder.tvName.setText(name); return convertView; } class ViewHolder{ TextView tvName; }
這就是很經典的ViewHolder的使用。
知道ViewHolder持有了佈局中組件的引用後,咱們就會思考一件事:引用的持有傳遞問題。
Android中的內存泄露問題,都是由於不應持有的引用被持有了,沒有及時釋放,Adapter並不保證是否會被傳遞給哪一個Activity,但Adapter中的ViewHolder卻持有了佈局中組件的引用,這就是內部類引發的內存泄露問題。
Adapter在這個問題上並無其餘狀況須要咱們特別謹慎,就算須要傳遞Adapter執行某些行爲,那也是相關業務的需求,一旦脫離這些場景,Adapter也就再也不被任何人持有,除非執意要把Adapter交給一個應用生命週期的類持有。
谷歌自己的ViewHolder是靜態修飾的,由於ViewHolder通常都是內部類的形式,而Java中,對於內部類的建議都是採用靜態的修飾,若是沒有必要持有外部類引用的話。
靜態內部類的闖將不依賴於外部類,所以在建立靜態內部類的實例時,無需建立外部類的實例,而內部類則須要,由於靜態內部類能夠節省建立外部類的內存開銷。而正如咱們上面所說的,ListView緩存的是一屏幕的item的View,所以convertView在開始的時候會初始化屢次,次數爲一屏幕的item + 1次,而ViewHolder一樣也會實例化屢次,而ViewHolder是內部類,持有外部類Adapter的引用,而Adapter自己又持有Activity的Context,雖然不至於形成內存泄露,只要咱們不對Adapter亂來的話,可是這部分的內存分配開銷卻會形成浪費,而靜態的ViewHolder則解決了這個問題。
ViewHolder是否靜態,影響並非很大,但養成良好的編碼習慣,倒是很重要的,內部類形成的隱藏內存泄露,是很難覺察到的。
至此,咱們能夠知道,ViewHolder其實並非多高深的技術,也不是什麼複雜的設計模式,無非就是一個巧妙的編碼策略,經過convertView的setTag將對應的組件緩存起來,咱們甚至能夠不用這個ViewHolder也能夠作到相似的,像是使用一個HashMap,記錄每一個位置的組件。
不過這裏咱們得注意一個特別的組件:ImageView。
ImageView是圖片加載組件,而加載的圖片資源若是過大,就會形成OOM,若是圖片是異步加載的URL地址,不作處理還會形成圖片錯亂的問題,由於滑動的時候,圖片還沒下載下來,可是對應位置的item已經滑出屏幕了,這時就會加載到下一屏的item上,由於這兩個位置的view實際上是同一個。
網上關於解決這個問題,是在對應的ImageView再設置一個tag值,而後存儲對應的URL,而後在每次加載的時候,都要將下載的URL和這個存儲的URL進行對比,若是相同,才加載已經下載下來的圖片。
列表在滑動的時候,會大量啓動異步任務來下載圖片,快速滑動的時候,假設第一個item的圖片還沒下載完,已經滑到了對應的第10個item,而這個item和第一個item都是指向同一個組件,此時第一個item的圖片已經下載完了,就會將圖片加載到本身持有的組件上,而這個組件雖然仍是第一個item上的組件,但實際上此時已是第10個item了,因此這時加個判斷,就能預防圖片錯亂。
這時咱們就會意識到:列表的圖片異步加載是一個多麼蛋疼的地方。想一想看,在快速滑動的時候,會啓動多少個異步線程在下載圖片,哪怕這個item已經滑出了屏幕,並且若是沒有作啥處理的話,就算滑回來,也是一樣的開啓一個新的異步下載線程。
開啓下載線程的開銷是不小的,加上圖片加載自己也佔內存,所以大量圖片的列表在滑動的時候,很是容易就OOM了。
因此爲了減小異步下載線程,咱們須要圖片緩存。
圖片緩存的實現有不少,咱們儘可能選擇成熟的圖片加載框架,像是ImageLoader或者Glide,沒有必要本身造輪子,但要知道對應框架的大概原理。
ImageLoader是分配了固定的圖片內存,這塊的處理並不複雜,實質就是一個LinkedHashMap,以圖片的url+圖片的寬高爲key值,存放對應的bitmap,每次put進來的時候,若是該key已經存在對應的bitmap,就會減去這個bitmap的size,而後再和設置好的最大圖片內存進行比較,比較的方法就是計算LinkedHashMap的總大小,若是大於最大值,則會移除第一個元素,而後再從新對比。
移除第一個元素看似影響很大,由於這個bitmap也是緩存下來的,也是可能須要用到的,可是想到緩存的圖片大小都超過了內置的最大內存,並且第一個元素已是滑出屏幕很遠的元素了,也就是使用頻率最低的元素,優先處理使用頻率最低的元素是符合常理的。
這也是使用LinkedHashMap的緣由,它可以保證元素的先進先出,而HashMap就是亂序的。
這部分的邏輯具體能夠在ImageLoder的DefaultConfigurationFactory和LruMemoryCache中看相關的代碼。
咱們來看看ImageLoader是怎麼解決上面開線程的問題。
線程確定是不能隨便亂開的,一個應用的內存空間有限,而線程開銷並不小,所以必需要經過某種機制來解決這個線程調度的問題。
Java自己提供了對應的庫,不過咱們須要針對實際的狀況本身作些處理。
思路很簡單:線程的總數是固定的,一個抽象負責這些線程的調度,包括建立和回收,這個抽象就是線程池。
線程池的知識很是多,這裏就只是大概說一下ImageLoader是如何避免多開線程同時又能知足多個圖片下載的需求。
線程池能夠限制容許運行的線程的數量,通常狀況是按照CPU的個數 * 2 + 1,ImageLoader剛出來的時候,雙核手機仍是主流,因此以至於如今看到的不少有關ImageLoader的線程數量限制,都是5,不過通常5個線程就差很少了。
爲何是CPU的個數 * 2 + 1呢?
咱們要理解併發和並行的意思。
併發是指同一個時間間隔內多個事件進行,而並行是指同一個時刻進行。線程併發實際上的要求就是:同一個時間間隔內多個事件可以快速進行,因此事件的執行速度和結束完畢後另外一個事件的執行就顯得很重要了,由於同一個時刻實際上只是一個事件在進行,因此若是這個事件一直在執行,其餘事件就得排隊,這就是堵塞。
線程池經過隊列來實現併發。咱們能夠指定一個隊列,這個隊列用於存放還在排隊的事件,假設線程池只容許5個線程的開銷,那麼同時進行的事件只有5個,這就是並行,而其餘的事件就必須等待這些事件中某一個執行完畢,而這種吞吐能力,就是併發能力。
這些只要交給ImageLoader本身自己就能夠了。
ImageLoader是使用HttpURLConnection來下載圖片,下載完後,若是配置有指定磁盤緩存,就會放到磁盤緩存裏面,而後下次取圖片的時候,就會從磁盤緩存裏面取。
ImageLoader自己並無對圖片是否錯亂這個現象作處理,實際的使用咱們能夠看到圖片是有可能一開始是錯的,不事後面又從新刷新變成對的,那是由於隊列中該位置的任務已經執行完畢了,就會對這個item的控件從新渲染。
因此ImageLoader自己的任務就是幫助咱們解決圖片下載緩存的工做,可是至於滑動卡頓和圖片錯亂等上層業務的問題,它是沒有必要去解決的。
使用ImageLoader在快速滑動的時候,會形成內存抖動,由於它頻繁的去計算當前的內存是否已經達到最大值,會常常的檢查和釋放,而內存抖動是會致使卡頓的,加上滑動時候任務不斷執行,執行完畢的時候會對控件進行從新渲染,這時候從人的感官來看,也是有所謂的閃動現象,就是控件原有的圖片被清除掉,短暫的出現白屏現象。
咱們能夠在滑動的時候中止加載圖片,可是滑動結束後,圖片的加載就會很慢,有可能它的任務那時候還沒開始。
影響到圖片加載慢的因素除了咱們須要下載圖片(雖然這個過程一般都是異步的,但正如上面說的,異步任務的數目不會是無限的,而且須要排隊),還有一個因素就是控件自己的渲染。
圖片加載庫幫咱們解決了圖片下載的問題,可是控件自己的渲染,如寬高的計算,繪製圖片等,一樣也會影響到這個過程。
在Android中,佈局文件常見的寬高指定一般就是match_parent和wrap_content,由於Android手機的分辨率太多了。這兩種方式都不指定寬高的具體指,所以控件在渲染的時候,會本身去計算,而這個計算就算已經完成,在同一個佈局中的其餘控件渲染的時候,也會從新計算,所以一個控件渲染屢次,是徹底正常的。
若是咱們能在一開始就指定控件的寬高,就不會再渲染的時候從新計算,可是爲了適配各類分辨率,指定寬高的作法通常比較少。
前面的狀況都是咱們在作有圖片內容的列表需求時都會遇到的,沒有什麼方案是完美的,只有根據實際狀況折衷的實現方案。
前面咱們看到ViewHolder本質就是維護item上組件的引用,減小了findViewById的調用,可是須要聲明一個ViewHolder的實例。
有沒有更加簡單的方案?
答案是有的,這個方案的原理就是咱們本身經過一個數據結構來維護組件id和組件之間的綁定關係。
咱們能夠嘗試使用HashMap來維護這個關聯。
Map<Integer, View> viewMap = (HashMap<Integer, View>) view.getTag(); if (viewMap == null) { viewMap = new HashMap<Integer, View>(); view.setTag(viewMap); } View childView = viewMap.get(id); if (childView == null) { childView = view.findViewById(id); viewMap.put(id, childView); }
這和ViewHolder是一樣的原理,只不過一個是類來維護一組引用,一個是數據結構存儲一組引用。
咱們還能夠將HashMap的數據結構替換成谷歌的SparseArray,這是谷歌優化過的HashMap,在速度上會更快。
最後代碼以下:
public class ViewHolder { public static <T extends View> T get(View view, int id) { SparseArray<View> viewHolder = (SparseArray<View>) view.getTag(); if (viewHolder == null) { viewHolder = new SparseArray<View>(); view.setTag(viewHolder); } View childView = viewHolder.get(id); if (childView == null) { childView = view.findViewById(id); viewHolder.put(id, childView); } return (T) childView; } } ... public View getView(..){ if(convertView == null){ ... } TextView tvName = (TextView)ViewHolder.get(convertView, R.id.tvName); return convertView; }
這種方式會更加簡潔,可是在效率上,由於多了HashMap的查找,是會慢一點,可是在這種數據量很是少的狀況下,這種查找損耗是徹底能夠忽略的。
上面就是咱們使用ListView會遇到的基本場景,若是有更加複雜的需求,那得看具體的狀況,而且不少時候單靠ListView也是很難解決的,像是一行兩列的佈局,用GridView會更好。
列表的控件常常會有這樣的需求,一行多列,因而咱們就得在ListView和GridView間切換,但實際上,在Android中,ListView和GridView其實都是同源的東西,都是AbsListView的子類,只不過GridView作了特殊處理而已。
基於這樣的事實,谷歌推出了一個全新的類:RecyclerView。
RecyclerView並非AbsListView的子類,它是一個全新的ViewGroup,兼具ListView和GridView的切換,還增長了一些默認的動畫,更重要的是,使用RecyclerView,就必須強制性的使用ViewHolder的機制。
使用RecyclerView並不像ListView同樣,基礎庫就有,必需要導入本身項目中對應的兼容庫版本的RecyclerView。
RecyclerView一樣也須要一個Adapter,可是這個Adapter和ListView的Adapter不同。
RecyclerView的Adapter須要繼承自Recycler.Adapter<VH extends ViewHolder>,而此次ViewHolder和咱們本身寫的不同,它不再是一個簡單的控件引用的集合體,它遠要複雜得多,可是咱們要作的工做卻和之前沒兩樣,不過此次須要明確的繼承自ViewHolder,而且實現構造器,而構造器中正是findViewById的地方。
構造器的參數正是要傳入的convertView,傳入的地方是咱們要實現的onCreateiewHolder方法,而onBindViewHolder是實現數據源和對應View綁定的地方。
咱們能夠發現,RecyclerView的Adapter其實是把BaseAdapter的getView的職責拆成了onCreateViewHolder和onBindViewHolder,onCreateViewHolder負責初始化View,而onBindViewHolder負責實現數據源和View的綁定,而以往這些工做都是在getView裏面。
這就是RecyclerView的Adapter要處理的工做,它甚至和ListView的BaseAdapter在功能上,並無實質的差別,惟一的區別就是咱們不再須要判斷convertView是否爲空,也不須要糾結是否要寫ViewHolder,而是必需要寫ViewHolder。
這就是框架的力量,框架提供了約束,正是爲了保證正確性,同時又幫咱們處理了不少實現細節,提供了便利性。
只要寫好Adapter,咱們就能夠和以往的ListView同樣,調用setAdapter就行。
單是看這個,RecyclerView自己並無太多的好處去誘惑咱們替換ListView,無非就是省事了,所以谷歌就爲RecyclerView提供了更增強大的特性:支持ListView和GridView的切換。
ListView和GridView在本質上來講,是AbsListView的兩種佈局形式,前者是線性佈局,然後者是網格狀佈局,這些在RecyclerView裏面,就是LayoutManager的配置,而RecyclerView的LayoutManager還提供了瀑布流的佈局形式,咱們只要在聲明的時候,指定聲明的是LinearLayoutManager(線性佈局),GridLayoutManager(網格佈局)或者StaggeredGridLayoutManager(瀑布流佈局)。
LayoutManager還提供了橫向滾動和垂直滾動等其餘設置。
RecyclerView還經過ItemTouchHelper類實現滑動或者拖曳刪除。
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback() { @Override public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { //actionState : action狀態類型,有三類 ACTION_STATE_DRAG (拖曳),ACTION_STATE_SWIPE(滑動),ACTION_STATE_IDLE(靜止) int dragFlags = makeFlag(ItemTouchHelper.ACTION_STATE_DRAG, ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT);//支持上下左右的拖曳 int swipeFlags = makeMovementFlags(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT);//表示支持左右的滑動 return makeMovementFlags(dragFlags, swipeFlags);//直接返回0表示不支持拖曳和滑動 } /** * @param recyclerView attach的RecyclerView * @param viewHolder 拖動的Item * @param target 放置Item的目標位置 * @return */ @Override public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { int fromPosition = viewHolder.getAdapterPosition();//要拖曳的位置 int toPosition = target.getAdapterPosition();//要放置的目標位置 Collections.swap(mData, fromPosition, toPosition);//作數據的交換 notifyItemMoved(fromPosition, toPosition); return true; } /** * @param viewHolder 滑動移除的Item * @param direction */ @Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { int position = viewHolder.getAdapterPosition();//獲取要滑動刪除的Item位置 mData.remove(position);//刪除數據 notifyItemRemoved(position); } }); itemTouchHelper.attachToRecyclerView(mRecyclerView);
RecyclerView甚至還提供了notifyItemChanged來實現單個item的局部刷新。
雖然RecyclerView提供了ListView不具有的不少實用功能,可是RecyclerView不能添加HeaderView和FooterView,也不能實現item的點擊事件,也沒有setEmptyView的方法,表面上來看,這彷佛很是難以想象,但若是認真看過ListView是如何添加HeaderView和FooterView,就會發現,ListView實際上是經過裝飾器的方式實現添加的,至於爲啥沒有onItemClick方法,那是由於RecyclerView自己並無實現相似的回調,這樣作的緣由是啥,就不得而知了。
撇開RecyclerView那些強大的功能,它和ListView的差異更可能是內置了ViewHolder,所以纔會在前文交代了ViewHolder的由來,而是否要使用RecyclerView,就看實際的需求了,若是隻是簡單的列表控件,ListView其實已經夠用了。