轉載請注明出處:http://blog.csdn.net/wl9739/article/details/57416433
在開發過程中,往往會聽到 “性能優化” 這個概念,這個概念很大,比如網絡性能優化、耗電量優化等等,對我們開發者而言,最容易做的,或者是影響最大的,應該是 View 的性能優化。一般小項目或許用不上 View 性能優化,然而,當業務愈加龐大、界面愈加復雜的時候,沒有一個良好的開發習慣和 View 布局優化常識,做出來的界面很容易出現 “卡頓” 現象,從而嚴重影響用戶體驗。而對于我們開發者來說,了解一些 View 性能優化的常識,增強開發技巧,可以說是一門必備的功課。
為了更好地理解 View 性能優化的原理,以及造成 “卡頓” 的可能原因,我們從 View 的繪制流程開始討論。之后,會介紹一些寫界面布局常用的一些標簽及使用注意事項。
我們都知道,View 的繪制分為三個階段:測量、布局和繪制,這三個階段各自的作用如下:
measure: 為整個 View 樹計算實際的大小,即設置實際的高(對應屬性:mMeasureHeight)和寬(對應屬性:mMeasureWidth),每個 View 的控件的實際寬高都是由父視圖和本身視圖所決定的。layout:為將整個根據子視圖的大小以及布局參數將 View 樹放到合適的位置上。draw:利用前兩部得到的參數,將視圖顯示在屏幕上。當一個 Activity 對象被創建完成之后,會將一個 DecorView 對象添加到 Window 中,同時會創建一個 ViewRootImpl 對象,并將 ViewRootImpl 對象和 DecorView 對象建立聯系,然后繪制流程就會從 ViewGroup 的 performTraversals() 方法開始執行,如下圖所示:

整個繪制流程從 ViewRootImpl 的 performTraversals() 方法開始,在該方法內會調用 performMeasure() 方法進行測量子 View(也就是根 View,頂級的 ViewGroup)。然后在 performMeasure 中會調用 measure() 方法來執行具體的測量邏輯,這個時候,代碼邏輯就從 ViewRootImp 跳轉到了 View 類中了:
在 measure() 方法中,有一個 onMeasure() 方法,用于這個方法用來測量子元素的大小,也將測量流程從父元素傳遞到子元素當中去。緊接著子元素會重復父元素的測量流程,如此反復,就完成了一顆 View 樹的遍歷。當 measure() 方法完成后,會將結果存儲在 LongSparseLongArray 類型的變量 mMeasureCache 中。
在 performTraversals() 方法中,調用完 performMeasure(),后,會接著調用 performLayout() 和 performDraw() 進行 View 的布局和繪制。這兩個流程和測量的流程差不多,就不再敘述。
而這三個階段分別作了什么呢?源碼太長就不貼了,主要的作用如下:
Measure 過程
設置本 View 視圖的最終大小。如果該 View 對象是個 ViewGroup 類型,需要重寫該 onMeasure() 方法,對其子視圖進行遍歷 measure() 過程。 measureChildren(),內部使用了一個 for 循環對子視圖進行遍歷,分別調用了子視圖的 measure() 方法。measureChild(),為指定的子視圖 measure,會被 measureChildren 調用。measureChildWidthMargins(),為指定的子視圖考慮了 margin 和 padding 的 measure。Layout 過程
layout() 方法會設置該 View 視圖位于父視圖的坐標軸,即 mLeft, mTop, mRight, mBottom.(調用 setFrame() 方法去實現),接下來回調 onLayout() 方法(如果該 View 是 ViewGroup 對象,需要實現該方法,對每個視圖進行布局);如果該 View 是個 ViewGroup 類型,需要遍歷每個子視圖 childView。調用該子視圖的 layout() 方法去設置它的坐標值。Draw 過程
繪制背景如果要視圖顯示漸變框,這里會做一些準備工作繪制視圖本身,即調用 onDraw() 方法。在 view 中,onDraw() 是個空方法,也就是說具體的視圖都啊喲覆蓋該方法來實現自己的顯示(比如 TextView 在這里實現了繪制文字的過程)。而對于 ViewGroup 則不需要實現該方法,因為作為容器是沒有內容的,其包含了多個子 View,而子 View 已經實現了自己的繪制方法,因此只需要告訴子 View 繪制自己就行了,也就是下面的 dispatchDraw() 方法。繪制視圖,即 dispatchDraw() 方法。在 View 中這是個空方法,具體的視圖不需要實現該方法,它是專門為容器類準備的,也就是容器必須實現該方法。如果需要,開始繪制漸變框。繪制滾動條。因此,如果我們去掉不必要的背景,去掉漸變框,去掉滾動條,在一定程度上是能加快繪制速度的。
幀率(frame per second,即 FPS),指的是每秒刷新的次數。一般電影的幀率為 24FPS、25FPS 和 30FPS。而游戲的幀率一般要保持 60FPS 才能叫做流暢,當游戲的 FPS 低于 30 時,我們就會感受到明顯地卡頓。Android 系統每隔 16ms 觸發一次 UI 刷新操作,這就要求我們的應用都能在 16ms 內繪制完成。如果有一次的界面繪制用了 22ms,那么,用戶在 32ms 內看見的都是同一個界面。情況嚴重的就會讓用戶感受到應用運行”卡頓“。
因此,優化的目的,主要就是減少繪制時間,盡量保證每個界面都能在 16ms 內完成繪制。而優化的方案,從上面的分析,我們可以分兩個方面:

