1. 程式人生 > >Android應用開發效能優化完全分析

Android應用開發效能優化完全分析

1 背景

其實有點不想寫這篇文章的,但是又想寫,有些矛盾。不想寫的原因是隨便上網一搜一堆關於效能的建議,感覺大家你一總結、我一總結的都說到了很多優化注意事項,但是看過這些文章後大多數存在一個問題就是隻給出啥啥啥不能用,啥啥啥該咋用等,卻很少有較為系統的進行真正效能案例分析的,大多數都是嘴上喊喊或者死記住規則而已(當然了,這話我自己聽著都有些刺耳,實在不好意思,其實關於效能優化的優質博文網上也還是有很多的,譬如Google官方都已經推出了優化專題,我這裡只是總結下自的感悟而已,若有得罪歡迎拍磚,我願捱打,因為我之前工作的一半時間都是負責效能優化)。

當然了,本文不會就此編輯這麼一次,因為技術在發展,工具在強大(寫著寫著Android Studio 1.4版本都推送了),自己的經驗也在增加,所以本文自然不會覆蓋所有效能優化及分析;解決的辦法就是該文章會長期維護更新,同時在評論區歡迎你關於效能優化點子的探討。

Android應用的效能問題其實可以劃分為幾個大的模組的,而且都具有相對不錯的優化除錯技巧,下面我們就會依據一個專案常規開發的大型別來進行一些分析講解。

PS:之前呆過一家初創醫療網際網路公司,別提效能優化了,老闆立完新專案後一個月就要求見到上線成品,這種壓迫下談何效能優化,純屬扯蛋,所以不到三個月時間我主動選擇撤了,這種現象後來我一打聽發現在很多初創公司都很嚴重,都想速成卻忽略了體驗。

PPPS:本文只是達到拋磚引玉的作用,很多東西細究下去都是值得深入研究的,再加上效能優化本來就是一個需要綜合考量的任務,不是說會了本文哪一點就能做效能分析了,需要面面俱到才可高效定位問題原因。

這裡寫圖片描述

2 應用UI效能問題分析

UI可謂是一個應用的臉,所以每一款應用在開發階段我們的互動、視覺、動畫工程師都拼命的想讓它變得自然大方美麗,可是現實總是不盡人意,動畫和互動總會覺得開發做出來的應用用上去感覺不自然,沒有達到他們心目中的自然流暢細節;這種情況之下就更別提釋出給終端使用者使用了,使用者要是能夠感覺出來,少則影響心情,多則解除安裝應用;所以一個應用的UI顯示效能問題就不得不被開發人員重視。

2-1 應用UI卡頓原理

人類大腦與眼睛對一個畫面的連貫性感知其實是有一個界限的,譬如我們看電影會覺得畫面很自然連貫(幀率為24fps),用手機當然也需要感知螢幕操作的連貫性(尤其是動畫過度),所以Android索性就把達到這種流暢的幀率規定為60fps。

有了上面的背景,我們開發App的幀率效能目標就是保持在60fps,也就是說我們在進行App效能優化時心中要有如下準則:

換算關係:60幀/秒-----------16ms/幀;

準則:儘量保證每次在16ms內處理完所有的CPU與GPU計算、繪製、渲染等操作,否則會造成丟幀卡頓問題。

從上面可以看出來,所謂的卡頓其實是可以量化的,每次是否能夠成功渲染是非常重要的問題,16ms能否完整的做完一次操作直接決定了卡頓效能問題。

當然了,針對Android系統的設計我們還需要知道另一個常識;虛擬機器在執行GC垃圾回收操作時所有執行緒(包括UI執行緒)都需要暫停,當GC垃圾回收完成之後所有執行緒才能夠繼續執行(這個細節下面小節會有詳細介紹)。也就是說當在16ms內進行渲染等操作時如果剛好遇上大量GC操作則會導致渲染時間明顯不足,也就從而導致了丟幀卡頓問題。

有了上面這兩個簡單的理論基礎之後我們下面就會探討一些UI卡頓的原因分析及解決方案。

2-2 應用UI卡頓常見原因

我們在使用App時會發現有些介面啟動卡頓、動畫不流暢、列表等滑動時也會卡頓,究其原因,很多都是丟幀導致的;通過上面卡頓原理的簡單說明我們從應用開發的角度往回推理可以得出常見卡頓原因,如下:

  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;

可以看見,上面這些導致卡頓的原因都是我們平時開發中非常常見的。有些人可能會覺得自己的應用用著還蠻OK的,其實那是因為你沒進行一些瞬時測試和壓力測試,一旦在這種環境下執行你的App你就會發現很多效能問題。

2-3 應用UI卡頓分析解決方法

分析UI卡頓我們一般都藉助工具,通過工具一般都可以直觀的分析出問題原因,從而反推尋求優化方案,具體如下細說各種強大的工具。

2-3-1 使用HierarchyViewer分析UI效能

我們可以通過SDK提供的工具HierarchyViewer來進行UI佈局複雜程度及冗餘等分析,如下:

xxx@ThinkPad:~$ hierarchyviewer   //通過命令啟動HierarchyViewer

選中一個Window介面item,然後點選右上方Hierarchy window或者Pixel Perfect window(這裡不介紹,主要用來檢查畫素屬性的)即可操作。

先看下Hierarchy window,如下:

這裡寫圖片描述

