文章目錄
前言
Android的性能優化是一個持續持續的過程,以發現問題、解決問題或者是組織Code Review爲推動力去實施。性能優化涉及到的方面很多,比如啓動優化、卡頓優化、內存優化、界面佈局優化、穩定性優化、耗電優化、安裝包大小優化等等。性能優化是每個開發者都需要關注的功課,本文從界面佈局優化做一個總結。
在查閱大量相關資料後,對界面優化的在此做個總結。本文會介紹一下卡頓產生原因、什麼是過度繪製和渲染機制,然後介紹如何定位問題和解決問題,最後會總結出在實際開發過程中的使用建議。如有不足之處,歡迎提出寶貴建議。
一、卡頓原因
一個App的用戶體驗好不好,是否流暢不卡頓是一個很直觀的感受。導致Android卡頓場景的原因有很多,比如界面繪製、應用啓動、頁面跳轉、事件響應等等。
這幾種卡頓場景的根本原因可以分爲兩大類:
- 界面繪製。主要原因是繪製的層級深、頁面複雜、刷新不合理,由於這些原因導致卡頓的場景更多出現在 UI 和啓動後的初始界面以及跳轉到頁面的繪製上。
- 數據處理。導致這種卡頓場景的原因是數據處理量太大,一般分爲三種情況,一是數據在處理 UI 線程,二是數據處理佔用 CPU 高,導致主線程拿不到時間片,三是內存增加導致 GC 頻繁,從而引起卡頓。
引起卡頓的原因很多,但不管怎麼樣的原因和場景,最終都是通過設備屏幕上顯示來達到用戶,歸根到底就是顯示有問題,所以,要解決卡頓,就要先了解 Android 系統的顯示原理。
二、Android 系統的顯示原理
Android 顯示過程可以簡單概括爲:Android 應用程序把經過測量、佈局、繪製後的 surface 緩存數據,通過 SurfaceFlinger
把數據渲染到顯示屏幕上, 通過 Android 的刷新機制來刷新數據。也就是說應用層負責繪製,系統層負責渲染,通過進程間通信把應用層需要繪製的數據傳遞到系統層服務,系統層服務通過刷新機制把數據更新到屏幕上。
我們都知道在 Android 的每個 View 繪製中有三個核心步驟:Measure
、Layout
、Draw
。具體實現是從 ViewRootImp
類的performTraversals()
方法開始執行,Measure
和 Layout
都是通過遞歸來獲取 View
的大小和位置,並且以深度作爲優先級,可以看出層級越深、元素越多、耗時也就越長。
三、什麼是渲染機制
渲染操作通常依賴於兩個核心組件:CPU與GPU。CPU負責包括Measure,Layout,Record,Execute的計算操作,GPU負責Rasterization(柵格化)操作。CPU通常存在的問題的原因是存在非必需的視圖組件,它不僅僅會帶來重複的計算操作,而且還會佔用額外的GPU資源。
從上圖可以得出結論:
- CPU產生的問題:不必要的佈局和失效(Layouts、Invalidations)
- GPU產生的問題:過度繪製(overdraw)
小結: 瞭解渲染機制,有助於我們瞭解卡頓的最終原因,方便找到解決問題的方向。
爲了能夠使得App流暢,我們需要在每幀16ms以內處理完所有的CPU與GPU的計算,繪製,渲染等等操作。
接下來介紹什麼是“過度繪製”,避免了過度繪製,界面優化也就做到了。
四、什麼是過度繪製(定位問題)
過度繪製(Overdraw)描述的是屏幕上的某個像素在同一幀的時間內被繪製了多次。在多層次重疊的 UI 結構裏面,如果不可見的 UI 也在做繪製的操作,會導致某些像素區域被繪製了多次,同時也會浪費大量的 CPU 以及 GPU 資源。
在Android開發人員選項中,找到“調試GPU過度繪製”,開啓顯示之後,手機會顯示出藍色、綠色、紅色等色塊。
其中藍色、綠色、紅色顯示的就是過度繪製的區域。
在官網的 Debug GPU Overdraw Walkthrough 說明中對過度重繪做了簡單的介紹,其中屏幕上顯示不同色塊的具體含義如下所示:
每個顏色的說明如下:
- 原色:沒有過度繪製(正常的繪製,只繪製了1次)
- 藍色:1 次過度繪製 (超過1次繪製是會顯示)
- 綠色:2 次過度繪製
- 粉色:3 次過度繪製
- 紅色:4 次及以上過度繪製
我們優化的目標是,減少紅色,看到更多的藍色。
五、優化方法(解決問題)
優化原則:減少佈局層級、減少過度繪製、佈局複用
下面結合項目中的實際使用情況做的優化,同時也在Code Review後發現的做一個總結,Code Review時結合Android studio中的工具檢測到的一些佈局優化建議提示。(注:記一次CodeReview實例,請點擊前往)
1.移除默認的 Window 背景
一般應用默認繼承的主題都會有一個默認的 windowBackground
,比如默認的AppTheme
主題:
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!--TODO:移除主題背景色,減少一次界面過度繪製-->
<item name="android:windowBackground">@android:color/white</item>
</style>
但是一般界面都會自己設置界面的背景顏色或者列表頁則由 item 的背景來決定,所以默認的 Window 背景基本用不上,如果不移除就會導致所有界面都多 1 次繪製。
可以在應用的主題中添加如下的一行屬性來移除默認的 Window 背景:
<item name="android:windowBackground">@android:color/transparent</item>
<!-- 或者 -->
<item name="android:windowBackground">@null</item>
或者在 BaseActivity
的 onCreate()
方法中使用下面的代碼移除:
getWindow().setBackgroundDrawable(null);
// 或者
getWindow().setBackgroundDrawableResource(android.R.color.transparent);
移除默認的 Window 背景的工作在項目初期做最好,因爲有可能有的界面未設置背景色,這就會導致該界面顯示成黑色的背景,如下圖所示,如果是後期移除的,就需要檢查移除默認 Window 背景之後的界面是否顯示正常。
原先的系統主題是白色,移除後就變爲黑色,此時渲染的顏色也變了,減少一次過度繪製。只是這時需要在子佈局中添加相應的背景色即可。
2.移除XML佈局文件中不必需的背景
例如在佈局文件中嵌套了RecyclerView,注意在item中需要用到背景色時再考慮添加背景色,這樣可以減少一次過度繪製。簡單的佈局出現顏色上出現了過度繪製,可以先好排查是否在xml中或者代碼中調用了多餘的繪製。
3.自定義控件使用 clipRect()
和 quickReject()
優化
當某些控件不可見時,如果還繼續繪製更新該控件,就會導致過度繪製。但是通過 Canvas clipRect()
方法可以設置需要繪製的區域,當某個控件或者 View 的部分區域不可見時,就可以減少過度繪製。
先看一下 clipRect()
方法的說明:
Intersect the current clip with the specified rectangle, which is expressed in local coordinates.
顧名思義就是給 Canvas 設置一個裁剪區,只有在這個裁剪矩形區域內的纔會被繪製,區域之外的都不繪製。
這個項目中暫時沒有用到,先記錄於此,後期用到了再完善使用注意細節。
可以參考一下其他人總結的使用方式:Android性能優化之渲染篇
4.使用合理高效的佈局
(1)RecyclerView中的item的分割線處理
一般的寫法如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="8dp"
android:background="@color/divider_gray">
<LinearLayout
android:padding="@dimen/mid_dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/white">
<ImageView
android:id="@+id/iv_app_icon"
android:layout_width="40dp"
android:layout_height="40dp"
tools:src="@mipmap/ic_launcher"/>
<TextView
android:id="@+id/tv_app_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="@dimen/mid_dp"
android:textColor="@color/text_gray_main"
android:textSize="@dimen/mid_sp"
tools:text="test"/>
</LinearLayout>
</LinearLayout>
這種改變佈局實現分割線的方式雖然很快捷方便,但是存在不少問題的:
(1)加深了佈局層級,和之前的佈局相比多了一級
(2)多了 2 次過度繪製
解決方式有兩種:
- 一種是使用
RelativeLayout
將分割線添加在 item 的佈局中,但是這樣會導致佈局複雜度增加,同時因爲RelativeLayout
佈局的兩次測量,也會延長 View 測量的時間,在解決這種需求時並不是一個好的方式。 - 另一種是使用
RecyclerView
的addItemDecoration(ItemDecoration decor)
方法添加分割線,這種方式在你自定義好一個分割線ItemDecoration
時是很方便的,網上有很多關於這方面的例子(如果你使用 ListView 的話,則使用setDivider(Drawable divider)
方法)。
我們採用第二種解決方法,優化前後的對比如下:
優化後的佈局 ImageView 和 item 背景區域均比優化前少了 2 次過度重繪,佈局層級也沒增加,需求也實現了。
(2)使用TextView本身的屬性同時顯示圖片和文字
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/batman"
android:drawableLeft="@drawable/batman"
android:drawableStart="@drawable/batman"
android:drawablePadding="5dp">
</TextView>
在界面中有圖片和文字的佈局時,不一定要用LinearLayout或RelativeLayout來嵌套,直接用TextVeiw也能實現,這樣減少一層嵌套,也更優雅。
(3)用LinearLayout自帶的分割線
分割線在App經常會用到的,使用頻率高到讓你驚訝。但是LinearLayout有一個屬性可以幫你添加分割線。下面的例子中,LinearLayout包含2個TextView和基於他們中間的分割線。
1.創建分割線(用shape實現)
下面是一個簡單的shape divider_horizontal.xml用來當做分割線。
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<size android:width="@dimen/divider_width"/>
<solid android:color="@color/colorPrimaryDark"/>
</shape>
2.將分割線放到佈局屬性divider
中
<?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"
android:divider="@drawable/divider_horizontal" //添加分割線圖片
android:dividerPadding="5dp" //設置padding
android:showDividers="middle"> //居中顯示
<TextView android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/batman"/>
<TextView android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="@string/superman"/>
</LinearLayout>
上面用到了三個xml
屬性:
-
divider
-用來定義一個drawable
或者color
作爲分割線 -
showDividers
-設置分隔線的顯示位置,有四個flag,分別是:begining
(開始位置),end
(結束位置),middle
(中間,最常見的),none
(不顯示,也是默認值) -
dividerPadding
-給divider
添加padding
注:
RadioGroup
繼續自LinearLayout
,同時也具有上述屬性。<RadioGroup android:id="@+id/rgSize" android:layout_width="wrap_content" android:layout_height="wrap_content" android:divider="@drawable/shape_space" android:showDividers="middle" android:orientation="horizontal" > </RadioGroup>
shape_space.xml
中<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <solid android:color="@android:color/transparent" /> <stroke android:width="0dp" android:color="@android:color/transparent" /> <size android:height="8dp" android:width="8dp" /> </shape>
實現效果爲:
六、總結
最後總結一下在實際開發中 ,在寫佈局界面時的建議:
-
使用合適的佈局
三種常見的
ViewGroup
的繪製速度:FrameLayout
>LinerLayout
>RelativeLayout
。-
ConstraintLayout
是一個更高性能的消滅佈局層級的神器 -
RelativeLayout
會讓子View調用2次onMeasure
,LinearLayout
在有weight時,也會調用子View
2次onMeasure
-
RelativeLayout
的子View如果高度和RelativeLayout
不同,則會引發效率問題,當子View很複雜時,這個問題會更加嚴重。如果可以,儘量使用padding代替margin。 -
在不影響層級深度的情況下,使用
LinearLayout
和FrameLayout
而不是RelativeLayout
。 -
RecycleView
中item 一般用ConstraintLayout
或直接使用控件來佈局,以業務需求爲準。 -
簡單佈局一般用
FrameLayout
來佈局,同時結合include、merge來使用。佈局文件都要有根節點,但android中的佈局嵌套過多會造成性能問題,於是在使用include嵌套的時候我們可以使用merge作爲根節點,這樣可以減少佈局嵌套,提高顯示速率。
小結:使用佈局優先級:
FrameLayout>ConstraintLayout>LinearLayout>RelativeLayout
,結合效率和需求實現。 -
-
儘量減少使用
wrap_content
,推薦使用mathch_parent
或固定尺寸配合gravity="center"
因爲 在測量過程中,
match_parent
和固定寬高度對應EXACTLY
,而wrap_content
對應AT_MOST
,這兩者對比AT_MOST
耗時較多。 -
在需要的地方添加渲染背景,外層不渲染,在內層需要的地方渲染。
-
文本控件,需要考慮文本過長時的省略策略
-
切圖至少提供兩套,
xhdpi
和xxhdpi
-
消除佈局警告,同時刪除控件中的無用屬性
-
對於只有在某些條件下才展示出來的組件,建議使用
viewStub
包裹起來,include 某佈局如果其根佈局和引入他的父佈局一致,建議使用merge包裹起來,如果你擔心preview效果問題,這裏完全沒有必要,可以tools:showIn=""
屬性,這樣就可以正常展示preview
了。 -
歡迎補充。
參考資料:
5.Android開發之merge結合include優化佈局