上一篇流暢度概念向大家詳細地描述了VSync機制和Choreographer編舞者的用法。可能所講解的內容偏向理論概念,因此這篇是流暢度優化實操,整篇主要分三層,UI層、程式碼邏輯層、IO層來講述各個優化點,其中還會穿插多個輔助檢測外掛。可謂乾貨滿滿,希望對大家有用。
最基本的UI層顯示優化
- 除錯GPU過度渲染
在App出現卡頓的時候,我們第一時間會想到我們的App是不是存在過度繪製的問題。為什麼要先看過度繪製的問題,因為直接直觀方便啊,在每一臺手機的開發者選項裡中開啟顯示過度繪製區域,通過顏色我們就能辨別我們的App是不是存在過度繪製的問題。 可能存在一部分的測試甚至開發的同學不知道什麼是過度繪製?過度繪製指的是在螢幕一個畫素上繪製多次(超過一次),例如一個有背景的TextView,那顯示文字的那個畫素至少繪製了兩次,一次是文字,一次是背景。 過度繪製顯示的各種顏色所示含義如下:
Overdraw倍數 | 畫素點繪製次數 | 可接受區域 | |
---|---|---|---|
無色 | 0X | 1 | 全屏 |
藍色 | 1X | 2 | 大部分 |
綠色 | 2X | 3 | 區域性 |
淡紅色 | 3X | 4 | 小部分 |
深紅色 | 4X | ≥5 | 無 |
現在大家可以看一下自己的專案,找一個你認為佈局稍微複雜的介面,然後在開發者選項開啟顯示過度繪製區域
通過顏色的判斷,我們檢查對應的佈局程式碼來優化過度繪製問題。
- Tracer for OpenGL ES
針對上面我們看到的過度繪製的區域,我們要想一想應該怎麼去優化,但這個時候我們並不太清楚這個過度繪製的區域是怎麼形成的,所以我們要藉助另外一個工具Tracer for OpenGL ES,它可以記錄和分析app每一幀的繪製過程,以及列出所有用到OpenGL ES的繪製函式和耗時,所以通過Tracer for OpenGL ES我們可以很容易的看出app的每一幀是怎麼畫出來的。
簡要使用步驟:
- 連線真機,在AndroidStudio中開啟Android Device Monitor,接著Window-> Open Perspective -> Tracer for OpenGL ES。
- 如下圖操作,點選捕捉跟蹤按鈕,然後輸入對應的資訊,點選跟蹤。(華為P10,MI5這兩款無法正常跟蹤,最後使用的是華為Mate7)
- 點"Stop Tracing"結束,Trace log檔案就會生成在預定的目錄下。
- 結束後,Trace檔案自動開啟,如下圖介紹,我們點選glDraw函式欄,在右上方看到當前繪製函式所繪製的影象。
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white">
<Button
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/background_dark"
android:text="Button"
android:textColor="@android:color/white" />
</android.support.design.widget.CoordinatorLayout>
複製程式碼
通過剛剛的演示,我們就可以用到了Tracer for OpenGL ES來查詢過度繪製的地方來。
小插曲:開啟自己mac的AndroidStudio3.1時,竟然一時找不到DDMS,查閱資料發現DDMS在AndroidStudio3.1已經不推薦使用了,只能用回公司AndroidStudio3.0截圖,這裡為以後的優化工具文章留下一個伏筆。
- Hierarchy Viewer
接著我們介紹一下Hierarchy Viewer,通過它我們可以查詢佈局不合理的地方,Hierarchy Viewer的使用方法較為簡單,AndroidStudio中,同樣通過Android Device Monitor,接著Window-> Open Perspective -> Hierarchy Viewer。通過Hierarchy Viewer可以看到我們開啟的Activity的UI Tree情況。(注意:我們用模擬器作為例子,先用模擬器執行開啟你的應用,再開啟Hierarchy Viewer皮膚。)
拿到UI Tree之後,我們主要分析以下三個問題:(我用一個簡單的Demo來分析)
【問題1】沒有用的父佈局
使用Hierarchy Viewer檢視我們的UI Tree,如發現紅框的RelativeLayout是CustomTestView唯一子View,我們可以看看是否能把RelativeLayout子View又放到CustomTestView裡,這樣就可以把RelativeLayout這一層去掉,通過檢視程式碼,我們發現其實RelativeLayout這一層是多餘的,我們直接通過merge標籤把RelativeLayout和CustomTestView合併.(這種情況在自定義View非常常見)
【問題2】某種情況才會使用的UI被設定成View.GONE 我們在開發應用程式的時候,經常會遇到這種情況,會在執行時動態根據條件來決定顯示哪一個View或者ViewGroup,把最先要顯示的放在第一位顯示,不是第一時間要顯示的暫時設定為View.GONE。這樣的做法優點是邏輯簡單,而且控制起來非常的方便,但是缺點是會消耗資源,雖然把View或者ViewGroup的初始可見設為View.GONE,但是在Inflate佈局的時候,View還是被Inflate,也就是說仍然會建立物件,會被例項化,會被設定屬性,也就是說會消耗記憶體等資源的。官方推薦的做法是使用ViewStub,ViewStub是一個輕量級的View,他是一個使用資源非常小的控制元件。(如果不明白設定成GONE,仍然會消耗資源的同學,可以通過關於View的建立與ViewStub的原始碼分析進行理解)在我們的程式碼中,錯誤頁面ErrorView經常會出現這種情況。
【問題3】使用LinearLayout排版導致佈局層次加深 從下圖可以發現,下面佈局是用兩個LinearLayout巢狀實現的,但是通過使用一個RelativeLayout我們可以實現同樣的效果,這樣就可以減少一個層次,從圖中可以很明顯的看出優化效果。
- 移除或修改Window預設的Background 我們通常在設定通用Theme時候,都用設定一個預設背景色,作為應用的基礎色
<style name="AppTheme.Base" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">@color/app_status_bar_bg_color</item>
<item name="colorPrimaryDark">@color/app_status_bar_bg_color</item>
<item name="android:textColorPrimary">@android:color/black</item>
<item name="android:windowBackground">@color/app_frame_bg_color</item>
</style>
複製程式碼
但是在佈局頁面,設計人員設計的底色,根本不是預設的背景色,如果我們在這個頁面的根佈局再設一個背景的話就是多繪製一層背景。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"><!-- 會導致過度繪製的寫法 -->
***
</RelativeLayout>
複製程式碼
這種情況,我們可以這樣處理:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().setBackgroundDrawableResource(android.R.color.white);
setContentView(R.layout.activity_main);
***
}
複製程式碼
這樣修改佈局的背景色,我們可以避免出現過度繪製的情況。另外上面的設定背景程式碼,要注意書寫順序,這裡可包含了不少View的建立的知識,有興趣的同學可以自行查閱。
- 減少寫View與ViewGroup
- 可以使用RelativeLayout減少層級的就使用RelativeLayout,否則使用LinearLayout線性佈局。因為Android中RelativeLayout的測量次數比LinearLayout(不含weight的情況下)多,可以瞭解一下關於RelativeLayout、LinearLayout、FrameLayout的ViewGroup的測量原始碼分析。
- 使用SpannableString。相信大家對SpannableString都非常熟悉了,這是一個優化減少書寫View的利器。
- 優雅的給LinearLayout、RecyclerView設定分割線。
<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:background="@android:color/white"
android:divider="@drawable/divider_horizontal_w7_transparent"
android:orientation="horizontal"
android:showDividers="middle">
***
</LinearLayout>
複製程式碼
mRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
}
});
複製程式碼
強烈不建議,直接在itemView中直接畫分割線,雖然是簡單,但是我們是一個有追求的開發者,儘量把程式碼寫得漂亮一點。
- 使用merge標籤、ViewStub標籤、include標籤。上面我們都有講解過。
- drawableLeft 代替ImageView + TextView
- 使用ConstraintLayout。作為AndroidStudio新版本的推薦的預設佈局,可想它的強大之處,它是RelativeLayout的加強版,它是百分比佈局(已被Deprecated)的替換品。
- %1$d代替TextView + TextView。(如果需要多語言適配你就懂了這的重要性)
這裡幫大家整理幾個比較經典的注意點,由於有不同層次的讀者,所以這裡不用具體程式碼來講解,如果有不理解的同學,可以單獨對某個點進行查閱。
- 程式碼檢測神器——Lint檢測工具 估計有一部分同學看完上面的分析講解之後會覺得,好麻煩呀,要開啟這個然後又要那裡弄一下。然後就放棄了。接下來這個真的非常適合這部分同學使用。 開啟Lint的步驟:Analyze -> Inspect Code -> 選擇你需要分析的目錄,然後點選確定分析
在Android Lint:Performance這個錯誤節點下,非常清晰地描述了你都有哪些錯誤,每一個錯誤都有非常清晰的描述,你應該如何去改,在右邊的箭頭,程式幫我們直接定位到錯誤程式碼地方,是不是非常方便!
程式碼邏輯層優化
經過上述的分析調整後,我們接著分析一下關於程式碼邏輯層的優化。
- Traceview
Traceview是Android裝置的一個非常好用的效能分析工具,它可以通過詳細的介面,讓我們跟蹤程式的效能,並且能清晰地檢視到每一個函式的耗時和呼叫次數,所以我們用Traceview的時候要主要兩種影響流暢度的原因。一:主執行緒佔用cpu時間很長的方法函式;二:執行緒呼叫的次數
我通過具體的應用來具體分析,比如說商城型別的首頁,通過是使用RecyclerView,那麼我們可以先推斷影響RecyclerView的流暢度大多數是RecyclerView.Adapter#onBindViewHolder的方法。
同樣是通過Android Device Monitor皮膚,在下圖左方選中需要分析的應用,再點選左上角按鈕,當你覺得資料收集足夠時,再次點選那個按鈕即可,這時Traceview會自動開啟trace檔案。
那麼通過Traceview皮膚的上部分為時間線皮膚,左上方皮膚顯示的是採集資料中所採集的執行緒資訊,右邊上方皮膚為時間線,時間線上,每一條執行緒在採集時間段內所涉及的函式呼叫資訊。而下部分為函式分析皮膚,是traceview核心介面,它所提供的資訊資料非常多,他主要展示了某條執行緒中各個函式方法呼叫的情況,包括cpu使用時間,函式方法呼叫次數,和函式方法真實執行時間等資訊,這些資訊就是我們分析流暢度的關鍵所在。
我們瞭解一下操作,獲取方法的呼叫順序:
- 在traceview中搜尋響應的方法名
- 搜尋出的方法會自動展開,其中包含Parents 和 Children 兩組資訊
- 點選Parents下的方法名,直接跳轉到呼叫當前的方法處。Children則相反
Profile Panel各列功能描述說明
列名 | 描述 |
---|---|
Name | 呼叫的函式方法名 |
Incl Cpu Time | 函式佔用的CPU時間,包含內部呼叫其它函式的CPU時間 |
Excl Cpu Time | 函式佔用的CPU時間,但不含內部呼叫其它函式所佔用的CPU時間 |
Incl Real Time | 函式執行的真實時間(以毫秒為單位),內含呼叫其它函式所佔用的真實時間 |
Excl Real Time | 函式執行的真實時間(以毫秒為單位),不含呼叫其它函式所佔用的真實時間 |
Call+Recur Calls/Total | 函式被呼叫次數、遞迴呼叫佔總呼叫次數的百分比 |
Cpu Time/Call | 函式呼叫CPU時間與呼叫次數的比,相當於該函式平均執行時間 |
Real Time/Call | 函式呼叫CPU時間與呼叫次數的比,相當於該函式平均執行時間,這個時間包含來內部呼叫的其他函式的執行時間 |
看回上圖,我通過搜尋RecyclerView.Adapter#onBindViewHolder中呼叫的抽象方法inflateFromModel,找到了首頁某一個ViewHolder,從這個ViewHolder#inflateFromModel方法中,找到它呼叫了兩個方法,一個是圖片顯示的方法,另一個是正則判斷的方法,由於ViewHolder#inflateFromModel在滑動機制中會不斷地呼叫,而這個正則判斷的目的是對點選事件中的控制元件進行setTag操作的值進行髒資料驗證,其實這個正則判斷其實沒有必要在這裡執行,那麼我們可以不作任何判斷,將拿到的值直接View#setTag,然後在對應的onClick()再進行正則判斷,這樣就可以減少那些不必要佔用主執行緒cpu時間的方法函式,達到提高流暢度的效果。
- Systrace
Systrace非常直觀地展示每個執行緒上面的API的呼叫順序和耗時情況。同樣是通過Android Device Monitor皮膚,下圖中的箭頭,建議跟蹤持續時間不要太長,為了更好地定位問題.接著生成trace.html檔案,通過Google Chrome瀏覽器開啟。
先了解一下幾個常用的快捷鍵:
操作 | 作用 |
---|---|
w | 放大 |
s | 縮小 |
a | 左移 |
d | 右移 |
m | 標記當前選定區域 |
/ | 搜尋關鍵字 |
下拉trace.html我們可以看到frame,每一幀就顯示為圓圈,正常繪製是1秒60幀,大約一幀16.6毫秒,在這個值以下是正常顏色綠色,如果超過它就會變成紅色、黃色。非綠色的都說明有問題。這時需要通過’w’鍵放大那一幀,然後按‘m’鍵高亮,進一步分析問題。
Systrace能自動分析trace中的事件,並能自動高亮效能問題作為一個Alerts,我們可以根據提示進行分析優化。 但是,這裡所標的問題,我們怎麼能定位到具體哪一部分的程式碼呢?Systrace為我們提供了對應的API,然後在對應的持續時間。@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Trace.beginSection("Activity_onCreate");
***
Trace.endSection();
}
複製程式碼
整體來說,Systrace展現的資訊是很多的,但是如何加以利用還得繼續研究,對應上面涉及程式碼定位看上去就像一個Log,監測開始與結束時間罷了,感覺有點雞肋。
App的IO層程式碼優化
IO可分成為網路請求和磁碟讀寫IO,相信大家都知道,MVP模式下的Model層也是對IO層進行操作。而在主執行緒中進行長時間和頻繁的IO操作,對流暢度是有非常大的影響的,對於網路請求在安卓4.0之後,就已經不能在主執行緒進行網路操作了,否則程式會出現crash,因此我們對IO層的操作要進行監控。而Android為我們提供了StrictMode方式來監控程式碼是否出現上述的情況。
StrictMode主要有兩種策略,一是執行緒方面策略(TreadPolicy),二是VM方面策略(VmPolicy).
- 執行緒策略主要用於檢測UI執行緒中是否存在讀寫磁碟的操作,是否有網路請求操作,以及檢查自定義程式碼是否在UI執行緒執行得比較慢的情況
- 自定義的耗時呼叫 使用detectCustomSlowCalls()開啟
- 磁碟讀取操作 使用detectDiskReads()開啟
- 磁碟寫入操作 使用detectDiskWrites()開啟
- 網路操作 使用detectNetwork()開啟
- VmPolicy策略主要用於發現記憶體問題,比如Activity記憶體洩漏,SQL物件記憶體洩漏,IO操作物件資源未釋放。
- Activity洩露 使用detectActivityLeaks()開啟
- 未關閉的Closable物件洩露 使用detectLeakedClosableObjects()開啟
- 洩露的Sqlite物件 使用detectLeakedSqlLiteObjects()開啟
- 檢測例項數量 使用setClassInstanceLimit()開啟
只要主執行緒中配置了並啟動,它就能監聽主執行緒的執行情況,當發現有重大問題時和違背策略的時候,就會以logcat的形式提示使用者。
流暢度優化經驗總結
最後我來總結一下通篇對流暢度優化上的經驗:
- UI佈局優化
- 使用LinearLayout代替RelativeLayout,因為LinearLayout效能上稍微好一點
- 如果複雜的佈局,我們可以使用RelativeLayout來解決複雜的佈局關係
- 儘量少用LinearLayout的layout_weight屬性,因為它會消耗較大的效能
- 對應可以複用的佈局使用include標籤來進行復用
- 使用ViewStub標籤來載入一些不是必定出現使用的佈局
- 使用merge來減少不必要的層級巢狀
- 去除多餘的背景顏色,減少過度繪製問題
- 使用compound drawables、%1$d 減少佈局的建立
2.RecyclerView效能優化
- 在RecyclerView.Adapter#onBindViewHolder函式下的複用問題,注意哪些不必要的變數建立
- 非同步載入圖片
- 對於一些不必要的操作不要在滑動複用部分進行實現,這樣會影響cpu運算
- UI主執行緒
- 非同步請求網路資料
- 如果較為耗時的操作不要放在UI執行緒中實現
- 不要在UI執行緒外操作UI
4.第三方平臺
- 騰訊開源工具——GT
- 聽雲——應用效能監控平臺
寫在結尾:我在這篇部落格的時候,剛剛出現了AndroidStudio3.2金絲雀版本,而部分上述的工具,Google已經不再推薦使用,接下來我會繼續更新Google新推薦的優化工具文章,努力成為一個效能優化的好手。