一個Activity的View樹,通過這個樹可以分析出View巢狀的冗餘層級,左下角可以輸入View的id直接自動跳轉到中間顯示;Save as PNG用來把左側樹儲存為一張圖片;Capture Layers用來儲存psd的PhotoShop分層素材;右側劇中顯示選中View的當前屬性狀態;右下角顯示當前View在Activity中的位置等;左下角三個進行切換;Load View Hierarchy用來手動重新整理變化(不會自動重新整理的)。當我們選擇一個View後會如下圖所示:

這裡寫圖片描述

類似上圖可以很方便的檢視到當前View的許多資訊;上圖最底那三個彩色原點代表了當前View的效能指標,從左到右依次代表測量、佈局、繪製的渲染時間,紅色和黃色的點代表速度渲染較慢的View(當然了,有些時候較慢不代表有問題,譬如ViewGroup子節點越多、結構越複雜,效能就越差)。

當然了,在自定義View的效能除錯時,HierarchyViewer上面的invalidate Layout和requestLayout按鈕的功能更加強大,它可以幫助我們debug自定義View執行invalidate()和requestLayout()過程,我們只需要在程式碼的相關地方打上斷點就行了,接下來通過它觀察繪製即可。

可以發現,有了HierarchyViewer除錯工具,我們的UI效能分析變得十分容易,這個工具也是我們開發中除錯UI的利器,在平時寫程式碼時會時常伴隨我們左右。

2-3-2 使用GPU過度繪製分析UI效能

我們對於UI效能的優化還可以通過開發者選項中的GPU過度繪製工具來進行分析。在設定->開發者選項->除錯GPU過度繪製(不同裝置可能位置或者叫法不同)中開啟除錯後可以看見如下圖(對settings當前介面過度繪製進行分析):

這裡寫圖片描述

可以發現,開啟後在我們想要除錯的應用介面中可以看到各種顏色的區域,具體含義如下:

顏色 含義
無色 WebView等的渲染區域
藍色 1x過度繪製
綠色 2x過度繪製
淡紅色 3x過度繪製
紅色 4x(+)過度繪製


由於過度繪製指在螢幕的一個畫素上繪製多次(譬如一個設定了背景色的TextView就會被繪製兩次,一次背景一次文字;這裡需要強調的是Activity設定的Theme主題的背景不被算在過度繪製層級中),所以最理想的就是繪製一次,也就是藍色(當然這在很多絢麗的介面是不現實的,所以大家有個度即可,我們的開發效能優化標準要求最極端介面下紅色區域不能長期持續超過螢幕三分之一,可見還是比較寬鬆的規定),因此我們需要依據此顏色分佈進行程式碼優化,譬如優化佈局層級、減少沒必要的背景、暫時不顯示的View設定為GONE而不是INVISIBLE、自定義View的onDraw方法設定canvas.clipRect()指定繪製區域或通過canvas.quickreject()減少繪製區域等。

2-3-3 使用GPU呈現模式圖及FPS考核UI效能

Android介面流暢度除過視覺感知以外是可以考核的(測試妹子專用),常見的方法就是通過GPU呈現模式圖或者實時FPS顯示進行考核,這裡我們主要針對GPU呈現模式圖進行下說明,因為FPS考核測試方法有很多(譬如自己寫程式碼實現、第三方App測試、韌體支援等),所以不做統一說明。

通過開發者選項中GPU呈現模式圖工具來進行流暢度考量的流程是(注意:如果是在開啟應用後才開啟此功能,記得先把應用結束後重新啟動)在設定->開發者選項->GPU呈現模式(不同裝置可能位置或者叫法不同)中開啟除錯後可以看見如下圖(對settings當前介面上下滑動列表後的圖表):

這裡寫圖片描述

當然,也可以在執行完UI滑動操作後在命令列輸入如下命令檢視命令列列印的GPU渲染資料(分析依據:Draw + Process + Execute = 完整的顯示一幀時間 < 16ms):

adb shell dumpsys gfxinfo [應用包名]

開啟上圖視覺化工具後,我們可以在手機畫面上看到豐富的GPU繪製圖形資訊,分別展示了StatusBar、NavgationBar、Activity區域等的GPU渲染時間資訊,隨著介面的重新整理,介面上會以實時柱狀圖來顯示每幀的渲染時間,柱狀圖越高表示渲染時間越長,每個柱狀圖偏上都有一根代表16ms基準的綠色橫線,每一條豎著的柱狀線都包含三部分(藍色代表測量繪製Display List的時間,紅色代表OpenGL渲染Display List所需要的時間,黃色代表CPU等待GPU處理的時間),只要我們每一幀的總時間低於基準線就不會發生UI卡頓問題(個別超出基準線其實也不算啥問題的)。

可以發現,這個工具是有侷限性的,他雖然能夠看出來有幀耗時超過基準線導致了丟幀卡頓,但卻分析不到造成丟幀的具體原因。所以說為了配合解決分析UI丟幀卡頓問題我們還需要藉助traceview和systrace來進行原因追蹤,下面我們會介紹這兩種工具的。

2-3-4 使用Lint進行資源及冗餘UI佈局等優化

上面說了,冗餘資源及邏輯等也可能會導致載入和執行緩慢,所以我們就來看看Lint這個工具是如何發現優化這些問題的(當然了,Lint實際的功能是非常強大的,我們開發中也是經常使用它來發現一些問題的,這裡主要有點針對UI效能的說明了,其他的雷同)。

在Android Studio 1.4版本中使用Lint最簡單的辦法就是將滑鼠放在程式碼區點選右鍵->Analyze->Inspect Code–>介面選擇你要檢測的模組->點選確認開始檢測,等待一下後會發現如下結果:

這裡寫圖片描述

可以看見,Lint檢測完後給了我們很多建議的,我們重點看一個關於UI效能的檢測結果;上圖中高亮的那一行明確說明了存在冗餘的UI層級巢狀,所以我們是可以點選跳進去進行優化處理掉的。

當然了,Lint還有很多功能,大家可以自行探索發揮,這裡只是達到拋磚引玉的作用。

2-3-5 使用Memory監測及GC列印與Allocation Tracker進行UI卡頓分析

關於Android的記憶體管理機制下面的一節會詳細介紹,這裡我們主要針對GC導致的UI卡頓問題進行詳細說明。

Android系統會依據記憶體中不同的記憶體資料型別分別執行不同的GC操作,常見應用開發中導致GC頻繁執行的原因主要可能是因為短時間內有大量頻繁的物件建立與釋放操作,也就是俗稱的記憶體抖動現象,或者短時間內已經存在大量記憶體暫用介於閾值邊緣,接著每當有新物件建立時都會導致超越閾值觸發GC操作。

如下是我工作中一個專案的一次經歷(我將程式碼回退特意抓取的),出現這個問題的場景是一次壓力測試導致整個系統卡頓,瞬間殺掉應用就OK了,究其原因最終查到是一個API的調運位置寫錯了方式,導致一直被狂調,當普通使用時不會有問題,壓力測試必現卡頓。具體記憶體參考圖如下:
這裡寫圖片描述
與此抖動圖對應的LogCat抓取如下:

//擷取其中比較密集一段LogCat,與上圖Memory檢測到的抖動圖對應,其中xxx為應用包名
......
10-06 00:59:45.619 xxx I/art: Explicit concurrent mark sweep GC freed 72515(3MB) AllocSpace objects, 65(2028KB) LOS objects, 80% free, 17MB/89MB, paused 3.505ms total 60.958ms
10-06 00:59:45.749 xxx I/art: Explicit concurrent mark sweep GC freed 5396(193KB) AllocSpace objects, 0(0B) LOS objects, 75% free, 23MB/95MB, paused 2.079ms total 100.522ms
......
10-06 00:59:48.059 xxx I/art: Explicit concurrent mark sweep GC freed 4693(172KB) AllocSpace objects, 0(0B) LOS objects, 75% free, 23MB/95MB, paused 2.227ms total 101.692ms
......

我們知道,類似上面logcat列印一樣,觸發垃圾回收的主要原因有以下幾種:

  • GC_MALLOC——記憶體分配失敗時觸發;

  • GC_CONCURRENT——當分配的物件大小超過一個限定值(不同系統)時觸發;

  • GC_EXPLICIT——對垃圾收集的顯式呼叫(System.gc()) ;

  • GC_EXTERNAL_ALLOC——外部記憶體分配失敗時觸發;

可以看見,這種不停的大面積列印GC導致所有執行緒暫停的操作必定會導致UI視覺的卡頓,所以我們要避免此類問題的出現,具體的常見優化方式如下:

  • 檢查程式碼,儘量避免有些頻繁觸發的邏輯方法中存在大量物件分配;
  • 儘量避免在多次for迴圈中頻繁分配物件;
  • 避免在自定義View的onDraw()方法中執行復雜的操作及建立物件(譬如Paint的例項化操作不要寫在onDraw()方法中等);
  • 對於併發下載等類似邏輯的實現儘量避免多次建立執行緒物件,而是交給執行緒池處理。

當然了,有了上面說明GC導致的效能後我們就該定位分析問題了,可以通過執行DDMS->Allocation Tracker標籤開啟一個新視窗,然後點選Start Tracing按鈕,接著執行你想分析的程式碼,執行完畢後點擊Get Allocations按鈕就能夠看見一個已分配物件的列表,如下:

這裡寫圖片描述

點選上面第一個表格中的任何一項就能夠在第二個表格中看見導致該記憶體分配的棧資訊,通過這個工具我們可以很方便的知道程式碼分配了哪類物件、在哪個執行緒、哪個類、哪個檔案的哪一行。譬如我們可以通過Allocation Tracker分別做一次Paint物件例項化在onDraw與構造方法的一個自定義View的記憶體跟蹤,然後你就明白這個工具的強大了。

PS一句,Android Studio新版本除過DDMS以外在Memory檢視的左側已經集成了Allocation Tracker功能,只是用起來還是沒有DDMS的方便實用,如下圖:

這裡寫圖片描述

2-3-6 使用Traceview和dmtracedump進行分析優化

關於UI卡頓問題我們還可以通過執行Traceview工具進行分析,他是一個分析器,記錄了應用程式中每個函式的執行時間;我們可以開啟DDMS然後選擇一個程序,接著點選上面的“Start Method Profiling”按鈕(紅色小點變為黑色即開始執行),然後操作我們的卡頓UI(小範圍測試,所以操作最好不要超過5s),完事再點一下剛才按的那個按鈕,稍等片刻即可出現下圖,如下:

這裡寫圖片描述

花花綠綠的一幅圖我們怎麼分析呢?下面我們解釋下如何通過該工具定位問題:

整個介面包括上下兩部分,上面是你測試的程序中每個執行緒執行的時間線,下面是每個方法(包含parent及child)執行的各個指標的值。通過上圖的時間面板可以直觀發現,整個trace時間段main執行緒做的事情特別多,其他的做的相對較少。當我們選擇上面的一個執行緒後可以發現下面的效能面板很複雜,其實這才是TraceView的核心圖表,它主要展示了執行緒中各個方法的呼叫資訊(CPU使用時間、呼叫次數等),這些資訊就是我們分析UI效能卡頓的核心關注點,所以我們先看幾個重要的屬性說明,如下:

屬性名 含義
name 執行緒中調運的方法名;
Incl CPU Time 當前方法(包含內部調運的子方法)執行佔用的CPU時間;
Excl CPU Time 當前方法(不包含內部調運的子方法)執行佔用的CPU時間;
Incl Real Time 當前方法(包含內部調運的子方法)執行的真實時間,ms單位;
Excl Real Time 當前方法(不包含內部調運的子方法)執行的真實時間,ms單位;
Calls+Recur Calls/Total 當前方法被調運的次數及遞迴調運佔總調運次數百分比;
CPU Time/Call 當前方法調運CPU時間與調運次數比,即當前方法平均執行CPU耗時時間;
Real Time/Call 當前方法調運真實時間與調運次數比,即當前方法平均執行真實耗時時間;(重點關注)


有了對上面Traceview圖表的一個認識之後我們就來看看具體導致UI效能後該如何切入分析,一般Traceview可以定位兩類效能問題:

  • 方法調運一次需要耗費很長時間導致卡頓;

  • 方法調運一次耗時不長,但被頻繁調運導致累計時長卡頓。

譬如我們來舉個例項,有時候我們寫完App在使用時不覺得有啥大的影響,但是當我們啟動完App後靜止在那卻十分費電或者導致裝置發熱,這種情況我們就可以開啟Traceview然後按照Cpu Time/Call或者Real Time/Call進行降序排列,然後開啟可疑的方法及其child進行分析檢視,然後再回到程式碼定位檢查邏輯優化即可;當然了,我們也可以通過該工具來trace我們自定義View的一些方法來權衡效能問題,這裡不再一一列舉嘍。

可以看見,Traceview能夠幫助我們分析程式效能,已經很方便了,然而Traceview家族還有一個更加直觀強大的小工具,那就是可以通過dmtracedump生成方法呼叫圖。具體做法如下:

dmtracedump -g result.png target.trace  //結果png檔案  目標trace檔案

通過這個生成的方法調運圖我們可以更加直觀的發現一些方法的調運異常現象。不過本人優化到現在還沒怎麼用到它,每次用到Traceview分析就已經搞定問題了,所以說dmtracedump自己酌情使用吧。

PS一句,Android Studio新版本除過DDMS以外在CPU檢視的左側已經集成了Traceview(start Method Tracing)功能,只是用起來還是沒有DDMS的方便實用(這裡有一篇AS MT個人覺得不錯的分析文章(引用自網路,連結屬於原作者功勞)),如下圖:

這裡寫圖片描述

2-3-7 使用Systrace進行分析優化

Systrace其實有些類似Traceview,它是對整個系統進行分析(同一時間軸包含應用及SurfaceFlinger、WindowManagerService等模組、服務執行資訊),不過這個工具需要你的裝置核心支援trace(命令列檢查/sys/kernel/debug/tracing)且裝置是eng或userdebug版本才可以,所以使用前麻煩自己確認一下。

我們在分析UI效能時一般只關注圖形效能(所以必須選擇Graphics和View,其他隨意),同時一般對於卡頓的抓取都是5s,最多10s。啟動Systrace進行資料抓取可以通過兩種方式,命令列方式如下:

python systrace.py --time=10 -o mynewtrace.html sched gfx view wm

圖形模式:
開啟DDMS->Capture system wide trace using Android systrace->設定時間與選項點選OK就開始了抓取,接著操作APP,完事生成一個trace.html檔案,用Chrome開啟即可如下圖:

這裡寫圖片描述
在Chrome中瀏覽分析該檔案我們可以通過鍵盤的W-A-S-D鍵來搞定,由於上面我們在進行trace時選擇了一些選項,所以上圖生成了左上方相關的CPU頻率、負載、狀態等資訊,其中的CPU N代表了CPU核數,每個CPU行的柱狀圖表代表了當前時間段當前核上的執行資訊;下面我們再來看看SurfaceFlinger的解釋,如下:

這裡寫圖片描述

可以看見上面左邊欄的SurfaceFlinger其實就是負責繪製Android程式UI的服務,所以SurfaceFlinger能反應出整體繪製情況,可以關注上圖VSYNC-app一行可以發現前5s多基本都能夠達到16ms重新整理間隔,5s多開始到7s多大於了15ms,說明此時存在繪製丟幀卡頓;同時可以發現surfaceflinger一行明視訊記憶體在類似不規律間隔,這是因為有的地方是不需要重新渲染UI,所以有大範圍不規律,有的是因為阻塞導致不規律,明顯可以發現0到4s間大多是不需要渲染,而5s以後大多是阻塞導致;對應這個時間點我們放大可以看到每個部分所使用的時間和正在執行的任務,具體如下:
這裡寫圖片描述

可以發現具體的執行明視訊記憶體在超時效能卡頓(原點不是綠色的基本都代表存在一定問題,下面和右側都會提示你選擇的幀相關詳細資訊或者alert資訊),但是遺憾的是通過Systrace只能大體上發現是否存在效能問題,具體問題還需要通過Traceview或者程式碼中嵌入Trace工具類等去繼續詳細分析,總之很蛋疼。

PS:如果你想使用Systrace很輕鬆的分析定位所有問題,看明白所有的行含義,你還需要具備非常紮實的Android系統框架的原理才可以將該工具使用的得心應手。