從內優化
減少 View 層級。這樣會加快 View 的循環遍歷過程。去除不必要的背景。由于 在 draw 的步驟中,會單獨繪制背景。因此去除不必要的背景會加快 View 的繪制。盡可能少的使用 margin、padding。在測量和布局的過程中,對有 margin 和 padding 的 View 會進行單獨的處理步驟,這樣會花費時間。我們可以在父 View 中設置 margin 和 padding,從而避免在子 View 中每個單獨設置和配置。去除不必要的 scrollbar。這樣能減少 draw 的流程。慎用漸變。能減少 draw 的流程。從外優化
布局嵌套過于復雜。這會直接 View 的層級變多。View 的過渡繪制。View 的頻繁重新渲染。UI 線程中進行耗時操作。在 Android 4.0 之后,不允許在 UI 線程做網絡操作。冗余資源及錯誤邏輯導致加載和執行緩慢。簡單的說,就是代碼寫的爛。頻繁觸發 GC,導致渲染受阻。當系統在短時間內有大量對象銷毀,會造成內存抖動,頻繁觸發 GC 線程,而 GC 線程的優先級高于 UI 線程,因而會造成渲染受阻。外部因素最為致命!日常開發中更多的應該關心布局的嵌套層級和冗余資源。
比如,當需要將一個 TextView 和一張圖片放在一起展示時,我們可以考慮使用 TextView 的 drawableLeft(drawableRight、drawableTop、drawableBottom) 屬性來設置圖片,而不是使用一個 LinearLayout 來將 TextView 和 ImageView 封裝在一起,這樣就能減少 View 的繪制層級。
又比如,子元素和父元素都是相同的背景時,就不必在每個子元素中都添加背景屬性,等等。
線性布局和相對布局是我們平時使用最多的布局方式。在一般開發場景中,兩者的渲染效率沒有明顯差別,但是如果真要較真的話,他們之間還是有細微差別的。
RelativeLayout 在測量子 View 排列方式是基于彼此的依賴關系,這種依賴關系導致了子 View 的顯示順序不一定和布局中的 View 的順序相同,在確定所有子 View 的時候,會先對所有的 View 進行排序,同時,由于 RelativeLayout 允許 “A在橫向上依賴于 B,B 在縱向上依賴于 A“,因此會測量兩次,導致測量效率較低。而 LinearLayout 由于有 orientation 屬性,則測量就很簡單了。
LinearLayout 在設置 weight 屬性的時候,也會導致二次測量:首先會遍歷測量沒有 weight 屬性的 View,然后再遍歷測量包含 weight 屬性的 View。
布局比較

