1. 程式人生 > >Android效能全面分析與優化方案研究

Android效能全面分析與優化方案研究

效能優化是一個持續的過程,要多種手段,一點一點優化,一般是優化影響比較大頭的,再逐步優化小頭的,今天是手機迅雷Android高階工程師做的效能分析及優化方案分享。原文連結:https://www.jianshu.com/p/307ba8911799,歡迎關注。

文章經授權釋出,如需轉載,請聯絡作者微信:tonglee

該文章是結合我司產品手機迅雷做的一個全面的效能分析及優化方案。本文篇幅較長,幾乎涵蓋了所有的效能方面問題,以及給出瞭如何查詢和解決問題的方案,幾乎是史上最全最實用的Android效能分析和優化文章。

結合以下四個部分講解:

  • 效能問題分類

  • 效能優化原則和方法

  • 藉助效能優化工具分析解決問題

  • 效能優化指標

效能問題分類

  • 1、渲染問題:過度繪製、佈局冗雜

  • 2、記憶體問題:記憶體浪費(記憶體管理)、記憶體洩漏

  • 3、功耗問題:耗電

效能優化原則和方法

1、效能優化原則

  • 堅持效能測試(開發和測試同學的測試方法略有不同):不要憑感覺去檢測效能問題、評估效能優化的效果,應該保持足夠多的測量,用資料說話(主要針對測試同學)。使用各種效能工具測試及快速定位問題(主要針對開發同學)。

  • 使用低配置的裝置:同樣的程式,在低端配置的裝置中,相同的問題會暴露得更為明顯。

  • 權衡利弊:在能夠保證產品穩定、按時完成需求的前提下去做優化。

2、優化方法

  • 瞭解問題(分為可感知和不可感知的效能問題):對於效能問題來講,這個步驟只適用於某些明顯的效能問題,很多無法感知的效能問題需要通過工具定位。例如:記憶體洩漏、層級冗雜、過度繪製等無法感知。滑動卡頓是可以感知到的。

  • 定位問題:通過工具檢測、分析資料,定位在什麼地方存在效能問題。

  • 分析問題:找到問題後,分析針對這個問題該如何解決,確定解決方案。

  • 解決問題:根據分析結果尋找解決方案。

  • 驗證問題:保證優化有效,沒有產生新的問題,以及產品穩定性。

效能優化工具

以下優化工具在下面文章中具體介紹使用方法。

  • 1、手機開發者選項:除錯GPU過度繪製、啟用嚴格模式、顯示CPU使用情況、GPU呈現模式分析、顯示所有"應用程式無響應"。(小米手機開發開發者選項中名字)

  • 2、IDE中:Android Studio,比如靜態程式碼檢測工具、Memory Monitor、CPU Monitor、NetWork Monitor、GPU Monitor、Layout Inspector、Analyze APK等。

  • 3、SDK中:sdk\tools,比如DDMS、HierarchyViewer、TraceView等。

  • 4、第三方工具:MAT、LeakCanary、GT等。

效能優化指標

1、渲染

  • 滑動流暢度:FPS,即Frame per Second,一秒內的重新整理幀數,越接近60幀越好;

  • 過度繪製:單頁面的3X(粉紅色區域) Overdraw小於25%

  • 啟動時間:這裡主要說的是Activity介面啟動時間,一般低於300ms,需要用高頻攝像機計算時間。

2、記憶體

  • 記憶體大小:峰值越低越好,需要優化前後做對比

  • 記憶體洩漏:需要用工具檢查對比優化前後

3、功耗

  • 單位時間內的掉電量,掉電量越少越好,業內沒有固定標準。華為有專門測試功耗的機器,以及自己的標準。

渲染問題

先來看看造成應用UI卡頓的常見原因都有哪些?

1、人為在UI執行緒中做輕微耗時操作,導致UI執行緒卡頓;
2、佈局Layout過於複雜,無法在16ms內完成渲染;
3、同一時間動畫執行的次數過多,導致CPU或GPU負載過重;
4、View過度繪製,導致某些畫素在同一幀時間內被繪製多次,從而使CPU或GPU負載過重;
5、View頻繁的觸發measure、layout,導致measure、layout累計耗時過多及整個View頻繁的重新渲染;
6、記憶體頻繁觸發GC過多(同一幀中頻繁建立記憶體),導致暫時阻塞渲染操作;
7、冗餘資源及邏輯等導致載入和執行緩慢;
8、臭名昭著的ANR;