2-3-8 使用traces.txt檔案進行ANR分析優化

ANR(Application Not Responding)是Android中AMS與WMS監測應用響應超時的表現;之所以把臭名昭著的ANR單獨作為UI效能卡頓的分析來說明是因為ANR是直接卡死UI不動且必須要解掉的Bug,我們必須儘量在開發時避免他的出現,當然了,萬一出現了那就用下面介紹的方法來分析吧。

我們應用開發中常見的ANR主要有如下幾類:

  • 按鍵觸控事件派發超時ANR,一般閾值為5s(設定中開啟ANR彈窗,預設有事件派發才會觸發彈框ANR);

  • 廣播阻塞ANR,一般閾值為10s(設定中開啟ANR彈窗,預設不彈框,只有log提示);

  • 服務超時ANR,一般閾值為20s(設定中開啟ANR彈窗,預設不彈框,只有log提示);

當ANR發生時除過logcat可以看見的log以外我們還可以在系統指定目錄下找到traces檔案或dropbox檔案進行分析,發生ANR後我們可以通過如下命令得到ANR trace檔案:

adb pull /data/anr/traces.txt ./

然後我們用txt編輯器開啟可以發現如下結構分析:

//顯示程序id、ANR發生時間點、ANR發生程序包名
----- pid 19073 at 2015-10-08 17:24:38 -----
Cmd line: com.example.yanbo.myapplication
//一些GC等object資訊,通常可以忽略
......
//ANR方法堆疊列印資訊!重點!
DALVIK THREADS (18):
"main" prio=5 tid=1 Sleeping
  | group="main" sCount=1 dsCount=0 obj=0x7497dfb8 self=0x7f9d09a000
  | sysTid=19073 nice=0 cgrp=default sched=0/0 handle=0x7fa106c0a8
  | state=S schedstat=( 125271779 68162762 280 ) utm=11 stm=1 core=0 HZ=100
  | stack=0x7fe90d3000-0x7fe90d5000 stackSize=8MB
  | held mutexes=
  at java.lang.Thread.sleep!(Native method)
  - sleeping on <0x0a2ae345> (a java.lang.Object)
  at java.lang.Thread.sleep(Thread.java:1031)
  - locked <0x0a2ae345> (a java.lang.Object)
//真正導致ANR的問題點,可以發現是onClick中有sleep導致。我們平時可以類比分析即可,這裡不詳細說明。
  at java.lang.Thread.sleep(Thread.java:985)
  at com.example.yanbo.myapplication.MainActivity$1.onClick(MainActivity.java:21)
  at android.view.View.performClick(View.java:4908)
  at android.view.View$PerformClick.run(View.java:20389)
  at android.os.Handler.handleCallback(Handler.java:815)
  at android.os.Handler.dispatchMessage(Handler.java:104)
  at android.os.Looper.loop(Looper.java:194)
  at android.app.ActivityThread.main(ActivityThread.java:5743)
  at java.lang.reflect.Method.invoke!(Native method)
  at java.lang.reflect.Method.invoke(Method.java:372)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:988)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:783)
......
//省略一些不常關注堆疊列印
......

至此常見的應用開發中ANR分析定位就可以解決了。

2-4 應用UI效能分析解決總結

可以看見,關於Android UI卡頓的效能分析還是有很多工具的,上面只是介紹了應用開發中我們經常使用的一些而已,還有一些其他的,譬如Oprofile等工具不怎麼常用,這裡就不再詳細介紹。

通過上面UI效能的原理、原因、工具分析總結可以發現,我們在開發應用時一定要時刻重視效能問題,如若真的沒留意出現了效能問題,不妨使用上面的一些案例方式進行分析。但是那終歸是補救措施,在我們知道上面UI卡頓原理之後我們應該儘量從專案程式碼架構搭建及編寫時就避免一些UI效能問題,具體專案中常見的注意事項如下:

  • 佈局優化;儘量使用include、merge、ViewStub標籤,儘量不存在冗餘巢狀及過於複雜佈局(譬如10層就會直接異常),儘量使用GONE替換INVISIBLE,使用weight後儘量將width和heigh設定為0dp減少運算,Item存在非常複雜的巢狀時考慮使用自定義Item View來取代,減少measure與layout次數等。

  • 列表及Adapter優化;儘量複用getView方法中的相關View,不重複獲取例項導致卡頓,列表儘量在滑動過程中不進行UI元素重新整理等。

  • 背景和圖片等記憶體分配優化;儘量減少不必要的背景設定,圖片儘量壓縮處理顯示,儘量避免頻繁記憶體抖動等問題出現。

  • 自定義View等繪圖與佈局優化;儘量避免在draw、measure、layout中做過於耗時及耗記憶體操作,尤其是draw方法中,儘量減少draw、measure、layout等執行次數。

  • 避免ANR,不要在UI執行緒中做耗時操作,遵守ANR規避守則,譬如多次資料庫操作等。

當然了,上面只是列出了我們專案中常見的一些UI效能注意事項而已,相信還有很多其他的情況這裡沒有說到,歡迎補充。還有一點就是我們上面所謂的UI效能優化分析總結等都是建議性的,因為效能這個問題是一個涉及面很廣很泛的問題,有些優化不是必需的,有些優化是必需的,有些優化掉以後又是得不償失的,所以我們一般著手解決那些必須的就可以了。

3 應用開發Memory記憶體效能分析優化

