編輯:關於Android編程
ListView是Android中最常用的視圖之一,使用的頻率僅僅次於三大基礎布局,雖然由於使用性和擴展性等原因備受爭議,且盡管後來出現了RecyclerView的替代方案,但是ListView仍然廣泛地使用在我們的項目中。
自從ListView出道至今,已經不知道衍生出了多少問題,然而很多人只關心功能功能的實現,卻極少關注ListView過度調用導致的性能問題。在實際項目中,即使你正確使用了ViewHolder機制來優化ListView性能,但是在某些場景下依然會感覺卡頓嚴重,到底是什麼原因導致的呢,我們來分析下。
1、問題演示
很多時候,我們在使用ListView的時候,都是隨手寫上一個layout_height=”wrap_content”或者layout_height=”match_parent”,非常常規的寫法,乍一看,並沒有什麼問題,尤其是功能實現上也是無可挑剔。
然而,就是layout_height=”wrap_content”這個屬性是導致嚴重的性能問題的根源,下面以一個簡單的例子說明一下:
布局如上,接下來,假設ListView一共有5項,那麼顯示邏輯代碼如下:
下面,我們來看看log打印的情況:
數一數,一個是15次getView調用,其中6次convertView為null,剩余9次convertView為復用,而ListView的數據源真正只有5項!
當然,為了場景的簡單化,我們先不考慮ListView內容超過一屏幕的情況(也就是不考慮其復用機制),所以我們期待的情況應該是getView調用5次且convertView全部為null,而事實上getView多調用了10次且有一次convertView為null。
同樣的,我們測試一下當layout_height=”match_parent”的情況:
另外,ListView內容超過一屏幕的情況下(考慮復用機制),測試結果一樣,這裡就不再演示了。
在實際項目中,Adapter的getView方法承載著大量的業務邏輯,在性能方面,除去創建視圖的損耗,不正確的ListView使用方式導致的性能損耗大約是正常的3倍左右!那麼到底是什麼原因導致的呢?我們下面來簡單分析下ListView源碼。
2、原因分析
在演示了layout_height=”wrap_content”導致性能問題的現象之後,我們來從源碼的角度分析下,出現這種過度調用問題的根本原因。(源碼以API 23為例)
首先,layout_height=”wrap_content”屬性意味著ListView的高度需要由子View決定,即在onMeasure的時候,需要一一測量子View的高度,所以我們先從其onMeasure方法入手。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { ... final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); ... if (heightMode == MeasureSpec.AT_MOST) { // TODO: after first layout we should maybe start at the first visible position, not 0 heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1); } ... }
了解View繪制原理的都知道wrap_content對應的mode為MeasureSpec.AT_MOST,所以很容易就能找打測量子視圖高度的代碼measureHeightOfChildren,當然方法名也體現出來了,所以具體來看這個方法。
final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition, int maxHeight, int disallowPartialChildPosition) { ... endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition; int i; View child; final boolean[] isScrap = new boolean[1]; ... for (i = startPosition; i <= endPosition; ++i) { child = obtainView(i, isScrap); measureScrapChild(child, i, widthMeasureSpec, maxHeight); ... recycleBin.addScrapView(child, -1); ... returnedHeight += child.getMeasuredHeight(); if (returnedHeight >= maxHeight) { return ...; } } ... }
核心代碼如上,很明顯,所有的子View實例都是由obtainView方法返回的,然後再調用具體measureScrapChild來具體測量子View的高度,正常情況下這裡for循環的次數就等於所有子項的個數,不過特殊的是已測量的子View高度之和大於maxHeight就直接return出循環了。這種做法其實很好理解,ListView能顯示的最大高度就是屏幕的高度,如果有1000個子項,前面10項已經占滿了一屏幕了,那後面的990項就沒必要繼續測量高度了,這樣可以大大提高性能。
另外,當一個子View測量完了之後,會通過recycleBin加到復用緩存之中,畢竟這個View只是測量了,還沒有加到視圖樹之中,完全是可以繼續復用的。
繼續來看obtainView方法的實現,源碼在AbsListView中。
View obtainView(int position, boolean[] isScrap) { ... final View scrapView = mRecycler.getScrapView(position); final View child = mAdapter.getView(position, scrapView, this); ... }
obtainView方法裡面核心的代碼其實就兩行,首先從復用緩存中取出一個可以復用的View,然後作為參傳入getView中,也就是convertView。
這時我們梳理一下measure過程中調用getView的全過程:
A、測量第0項的時候,convertView肯定是null的,通常需要我們Inflate一個View返回;
B、第0項測量結束,這個第0項的View就被加入到復用緩存當中了;
C、開始測量第1項,這時因為是有第0項的View緩存的,所以getView的參數convertView就是這個第0項的View緩存,然後重復B步驟添加到緩存,只不過這個View緩存還是第0項的View;
D、繼續測量3、4、5…項,重復C。
所以,我們log中的情況是position=0,convertView=null,而position 1,2 … convertView都是同一個對象實例,即被復用第0項。
當Measure過程結束了,下面就要開始Layout過程了,由於onLayout方法代碼較多,我們直接pass,來看makeAndAddView方法,也就是真真創建View的代碼。
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) { ... // Make a new view for this position, or convert an unused view if possible child = obtainView(position, mIsScrap); // This needs to be positioned and measured setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); ... }
同樣的,子View實例都是由obtainView方法返回的。這時候就有個小細節了,由於前面Measure的時候,第0項的View已經創建了並且加入到了復用緩存當中,這一次就可以直接拿出來繼續用了。接著創建第1,2 … 後面項的時候就沒復用緩存了,只能一次次地Inflate。
所以,我們log中的情況是position=0,convertView復用第0項,而position 1,2 … convertView=null。
按理說,Layout之後,應該就不會在調用getView方法了,但是我們明顯能看到log仍然多了5次調用,那麼這又是怎麼回事呢?
前面說到onMeasure方法會導致getView調用,而一個View的onMeasure方法調用時機並不是由自身決定,而是由其父視圖來決定。ListView放在FrameLayout和RelativeLayout中其onMeasure方法的調用次數是完全不同的。具體可以參考我之前的一篇博客:http://blog.csdn.net/megatronkings/article/details/52270461
由於onMeasure方法會多次被調用,例子中是兩次,其實完整的調用順序是onMeasure - onLayout - onMeasure - onLayout - onDraw。所以我們又會看到5次調用,和最前面5次是一模一樣的。
那麼,肯定有童鞋又要問,既然onLayout也被執行兩次,那為何不是調用5x2+5x2=20次呢?
這就涉及到makeAndAddView方法中一段關鍵的代碼:
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) { ... if (!mDataChanged) { // Try to use an existing view for this position child = mRecycler.getActiveView(position); if (child != null) { // Found it -- we're using an existing child // This just needs to be positioned setupChild(child, position, y, flow, childrenLeft, selected, true); return child; } } // Make a new view for this position, or convert an unused view if possible child = obtainView(position, mIsScrap); // This needs to be positioned and measured setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); ... }
在第2次onLayout的時候,由於數據並沒有變化,即mDataChanged=false,這時候可以直接用當前項已經存在的View了,不要再通過getView方法重新綁定數據,所以getView是不需要被調用的。
從上面的分析中,我們可以得到wrap_content情況下getView被調用的時機和次數,假設onMeasure(heightMeasureSpec為AT_MOST)次數為n,onLayout次數為m,ListView控件內同時顯示的子項數為i,那麼getView次數=(n + 1)* i,正常情況match_parent時,getView次數= i,多余的getView調用次數應該是 (n + 1)* i - i = n * i;
由公式可以看出getView多余調用次數與onMeasure次數n以及顯示子項數i成正比關系。如果從關系式看不出來這個值有多恐怖,那麼我下面來舉個項目裡的大坑你就明白了。
坑一:ListView高度為wrap_content放在RelativeLayout中
前面博客http://blog.csdn.net/megatronkings/article/details/52270461中分析到,RelativeLayout布局會使得子View的onMeasure周期翻倍調用。
比如4層嵌套的RelativeLayout會使得子View的onMeasure次數達到32,其中heightMeasureSpec為AT_MOST的次數為16,所以如果ListView同時顯示的項數為10,那麼getView的次數達到(16+1) * 10=170次,雖然只有10項,但是卻相當於一次性加載了170項,性能損耗之大可想而知。
下面,我們用實際的測試來驗證這個現象。
上面是4層RelativeLayout嵌套一個ListView,不要說實際項目中不可能出現這種情況,要知道布局嵌套有時其實非常隱蔽,稍不注意就這樣了。
為了方便查看日志,我們將ListView的Item數量設置成1項,log如下:
雖然onMeasure次數一共是32次,但實際上heightMeasureSpec是一次EXACTLY一次AT_MOST對半,所以導致getView調用的onMeasure次數為16次,加上onLayout的1次getView,總共恰好是17次,如日志所示。
可以總結出一個公式:如果RelativeLayout嵌套層數為n,ListView顯示項數為m,getView調用次數為(2^n+1)* m
坑二:ListView放在ScrollView等垂直滾動視圖中
從官方的設計來看,ListView其實是禁止防止在ScrollView等垂直滾動視圖中的,但無奈各種各樣的業務和設計導致我們不得不這麼做,然後就衍生出了可謂ListView歷史上最大的坑:NoScrollListView。
NoScrollListView是什麼呢?來看一段代碼你就明白了,估計絕大數多項目中都有。
public class NoScrollListview extends ListView { ... public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST); super.onMeasure(widthMeasureSpec, expandSpec); } ... }
NoScrollListview 出現的主要目的是為了支持ListView放在ScrollView等垂直滾動視圖中,原理很簡單,利用前面ListView測量原理分析到的機制,強行設置AT_MOST來測量子View高度,也就是強制ListView自適應,即使你在xml中正確地使用layout_height=”match_parent”,在Java代碼裡面也會強行設置成wrap_content,導致的結果就是每一次onMeasure都會不停調用getView。
如果,結合上前面說的RelativeLayout嵌套,ListView的性能損耗還要再翻倍!
假設ScrollView中存在RelativeLayout裡面嵌套NoScrollListview,RelativeLayout嵌套層數為n,那麼onMeasure的次數為2^n+2^(n+1)次,ListView顯示項數為m,getView調用次數為(2^n + 2^(n+1) +1)* m次。如果n=4,m=10,getView次數為490次!
相信看到這裡,終於知道為什麼ScrollView中嵌有列表的頁面會卡出翔了吧!
當然,事情還遠遠不止這麼簡單,尤其在某些特殊的場景下,容易導致onMeasure頻繁調用,以實際項目中遇到的問題場景舉兩個例子。
1、有些ScrollView具有下拉彈性功能,當手指下拉時會導致子View不停onMeasure,如果子View包含NoScrollListview,頁面肯定一頓一頓的。
2、如果你在getView中的某些不恰當的操作導致ListView重新onMeasure,比如setVisibility為Gone等,就會造成onMeasure和getView的相互循環調用,這時候性能消耗非常嚴重(一般不會ANR)。
3、同樣的,某些時候我們需要監聽ListView的滾動狀態,會使用setOnScrollListener,由於在onMeasure的時候會觸發OnScrollListener的回調,如果回調裡面某些不恰當的操作導致ListView再次觸發onMeasure就會導致OnScrollChangeListener和onMeasure兩者的死循環。
3、幾點建議
對於以上幾點坑和問題,有如下一些建議:
1、使用ListView的時候注意盡量使用layout_height=”match_parent”。
2、如果第1點無法避免,需要注意ListView的父布局,父布局以上絕對不要使用RelativeLayout,即使使用FrameLayout或LinearLayout會增加布局層級。
3、如果第1點無法避免,需要注意不要在getView中使用setVisibility這種會觸發ListView重新onMeasure的操作。
4、如果ListView存在位移,比如下來刷新等,絕對要遵循第1點來設置layout_height=”match_parent”,不然頻繁觸發onMeasure會導致交互卡頓。
5、關於NoScrollListView,這種布局是嚴禁使用的,無論是哪種場景,如果ScrollView中必須要使用ListView,可以使用SimulateListView控件代替ListView https://github.com/MegatronKing/SimulateListView
6、由於GridView的measure機制和ListView有些差別,雖然同樣會有性能損耗但不大,不過還是建議開發者遵循以上幾點!
ListFragment繼承於Fragment。因此它具有Fragment的特性,能夠作為activity中的一部分,目的也是為了使頁面設計更加靈活。相比Fragment
在第零篇文章簡單地介紹了JNI編程的模式之後,後面兩三篇文章,我們又針對JNI中的一些概念做了一些簡單的介紹,也不知道我到底說的清楚沒有,但相信很多童鞋跟我一樣,在剛開始
概述Android Settings模塊說簡單也簡單,說難也難,裡面涉及到的知識點也挺多的。我們知道Settings主要是用於配置一些系統選項或屬性值,通過修改設置項就能
0x00我們首先講一個webView這個方法的作用:webView.getSettings().setAllowFileAccessFromFileURLs(false)