大多數使用者感知到的卡頓等效能問題的最主要根源都是因為渲染效能。(Google官方說的)

Android系統每隔16ms發出VSYNC訊號(vertical synchronization --場掃描同步,場同步,垂直同步),觸發對UI進行渲染,如果每次渲染都成功,這樣就能夠達到流暢的畫面所需要的60fps,為了能夠實現60fps,這意味著程式的大多數操作都必須在16ms(1000/60=16.67ms)內完成。

如果你的某個操作花費時間是24ms,系統在得到VSYNC訊號的時候就無法進行正常渲染,這樣就發生了丟幀現象。那麼使用者在32ms內看到的會是同一幀畫面。

640?wx_fmt=png

1、過度繪製
Overdraw(過度繪製)描述的是螢幕上的某個畫素在同一幀的時間內被繪製了多次。在多層次的UI結構裡面,如果不可見的UI也在做繪製的操作,這就會導致某些畫素區域被繪製了多次。這就浪費大量的CPU以及GPU資源,找出介面滑動不流暢、介面啟動速度慢、手機發熱。

  • 如何檢視過度繪製?

  • 設定 — 開發中選項 — 除錯GPU過度繪製

  • 來看看手雷裡的過度繪製和優化效果(目前手雷還存在很多待優化的頁面)

640?wx_fmt=png
  • 上圖中的各種顏色都代表什麼意思?

640?wx_fmt=png

接下來舉例說明:

1、MainTabActivity

在MainTabActivity的Theme中修改背景

640?wx_fmt=png

去除佈局(main_activity_linerlayout.xml)中的background

640?wx_fmt=png

如果不給當前Activity設定主題,預設主題是什麼,預設主題背景是什麼?

640?wx_fmt=png
640?wx_fmt=png
640?wx_fmt=png
<!--可以在預設主題中新增通用主題背景-->
<item name="android:windowBackground">@drawable/common_layout_content_bkg</item>
<!--去除背景-->
<item name="android:windowBackground">null</item>

2、除了佈局中多餘背景,還有可能在程式碼裡添加了多餘的背景。

檢視分享彈窗的佈局程式碼發現只有一個background,但為什麼會過度繪製呢?

640?wx_fmt=png
640?wx_fmt=png

程式碼修改(SharePlatformsDialog.java)

640?wx_fmt=png

3、彈窗底部佈局不會導致彈窗本身過度繪製

640?wx_fmt=png

彈窗的繪製是屬於剪下式繪製不是覆蓋繪製,蒙層是透明度亮度的調節不是繪製一層灰色。
如果我們不想用系統dialog而是自定義一個彈窗view,就需要考慮過度繪製問題。

4、自定義view時,通過Canvas的clipRect方法控制每個檢視每次重新整理的區域,這樣可以避免重新整理不必要的區域,從而規避過渡繪製的問題。還可以使用canvas.quickreject()來判斷是否和某個矩形相交,從而跳過那些非矩形區域內的繪製操作。參考:http://jaeger.itscoder.com/android/2016/09/29/android-performance-overdraw.html

優化方法和步驟關鍵總結

總結一下,優化步驟如下:

  • 1、移除或修改Window預設的Background

  • 2、移除XML佈局檔案中非必需的Background

  • 3、按需顯示佔位背景圖片

  • 4、控制繪製區域

2、佈局優化
佈局太過複雜,層級巢狀太深導致繪製操作耗時,且增加記憶體的消耗。
我們的目標就是,層級扁平化。

佈局優化的建議:

  • 第一個建議:可以使用相對佈局減少層級的就使用相對佈局,否則使用線性佈局。Android中RelativeLayout和LinearLayout效能分析,參考:http://www.jianshu.com/p/8a7d059da746#

  • 第二個建議:用merge標籤來合併佈局,這可以減少佈局層次。

  • 第三個建議:用include標籤來重用佈局,抽取通用的佈局可以讓佈局的邏輯更清晰明瞭,但要避免include亂用。

  • 第四個建議:避免建立不必要的佈局層級。(最容易發生的!)

  • 第五個建議:使用惰性控制元件ViewStub實現佈局動態載入