說完了應用開發中的UI效能問題後我們就該來關注應用開發中的另一個重要、嚴重、非常重要的效能問題了,那就是記憶體效能優化分析。Android其實就是嵌入式裝置,嵌入式裝置核心關注點之一就是記憶體資源;有人說現在的裝置都在堆硬體配置(譬如國產某米的某兔跑分手機、盒子等),所以記憶體不會再像以前那麼緊張了,其實這句話聽著沒錯,但為啥再牛逼配置的Android裝置上有些應用還是越用系統越卡呢?這裡面的原因有很多,不過相信有了這一章下面的內容分析,作為一個移動開發者的你就有能力打理好自己應用的那一畝三分地記憶體了,能做到這樣就足以了。關於Android記憶體優化,這裡有一篇Google的官方指導文件,但是本文為自己專案摸索,會有很多不一樣的地方。

3-1 Android記憶體管理原理

系統級記憶體管理:

Android系統核心是基於Linux,所以說Android的記憶體管理其實也是Linux的升級版而已。Linux在程序停止後就結束該程序,而Android把這些停止的程序都保留在記憶體中,直到系統需要更多記憶體時才選擇性的釋放一些,保留在記憶體中的程序預設(不包含後臺service與Thread等單獨UI執行緒的程序)不會影響整體系統的效能(速度與電量等)且當再次啟動這些保留在記憶體的程序時可以明顯提高啟動速度,不需要再去載入。

再直白點就是說Android系統級記憶體管理機制其實類似於Java的垃圾回收機制,這下明白了吧;在Android系統中框架會定義如下幾類程序、在系統記憶體達到規定的不同level閾值時觸發清空不同level的程序型別。

這裡寫圖片描述

可以看見,所謂的我們的Service在後臺跑著跑著掛了,或者盒子上有些大型遊戲啟動起來就掛(之前我在上家公司做盒子時遇見過),有一個直接的原因就是這個閾值定義的太大,導致系統一直認為已經達到閾值,所以進行優先清除了符合型別的程序。所以說,該閾值的設定是有一些講究的,額,扯多了,我們主要是針對應用層記憶體分析的,系統級記憶體回收瞭解這些就基本夠解釋我們應用在裝置上的一些表現特徵了。

應用級記憶體管理:

在說應用級別記憶體管理原理時大家先想一個問題,假設有一個記憶體為1G的Android裝置,上面運行了一個非常非常吃記憶體的應用,如果沒有任何機制的情況下是不是用著用著整個裝置會因為我們這個應用把1G記憶體吃光然後整個系統執行癱瘓呢?

哈哈,其實Google的工程師才不會這麼傻的把系統設計這麼差勁。為了使系統不存在我們上面假想情況且能安全快速的執行,Android的框架使得每個應用程式都執行在單獨的程序中(這些應用程序都是由Zygote程序孵化出來的,每個應用程序都對應自己唯一的虛擬機器例項);如果應用在執行時再存在上面假想的情況,那麼癱瘓的只會是自己的程序,不會直接影響系統執行及其他程序執行。

既然每個Android應用程式都執行在自己的虛擬機器中,那瞭解Java的一定明白,每個虛擬機器必定會有堆記憶體閾值限制(值得一提的是這個閾值一般都由廠商依據硬體配置及裝置特性自己設定,沒有統一標準,可以為64M,也可以為128M等;它的配置是在Android的屬性系統的/system/build.prop中配置dalvik.vm.heapsize=128m即可,若存在dalvik.vm.heapstartsize則表示初始申請大小),也即一個應用程序同時存在的物件必須小於閾值規定的記憶體大小才可以正常執行。

接著我們執行的App在自己的虛擬機器中記憶體管理基本就是遵循Java的記憶體管理機制了,系統在特定的情況下主動進行垃圾回收。但是要注意的一點就是在Android系統中執行垃圾回收(GC)操作時所有執行緒(包含UI執行緒)都必須暫停,等垃圾回收操作完成之後其他執行緒才能繼續執行。這些GC垃圾回收一般都會有明顯的log打印出回收型別,常見的如下:

  • GC_MALLOC——記憶體分配失敗時觸發;

  • GC_CONCURRENT——當分配的物件大小超過一個限定值(不同系統)時觸發;

  • GC_EXPLICIT——對垃圾收集的顯式呼叫(System.gc()) ;

  • GC_EXTERNAL_ALLOC——外部記憶體分配失敗時觸發;

通過上面這幾點的分析可以發現,應用的記憶體管理其實就是一個蘿蔔一個坑,坑都一般大,你在開發應用時要保證的是記憶體使用同一時刻不能超過坑的大小,否則就裝不下了。

3-2 Android記憶體洩露效能分析

有了關於Android的一些記憶體認識,接著我們來看看關於Android應用開發中常出現的一種記憶體問題—-記憶體洩露。

3-2-1 Android應用記憶體洩露概念

眾所周知,在Java中有些物件的生命週期是有限的,當它們完成了特定的邏輯後將會被垃圾回收;但是,如果在物件的生命週期本來該被垃圾回收時這個物件還被別的物件所持有引用,那就會導致記憶體洩漏;這樣的後果就是隨著我們的應用被長時間使用,他所佔用的記憶體越來越大。如下就是一個最常見簡單的洩露例子(其它的洩露不再一一列舉了):

public final class MainActivity extends Activity {
    private DbManager mDbManager;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        //DbManager是一個單例模式類,這樣就持有了MainActivity引用,導致洩露
        mDbManager = DbManager.getInstance(this);
    }
}