選擇布局容器的基本準則:
盡可能的使用 RelativeLayout 以減少 View 層級,使 View 樹趨于扁平化。在不影響層級深度的情況下,使用 LinearLayout 和 FrameLayout 而不是 RelativeLayout。說到布局標簽,我想大概很多人都用過一些。為了說明 Android 系統對于這些標簽的處理,我們先看一下 xml 布局是如何解析繪制到屏幕的。
在 Activity 的 onCreate() 方法中,我們一般會調用 setContentView() 方法,這個方法負責將 XML 文件解析繪制到屏幕上,這個方法很簡單:
這個方法第一行是調用 Window 類的 setContentView(),第二行是初始化 ActionBar。Window 類是一個抽象類,它是所有視圖相關類的頂層類,其唯一一個實現類是 PhoneWindow,在 PhoneWindow 類的 setContentView() 方法中,會先移除掉所有的 view 視圖,然后再調用 LayoutInflater.inflate() 方法繪制,在 LayoutInflater 的 inflate() 方法中,會創建一個 XmlResourceParser 解析器,然后再進行解析。我們來看看 inflate() 方法里面干了什么:
為了方便閱讀,我將一些代碼省略掉,從上面可以看出大致的解析流程:先判斷是否有 merge 標簽,然后檢查其合理性,注意源碼已經說明了,merge 標簽只能用在 ViewGroup 的根布局中,并且 attachToRoot 必須要設置為 true。然后調用 rInflate() 方法;如果沒有 merge 標簽,就會調用 rInflateChildren() 方法生成子元素的布局,而這個 rInflateChildren() 方法最終也是輾轉到前面的 rInflate() 方法中,我們來看一下這個方法:
這里面一共涉及到了四個標簽:<requestFocus/>,<tag/>,<include/> 和 <merge/>。下面來分別說一下:
requestFocus 標簽就是讓標簽內的 View 獲取焦點,其內部就是使用 view.requestFocus() 方法實現的。
tag 標簽是 API 21 里面新增的,用來給 View 對象添加額外的信息。從 Android 1.0 開始,Android 就開始支持給 View 對象調用 setTag(Object) 和 getTag(Object) 來添加和獲取標簽信息,到了 Android 1.6 ,添加和獲取標簽信息有了新的方法:setTag(int, Object)。而在 Android 4.0 之后,setTag(int, Object) 的內部實現改為非靜態的 SparseArray 來實現。Android 5.0 的時候,提供了一種全新的寫法,就是 tag 標簽。
舉一個例子來說明這個標簽怎么用,先編寫一個 XML 布局文件:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <Button android:id="@+id/btn_negative" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="cancel"> <tag android:id="@+id/btn_state_negative" android:value="@string/btn_state_negative" /> </Button> <Button android:id="@+id/btn_positive" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="ok"> <tag android:id="@+id/btn_state_positive" android:value="@string/btn_state_positive" /> </Button></LinearLayout>然后我們就可以通過下面的方式獲取標簽信息:
Button btn_negative = (Button) findViewById(R.id.btn_negative);String tag = (String) btn_negative.getTag(R.id.btn_state_negative);tag 標簽是這四個標簽中唯一一個需要指定 id 屬性的!
我們來看看 處理 include 標簽的方法 parseInclude() 里面的邏輯:
在 parseInclude() 方法里面會判斷是否需要處理 merge 標簽,然后根據標簽名(如 Button、TextView 等)調用 createViewFromTag() 方法創建一個 view 對象,然后生成該對象的布局參數,設置 id 屬性,設置可見性等等。
然后注意到 createViewFromTag(),顧名思義,該方法會根據 XML 的標簽來創建 View 對象,這個方法里面最終會調用到 createView() 方法,是使用反射來創建 View 對象的具體實現。
有個問題不知道大家注意到沒有,這些 id、可見性等等的屬性都是 view 對象的,而 include 標簽和 merge 標簽并并沒有這些屬性,也就是說,如果你在 include 或 merge 標簽中設置了一個 id,然后在代碼中通過 findViewById() 方法企圖找到這個 include 或 merge 的布局,是會報空指針異常的!
我這里為了方便區分,將 include、merge 等標簽稱為“布局標簽”,它們不能創建為 View 對象,設置 id 屬性對它們沒有意義。而將 XML 中的 Button、TextView 等標簽稱為“視圖標簽”(視圖元素、控件等),因為它們能被創建為 View 對象,可以設置 id 等熟悉。
除了上面介紹的 merge、include、requestFocus 和 tag 等布局標簽外,還有如下常用的 View 標簽:
ViewStub
利用 ViewStub 標簽可以讓布局懶加載。當你界面要顯示很多內容,而其中一些不用立即顯示出來的時候(比如商品詳情、下載進度條等等),可以使用 ViewStub 標簽,讓 ViewStub 引用這些不用立馬加載的界面布局,當需要的時候再讓它們加載出來。ViewStub 雖然是 View 標簽,但是其本身沒有大小,不會繪制任何東西,因此是一個非常輕量的 View 標簽。
使用 ViewStub 和 include 標簽類似,需要使用 android:layout 屬性來確定哪些布局需要懶加載,同時,由于 ViewStub 是一個 View 標簽,因此需要使用一個 id 來操作 ViewStub。比如使用 ViewStub 簡單的 XML 如下:
在 java 代碼中,對 ViewStub 的操作有兩種方式:
設置 View 的可見性
findViewById(R.id.stab_view)).setVisibility(View.VISIBLE);調用 ViewStub 的 inflate() 方法
上面兩種方法都可以加載由 ViewStub 引用的布局。使用 ViewStub 有兩點需要注意:
當調用了inflate() 方法后,ViewStub 標簽就從視圖中移除了,也就是說,inflate() 方法不能對同一個 ViewStub 調用兩次。ViewStub 所引用的布局的根標簽不能為 標簽。Space
這是是一個空控件,該 View 沒有實現 onDraw() 方法,因此繪制效率比較高。該控件可以用來占用空白(比如代替 padding 和 margin)。
大概差不多了,View 的性能優化還有一些沒有介紹,比如 Overdraw 等,這里就給個鏈接吧:OverDraw
同時,對于 View 性能優化有興趣的同學,歡迎參加視頻課程:有心課堂
新聞熱點
疑難解答