如何藉助工具檢視程式碼佈局?

Android SDK 工具箱中有一個叫做 Hierarchy Viewer 的工具,能夠在程式執行時分析 Layout。
可以用這個工具找到 Layout 的效能瓶頸
該工具的使用條件:模擬器或者Root版真機。
如何開啟該功能:AndroidStudio中,Tools — Android — Android Devices Monitor
該工具的缺點:使用起來麻煩。

640?wx_fmt=png

看看專案中遇到的問題(MainTabAvtivity)。

merge標籤的使用

640?wx_fmt=png

未使用merge,例如:XLTabLayout.java

640?wx_fmt=png
640?wx_fmt=png

使用merge,例如:賬號資訊頁的條目UserAccountItem

640?wx_fmt=png

include標籤的使用導致的問題

640?wx_fmt=png
640?wx_fmt=png

避免建立不必要的層級(MainTabActivity)

640?wx_fmt=png

ViewStub的使用

這個標籤最大的優點是當你需要時才會載入,使用他並不會影響UI初始化時的效能。
通常情況下我們需要在某個條件下使用某個佈局的時候會通過gone或者invisible來隱藏,其實這樣的方式雖然隱藏了佈局,但是當顯示該介面的時候還是將該佈局例項化的。使用ViewStub可以避免記憶體的浪費,加快渲染速度。
其實ViewStub就是一個寬高都為0的一個View,它預設是不可見的,只有通過呼叫setVisibility函式或者Inflate函式才會將其要裝載的目標佈局給加載出來,從而達到延遲載入的效果,這個要被載入的佈局通過android:layout屬性來設定。

640?wx_fmt=png

當準備inflate ViewStub時,呼叫inflate()方法即可。還可以設定ViewStub的Visibility為VISIBLE或INVISIBLE,也會觸發inflate。注意的是,使用inflate()方法能返回佈局檔案的根View。

((ViewStub) findViewById(R.id.stub_import)).setVisibility(View.VISIBLE);
 // or
View importPanel = ((ViewStub) findViewById(R.id.stub_import)).inflate();

setVisibility的時候會觸發了inflate

640?wx_fmt=png

注意:使用ViewStub載入的佈局中不能使用merge標籤。

看看Space標籤(不常用)

space標籤可以只在佈局檔案中佔位,不繪製,Space標籤有對應的java類Space.java,通過閱讀原始碼可以發現,它繼承至View.java,並且複寫了draw方法,該方法為空,既沒有呼叫父類的draw方法,也沒有執行自己的程式碼,表示該類是沒有繪製操作的,但onMeasure方法正常呼叫,說明是有寬高的。
主要功能用來設定間距,這個標籤不常用,常使用margin或padding。

3、介紹一下檢視渲染效能的工具
GPU呈現模式分析(大致定位問題)

開發者選項 — GPU呈現模式分析 — 選擇“在螢幕上顯示為條形圖”

640?wx_fmt=png

Android開發者選項——Gpu呈現模式分析,參考:http://www.voidcn.com/blog/gjy211/article/p-6210447.html
自動播放的視訊停止的時候會有兩條很長的柱線,下個視訊播放的時候還會有一條。這裡有一個明顯的卡頓。
播放器操作(DefaultPlayerView.java - doPlay,player_auto_control_layout.xml):

640?wx_fmt=png

上圖的E total time = 68 是播放器停止播放的時候耗費的時間。

total time = 29 是播放器開始播放的時候耗費的時間。
其中,大部分時間耗費在了 5total time = 18 上面,這個是inflate播放器介面的時候耗費的時間。

640?wx_fmt=png

是不是所有的inflate都很耗費時間,看一下賬號資訊頁:

640?wx_fmt=png

640?wx_fmt=png

GPU Monitor

640?wx_fmt=png

啟用嚴格模式(不止渲染效能)

應用在主執行緒上執行長時間操作時會閃爍螢幕。
通過程式碼進行嚴格模式(StrictMode)除錯,參考:http://www.tuicool.com/articles/ueeM7b6

應作者要求,如果程式設計師有喜歡小說的,可以關注作者的公號:

640?wx_fmt=jpeg