可以看見,上面例子中我們讓一個單例模式的物件持有了當前Activity的強引用,那在當前Acvitivy執行完onDestroy()後,這個Activity就無法得到垃圾回收,也就造成了記憶體洩露。

記憶體洩露可以引發很多的問題,常見的記憶體洩露導致問題如下:

  • 應用卡頓,響應速度慢(記憶體佔用高時JVM虛擬機器會頻繁觸發GC);

  • 應用被從後臺程序幹為空程序(上面系統記憶體原理有介紹,也就是超過了閾值);

  • 應用莫名的崩潰(上面應用記憶體原理有介紹,也就是超過了閾值OOM);

造成記憶體洩露洩露的最核心原理就是一個物件持有了超過自己生命週期以外的物件強引用導致該物件無法被正常垃圾回收;可以發現,應用記憶體洩露是個相當棘手重要的問題,我們必須重視。

3-2-2 Android應用記憶體洩露察覺手段

知道了記憶體洩露的概念之後肯定就是想辦法來確認自己的專案是否存在記憶體洩露了,那該如何察覺自己專案是否存在記憶體洩露呢?如下提供了幾種常用的方式:

察覺方式 場景
AS的Memory視窗 平時用來直觀瞭解自己應用的全域性記憶體情況,大的洩露才能有感知。
DDMS-Heap記憶體監測工具 同上,大的洩露才能有感知。
dumpsys meminfo命令 常用方式,可以很直觀的察覺一些洩露,但不全面且常規足夠用。
leakcanary神器 比較強大,可以感知洩露且定位洩露;實質是MAT原理,只是更加自動化了,當現有程式碼量已經龐大成型,且無法很快察覺掌控全域性程式碼時極力推薦;或者是偶現洩露的情況下極力推薦。

AS的Memory視窗如下,詳細的說明這裡就不解釋了,很簡單很直觀(使用頻率高):

這裡寫圖片描述

DDMS-Heap記憶體監測工具視窗如下,詳細的說明這裡就不解釋了,很簡單(使用頻率不高):

這裡寫圖片描述

dumpsys meminfo命令如下(使用頻率非常高,非常高效,我的最愛之一,平時一般關注幾個重要的Object個數即可判斷一般的洩露;當然了,adb shell dumpsys meminfo不跟引數直接展示系統所有記憶體狀態):

這裡寫圖片描述

leakcanary神器使用這裡先不說,下文會專題介紹,你會震撼的一B。有了這些工具的定位我們就能很方便的察覺我們App的記憶體洩露問題,察覺到以後該怎麼定位分析呢,繼續往下看。

3-2-3 Android應用記憶體洩露leakcanary工具定位分析

leakcanary是一個開源專案,一個記憶體洩露自動檢測工具,是著名的GitHub開源組織Square貢獻的,它的主要優勢就在於自動化過早的發覺記憶體洩露、配置簡單、抓取貼心,缺點在於還存在一些bug,不過正常使用百分之九十情況是OK的,其核心原理與MAT工具類似。

關於leakcanary工具的配置使用方式這裡不再詳細介紹,因為真的很簡單,詳情點我參考官方教程學習使用即可

PS:之前在優化效能時發現我們有一個應用有兩個介面退出後Activity沒有被回收(dumpsys meminfo發現一直在加),所以就懷疑可能存在記憶體洩露。但是問題來了,這兩個Activity的邏輯十分複雜,程式碼也不是我寫的,相關聯的程式碼量也十分龐大,更加鬱悶的是很難判斷是哪個版本修改導致的,這時候只知道有洩露,卻無法定位具體原因,使用MAT分析解決掉了一個可疑洩露後發現洩露又變成了概率性的。可以發現,對於這種概率性的洩露用MAT去主動抓取肯定是很耗時耗力的,所以決定直接引入leakcanary神器來檢測專案,後來很快就徹底解決了專案中所有必現的、偶現的記憶體洩露。

總之一點,工具再強大也只是幫我們定位可能的洩露點,而最核心的GC ROOT洩露資訊推匯出洩露問題及如何解決還是需要你把住程式碼邏輯及洩露核心概念去推理解決。

3-2-4 Android應用記憶體洩露MAT工具定位分析

Eclipse Memory Analysis Tools(點我下載)是一個專門分析Java堆資料記憶體引用的工具,我們可以使用它方便的定位記憶體洩露原因,核心任務就是找到GC ROOT位置即可,哎呀,關於這個工具的使用我是真的不想說了,自己搜尋吧,實在簡單、傳統的不行了。

PS:這是開發中使用頻率非常高的一個工具之一,麻煩務必掌握其核心使用技巧,雖然Android Studio已經實現了部分功能,但是真的很難用,遇到問題目前還是使用Eclipse Memory Analysis Tools吧。

原諒我該小節的放蕩不羈!!!!(其實我是困了,嗚嗚!)

3-2-5 Android應用開發規避記憶體洩露建議