記憶體問題

1、記憶體浪費
程式記憶體的管理是否合理高效對應用的效能有著很大的影響。
推薦閱讀Android效能優化典範-第3季,參考:http://hukai.me/android-performance-patterns-season-3/

ArrayMap(我們專案中沒有用到,Android原始碼中很多使用)

Android為移動作業系統特意編寫了一些更加高效的容器,例如ArrayMap、SparseArray。
為了解決HashMap更佔記憶體的弊端,Android提供了記憶體效率更高的ArrayMap。

先來看看HashMap的原理

HashMap的整體結構如下:

640?wx_fmt=png

儲存位置的確定流程:

640?wx_fmt=png

再看來看看ArrayMap是如何優化記憶體的

它內部使用兩個陣列進行工作,其中一個數組記錄key hash過後的順序列表,另外一個數組按key的順序記錄Key-Value值,如下圖所示:

640?wx_fmt=jpeg

當你想獲取某個value的時候,ArrayMap會計算輸入key轉換過後的hash值,然後對hash陣列使用二分查詢法尋找到對應的index,然後我們可以通過這個index在另外一個數組中直接訪問到需要的鍵值對。

640?wx_fmt=jpeg

既然ArrayMap中的記憶體佔用是連續不間斷的,那麼它是如何處理插入與刪除操作的呢?它跟HashMap有什麼區別?二者之間的刪除插入效率有什麼差異?請看下圖所示,演示了Array的特性:

640?wx_fmt=jpeg
640?wx_fmt=jpeg

HashMap與ArrayMap之間的記憶體佔用效率對比圖如下:

640?wx_fmt=jpeg

與HashMap相比,ArrayMap在迴圈遍歷的時候更加高效。

640?wx_fmt=jpeg

什麼時候使用ArrayMap呢?

  • 1、物件個數的數量級最好是千以內,沒有頻繁的插入刪除操作

  • 2、資料組織形式包含Map結構

Autoboxing(避免自動裝箱)

Autoboxing的行為還經常發生在類似HashMap這樣的容器裡面,對HashMap的增刪改查操作都會發生了大量的autoboxing的行為。當key是int型別的時候,HashMap和ArrayMap都有Autoboxing行為。

SparseArray(專案中用到較多 -- 後面再說如何利用工具查詢該用SparseArray而沒有用到的地方)

為了避免Autoboxing行為Android提供了SparseArray,此容器使用於key為int型別。
SparseBooleanMap,SparseIntMap,SparseLongMap等容器,是key為int,value型別相應為boolean、int、long等。

Enum(列舉,專案中較多使用,應儘量避免)

Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.
Android官方強烈建議不要在Android程式裡面使用到enum。
關於enum的效率,請看下面的討論。假設我們有這樣一份程式碼,編譯之後的dex大小是2556 bytes,在此基礎之上,新增一些如下程式碼,這些程式碼使用普通static常量相關作為判斷值:

640?wx_fmt=jpeg

增加上面那段程式碼之後,編譯成dex的大小是2680 bytes,相比起之前的2556 bytes只增加124 bytes。假如換做使用enum,情況如下:

640?wx_fmt=jpeg

使用enum之後的dex大小是4188 bytes,相比起2556增加了1632 bytes,增長量是使用static int的13倍。不僅僅如此,使用enum,執行時還會產生額外的記憶體佔用,如下圖所示:

640?wx_fmt=jpeg

2、記憶體洩漏
什麼是記憶體洩漏?一些不用的物件被長期持有,導致記憶體無法被釋放。

可能發生記憶體洩漏的地方有哪些?

內部類引用導致Activity的洩漏

在Java中,非靜態(匿名)內部類會預設隱性引用外部類物件。而靜態內部類不會引用外部類物件。
最典型的場景是Handler導致的Activity洩漏,如果Handler中有延遲的任務或者是等待執行的任務佇列過長,都有可能因為Handler繼續執行而導致Activity發生洩漏。
為了解決這個問題,可以在UI退出之前,執行remove Handler訊息佇列中的訊息與runnable物件。或者是使用Static + WeakReference的方式來達到斷開Handler與Activity之間存在引用關係的目的。
舉例,MainTabActivity - MainTabHandler:

640?wx_fmt=png
640?wx_fmt=png
640?wx_fmt=png

如何修復?

640?wx_fmt=png
640?wx_fmt=png
640?wx_fmt=png

Android Weak Handler:可以避免記憶體洩漏的Handler庫,參考:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2014/1123/2047.html

Activity Context被傳遞到其他例項中,這可能導致自身被引用而發生洩漏。

考慮使用Application Context而不是Activity Context。
例如:全域性Dialog或者Context被單例持有。

靜態造成的記憶體洩漏

640?wx_fmt=png
640?wx_fmt=png

還有靜態變數持有View,例如:

private static View view;
void setStaticView() {
  view = findViewById(R.id.sv_button);
}
  • 注意監聽器的登出(稍後利用工具分析一個例子)

  • register就要unregister

  • 注意Cursor物件是否及時關閉(專案中也存在,不再列舉)

  • WebView的引起的洩漏(暫時沒有研究)

  • 使用工具分析定位解決記憶體洩漏

Memory monitor

通過MemoryMonitor可以看到,啟動手雷進入手雷的記憶體情況如下(為什麼沒有做任何操作記憶體一直在增加?):

640?wx_fmt=png
640?wx_fmt=png

通過例項分析一處記憶體洩漏,操作步驟如下:
啟動手雷 - 進入首頁 - 切換底部tab到我的tab - 點選登入彈窗彈窗 - 登入成功返回首頁

640?wx_fmt=png

640?wx_fmt=png

MAT(Memory Analyzer Tool)

需要下載MAT獨立版,可到這裡下載解壓使用:\192.168.8.188\上傳2\ltsoft
分析剛才的操作記憶體情況

640?wx_fmt=png
640?wx_fmt=png
640?wx_fmt=png

進入首頁並沒有進行任何活動操作,為什麼會有那麼多bitmap物件呢?

640?wx_fmt=png

LeakCanary

640?wx_fmt=jpeg

LeakCanary 中文使用說明,參考:https://www.liaohuqiu.net/cn/posts/leak-canary-read-me/
LeakCanary:讓記憶體洩露無所遁形,參考:https://www.liaohuqiu.net/cn/posts/leak-canary/

TraceView(不做詳細分析)

GT(應該更適合測試同學測試APP效能)

利用GT,僅憑一部手機,無需連線電腦,您即可對APP進行快速的效能測試(CPU、記憶體、流量、電量、幀率/流暢度等等)、開發日誌的檢視、Crash日誌檢視、網路資料包的抓取、APP內部引數的除錯、真機程式碼耗時統計等。
GT官網:http://gt.qq.com/index.html

記憶體使用策略優化

看看下載一個視訊加上瀏覽一下精選頁,然後將應用切到後臺,記憶體使用情況

有什麼優化記憶體的策略

  • onLowMemory():Android系統提供了一些回撥來通知當前應用的記憶體使用情況,通常來說,當所有的background應用都被kill掉的時候,forground應用會收到onLowMemory()的回撥。在這種情況下,需要儘快釋放當前應用的非必須的記憶體資源,從而確保系統能夠繼續穩定執行。

  • onTrimMemory(int):Android系統從4.0開始還提供了onTrimMemory()的回撥,當系統記憶體達到某些條件的時候,所有正在執行的應用都會收到這個回撥,同時在這個回撥裡面會傳遞引數,代表不同的記憶體使用情況,收到onTrimMemory()回撥的時候,需要根據傳遞的引數型別進行判斷,合理的選擇釋放自身的一些記憶體佔用,一方面可以提高系統的整體執行流暢度,另外也可以避免自己被系統判斷為優先需要殺掉的應用。

3、效能優化必備神器推薦(Lint)

上面分析的一些專案中的問題,怎麼找到的呢?

Lint:靜態程式碼分析工具

  • 如何通過Lint查詢專案中的問題,如何使用?

640?wx_fmt=png
640?wx_fmt=png
640?wx_fmt=png
640?wx_fmt=png
640?wx_fmt=png
640?wx_fmt=png
640?wx_fmt=png
640?wx_fmt=png
640?wx_fmt=png
640?wx_fmt=png
  • 如果只想分析某個資料夾的程式碼

640?wx_fmt=png
640?wx_fmt=png
  • 設定程式碼分析選項