有了上面的原理及案例處理其實還不夠,因為上面這些處理辦法是補救的措施,我們正確的做法應該是在開發過程中就養成良好的習慣和敏銳的嗅覺才對,所以下面給出一些應用開發中常見的規避記憶體洩露建議:

  • Context使用不當造成記憶體洩露;不要對一個Activity Context保持長生命週期的引用(譬如上面概念部分給出的示例)。儘量在一切可以使用應用ApplicationContext代替Context的地方進行替換(原理我前面有一篇關於Context的文章有解釋)。

  • 非靜態內部類的靜態例項容易造成記憶體洩漏;即一個類中如果你不能夠控制它其中內部類的生命週期(譬如Activity中的一些特殊Handler等),則儘量使用靜態類和弱引用來處理(譬如ViewRoot的實現)。

  • 警惕執行緒未終止造成的記憶體洩露;譬如在Activity中關聯了一個生命週期超過Activity的Thread,在退出Activity時切記結束執行緒。一個典型的例子就是HandlerThread的run方法是一個死迴圈,它不會自己結束,執行緒的生命週期超過了Activity生命週期,我們必須手動在Activity的銷燬方法中中調運thread.getLooper().quit();才不會洩露。

  • 物件的註冊與反註冊沒有成對出現造成的記憶體洩露;譬如註冊廣播接收器、註冊觀察者(典型的譬如資料庫的監聽)等。

  • 建立與關閉沒有成對出現造成的洩露;譬如Cursor資源必須手動關閉,WebView必須手動銷燬,流等物件必須手動關閉等。

  • 不要在執行頻率很高的方法或者迴圈中建立物件,可以使用HashTable等建立一組物件容器從容器中取那些物件,而不用每次new與釋放。

  • 避免程式碼設計模式的錯誤造成記憶體洩露。

關於規避記憶體洩露上面我只是列出了我在專案中經常遇見的一些情況而已,肯定不全面,歡迎拍磚!當然了,只有我們做到好的規避加上強有力的判斷嗅覺洩露才能讓我們的應用駕馭好自己的一畝三分地。

3-3 Android記憶體溢位OOM效能分析

上面談論了Android應用開發的記憶體洩露,下面談談記憶體溢位(OOM);其實可以認為記憶體溢位與記憶體洩露是交集關係,具體如下圖:

這裡寫圖片描述

下面我們就來看看記憶體溢位(OOM)相關的東東吧。

3-3-1 Android應用記憶體溢位OOM概念

上面我們探討了Android記憶體管理和應用開發中的記憶體洩露問題,可以知道記憶體洩露一般影響就是導致應用卡頓,但是極端的影響是使應用掛掉。前面也提到過應用的記憶體分配是有一個閾值的,超過閾值就會出問題,這裡我們就來看看這個問題—–記憶體溢位(OOM–OutOfMemoryError)。

記憶體溢位的主要導致原因有如下幾類:

  • 應用程式碼存在記憶體洩露,長時間積累無法釋放導致OOM;

  • 應用的某些邏輯操作瘋狂的消耗掉大量記憶體(譬如載入一張不經過處理的超大超高清圖片等)導致超過閾值OOM;

可以發現,無論哪種型別,導致記憶體溢位(OutOfMemoryError)的核心原因就是應用的記憶體超過閾值了。

3-3-2 Android應用記憶體溢位OOM效能分析

通過上面的OOM概念和那幅交集圖可以發現,要想分析OOM原因和避免OOM需要分兩種情況考慮,洩露導致的OOM,申請過大導致的OOM。

記憶體洩露導致的OOM分析:

這種OOM一旦發生後會在logcat中列印相關OutOfMemoryError的異常棧資訊,不過你別高興太早,這種情況下導致的OOM列印異常資訊是沒有太大作用,因為這種OOM的導致一般都如下圖情況(圖示為了說明問題資料和場景有誇張,請忽略):

這裡寫圖片描述

從圖片可以看見,這種OOM我們有時也遇到,第一反應是去分析OOM異常列印棧,可是後來發現列印棧列印的地方沒有啥問題,沒有可優化的餘地了,於是就鬱悶了。其實這時候你留心觀察幾個現象即可,如下:

  • 留意你執行觸發OOM操作前的介面是否有卡頓或者比較密集的GC列印;
  • 使用命令檢視下當前應用佔用記憶體情況;

確認了以上這些現象你基本可以斷定該OOM的log真的沒用,真正導致問題的原因是記憶體洩露,所以我們應該按照上節介紹的方式去著手排查記憶體洩露問題,解決掉記憶體洩露後紅色空間都能得到釋放,再去顯示一張0.8M的優化圖片就不會再報OOM異常了。

不珍惜記憶體導致的OOM分析:

上面說了記憶體洩露導致的OOM異常,下面我們再來看一幅圖(資料和場景描述有誇張,請忽略),如下:

這裡寫圖片描述

可見,這種型別的OOM就很好定位原因了,一般都可以從OOM後的log中得出分析定位。

如下例子,我們在Activity中的ImageView放置一張未優化的特大的(30多M)高清圖片,執行直接崩潰如下:

//丟擲OOM異常
10-10 09:01:04.873 11703-11703/? E/art: Throwing OutOfMemoryError "Failed to allocate a 743620620 byte allocation with 4194208 free bytes and 239MB until OOM"
10-10 09:01:04.940 11703-11703/? E/art: Throwing OutOfMemoryError "Failed to allocate a 743620620 byte allocation with 4194208 free bytes and 239MB until OOM"
//堆疊列印
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: FATAL EXCEPTION: main
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: Process: com.example.application, PID: 11703
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.application/com.example.myapplication.MainActivity}: android.view.InflateException: Binary XML file line #21: Error inflating class <unknown>
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2610)
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2684)
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at android.app.ActivityThread.access$800(ActivityThread.java:177)
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1542)
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at android.os.Handler.dispatchMessage(Handler.java:111)
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at android.os.Looper.loop(Looper.java:194)
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at android.app.ActivityThread.main(ActivityThread.java:5743)
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at java.lang.reflect.Method.invoke(Native Method)
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at java.lang.reflect.Method.invoke(Method.java:372)
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at com