Android深度效能測試:功能決定現在,效能決定未來!

木子1990發表於2017-03-09

深度效能測試能協助測試人員發現APP中存在的深層次效能問題,直接定位多項效能問題及瓶頸的根本原因,方便開發者快速提升APP效能表現,使得APP執行得更加穩定。MQC深度效能測試能夠幫助開發者發現深層次的效能問題,更精準地定位問題。

功能決定現在,效能決定未來!

一、 記憶體洩漏

記憶體洩漏是指由於程式碼編寫不當導致不再使用的物件無法得到及時釋放。記憶體洩漏產生的記憶體垃圾不僅浪費資源,拖慢執行效率,甚至還可能造成記憶體溢位,直接導致應用崩潰。

對於Android應用,比較容易發生洩漏的是Activity、Fragment物件,此類物件的共性是其都有一定的生命週期。以Activity為例,一個Activity例項的生命起始於onCreate(),終結於onDestroy()。當一個Activity不再使用時,系統會呼叫回撥方法Activity.onDestroy()方法做一些清理操作。但是對於Activity物件本身所佔記憶體,則完全由虛擬機器的垃圾回收器來完成回收。垃圾回收器會檢查該例項是否被持有強引用,如果存在指向該物件的強引用,則不會回收其所佔記憶體空間,這塊記憶體空間也就成了記憶體垃圾。由此可見記憶體洩漏是由不當的強引用導致的。

MQC支援對Activity、Fragment物件的記憶體洩漏檢測,檢測結果可在效能報告-效能問題模組檢視。

 

1.1 物件的引用鏈

從GC ROOT到洩漏物件的引用鏈能精準地定位導致記憶體洩漏的原因。物件無法被垃圾回收器回收,一定是由於GC ROOT直接或間接持有了它的強引用。

常見的GCROOT有:宣告為static的變數,未停止的執行緒,Application物件,甚至是棧記憶體中的區域性變數。

1.2 Android中常見的記憶體洩露      

a.集合中物件沒清理造成的記憶體洩露

程式設計過程中,我們常常會把一些物件加入到集合中。在我們不再需要該物件時,如果沒有及時把它從集合中清理掉,就會導致這個集合佔用的記憶體越來越大。同時如果這個集合是靜態的話,那情況就更嚴重了。如下的程式碼段中在每次啟動Activity的時候都往靜態集合中新增了一個物件,如果Activity被頻繁啟動,set將不斷變大,影響APP的正常執行。

     

所以,集合中不再使用的物件應及時釋放掉。上述程式碼應該在Activity的onDestroy()方法中,及時清理set裡的元素,避免無用物件繼續存在強引用,例如:

這樣可以保證set持有的強引用都被釋放。      

b. 單例模式造成的記憶體洩漏

單例的靜態特性使得其生命週期可能跟應用的生命週期一樣長,如果使用不恰當的話,很容易造成記憶體洩漏。

如下程式碼是一個簡單的單例模式實現:


 

在建立單例的時候,如果我們傳入當前Activity的Context,例如:

單例testContextHelper裡面一直儲存著該Activity的引用,當這個Context 對應的 Activity 退出時,由於該 Context 的引用一直被單例物件持有,所以該Activity佔用的記憶體並不會被回收,造成洩漏。在使用單例模式時,一定要避免持有短生命週期物件的引用,比如上述程式碼在引用Context時可以使用Application的Context代替Activity的Context,即:

 

因為Application在應用的執行過程中一直存在,不會退出。 

c. 非靜態內部類建立靜態例項造成的記憶體洩漏

在啟動頻繁的Activity中,為了避免反覆建立某些資源,提高載入速度,我們可能會在Activity內部建立一個靜態例項,每次啟動Activity時都會使用該例項,如下程式碼:

 

此時Activity內部有一個靜態單例,且為非靜態內部類的例項。由於非靜態內部類預設會持有外部類的引用,並且該類建立了一個靜態例項,該例項的生命週期和應用的一樣長,這就導致了該靜態例項一直會持有該Activity的引用,導致Activity的記憶體資源不能正常回收。為了避免這一問題,在使用過程中,正確的做法是將內部類設為靜態類或者變成單獨的類。 

d. 使用handler時的記憶體問題

在Android應用中,Handler通過傳送Message與其他執行緒互動,發出的Message被儲存在目標執行緒的MessageQueue中的,並且Message不一定馬上就被處理,駐留時間可能比較久。比如我們用Handler傳送一個延時比較久的Message:

而Message中持有Handler例項的強引用,如果Message在Queue中一直存在,就會導致Handler例項無法被回收,而Handler持有Activity的強引用,Activity物件也不會被回收,這就造成了例項洩露。所以,在建立Handler時,最好使用弱引用來引用目標Activity物件,比如:

 

這樣可以避免由於Handler持有強引用導致Activity無法回收。 

e. 靜態成員變數造成的記憶體洩露

如果成員變數被宣告為 static,其生命週期將與整個應用程式的生命週期一樣。如果靜態變數直接或間接強引用了某一短生命週期物件(比如Activity),這會導致即使app切到後臺,這部分記憶體也不會被釋放。下面的錯誤示範程式碼中,在Activity啟動的時候,直接將其引用賦給了靜態變數obj,會導致該Activity一直不能被回收,導致記憶體洩露。

 

 

因此,在使用靜態變數時,應該避免其持有短生命週期物件的強引用,可以使用弱引用來代替強引用。 

 

f. 資源未關閉造成的記憶體洩漏

對於使用了BroadcastReceiver,ContentObserver,File,遊標Cursor,Stream,Bitmap等資源的使用,應該在Activity銷燬時及時關閉或者登出,否則這些資源將可能不會被回收,造成記憶體洩漏。雖然有些系統程式,它本身可以自動取消註冊的(非即時),但是我們還是應該在我們的程式中明確的取消註冊,程式結束時應該把所有的註冊都取消掉。

 

1.3 MQC提供的記憶體洩露分析

MQC提供的深度效能測試能夠幫助您發現並定位發生了記憶體洩露的地方。當發生記憶體洩漏時,測試報告中會給出發生洩露的記憶體大小,洩露型別,發生洩露的物件,以及該物件的引用鏈等資訊,下圖是MQC檢測到的APP記憶體洩漏案例。

 

圖中可以看到檢測到了兩條記憶體洩漏資訊。並指出了洩露記憶體的大小和物件型別,均為Activity物件洩露。檢視引用鏈得知,APP在ActivityManager裡面持有了所有Activity的強引用,最終導致Activity退出後無法回收,屬於前述介紹的集合物件使用不當造成的記憶體洩露。可以看到,MQC提供的記憶體洩漏分析能直接定位到相關程式碼,方便您快速修復BUG。

 

二、 記憶體溢位

記憶體溢位(OOM, Out OfMemory)是指當已存在的物件的佔用了絕大部分或者全部分配給該程式的記憶體空間時,如果程式再申請新的記憶體空間,由於沒有空餘記憶體可用於分配,或可分配的記憶體不夠滿足申請者的需求,此時系統就會丟擲記憶體溢位異常。

2.1 常見的記憶體溢位原因

很大一部分記憶體溢位都是由於記憶體洩露導致,由於已分配的記憶體被洩露物件佔用並且無法釋放,隨著洩露的物件例項越來越多,導致可用記憶體越來越少,最終當記憶體耗盡時,系統就會丟擲記憶體溢位異常。此時只要解決了記憶體洩露,也就解決了記憶體溢位。

另一個記憶體溢位的重要原因就是應用載入了多個佔用記憶體較多的物件。比如應用在執行過程中載入並儲存了多個較大的Bitmap,導致可用記憶體急劇減少。因此,在程式碼編寫過程中,對於可能佔據大量記憶體空間的物件,我們應該使用軟引用或虛引用持有該物件,使得系統GC能在記憶體吃緊時回收該物件釋放空間。並且在不使用Bitmap時,應及時recycle,主動釋放記憶體空間。

2.2 MQC提供的記憶體溢位分析

在應用丟擲記憶體溢位時,深度效能測試會主動捕獲這一異常,給出丟擲該異常的堆疊資訊。並分析當前應用程式佔用的總記憶體大小,已分配的記憶體大小和可用記憶體大小,方便開發者定位問題。

如下圖,測試報告中首先給出發生記憶體溢位的機型,同時指出檢測到記憶體溢位時應用自身和裝置記憶體的使用情況,可以看到Native Heap和VM Heap的空餘記憶體都已不多。開啟StackTrace後,可以看到出現OOM錯誤的程式碼行,由此我們發現可能是在載入Bitmap的時候導致的記憶體溢位。圖中紅色箭頭所指的地方是應用自身的程式碼,我們根據這些提示就能夠快速找到原始檔中出錯的程式碼,立即修復。

 

三、記憶體抖動

記憶體抖動指的是短時間內大量物件被建立和回收。由於短時間內產生了大量的物件,需要分配大量記憶體,此時需要垃圾回收器(GC)頻繁工作,回收不再使用的物件來騰出記憶體空間。GC的頻繁啟動佔用了一定的系統資源,最終影響應用表現。

3.1 常見的記憶體抖動

常見的記憶體抖動主要是由於在迴圈或其他場合中不停地建立新物件,並且短時間內這些物件又被釋放。瞬間產生大量的物件會嚴重佔用記憶體區域,當達到閥值,剩餘空間不夠的時候觸發GC。即使每次分配的物件佔用了很少的記憶體,但是他們疊加在一起會增加Heap的壓力。GC啟動時會佔用CPU等資源,直接導致應用執行受到影響,可能出現介面操作不流暢等現象。

3.2 MQC提供的記憶體抖動分析

MQC能夠監控系統的每一次GC,並給出GC發生時的記憶體使用情況,如下圖所示。

 

圖中給出了3種GC發生的時刻和記憶體變化的曲線圖。3種GC分別為:

GC_EXPLICIT:應用主動呼叫System.gc()產生的GC事件

GC_FOR_ALLOC:記憶體分配時,發現可用記憶體不夠時觸發的GC事件

GC_CONCURRENT:已分配的記憶體大小達到某一閾值時會觸發的GC事件

其中後兩種是系統自己決定啟動的GC,應用無法控制。但是我們可以優化程式碼,避免頻繁生成和回收物件,比如不要在迴圈中頻繁new新的物件。

 

四、介面卡頓

介面卡頓指的是短時間內介面對使用者操作沒有響應。應用在出現卡頓的時候,就算知道是哪個頁面出了問題,但是很難定位到具體的程式碼。應用卡頓檢測就是幫助您快速定位卡頓的具體位置,方便您進行鍼對性的修復。 

4.1 常見的介面卡頓原因

Android應用的UI繪製和使用者操作訊息分發都發生在應用主執行緒,如果主執行緒來不及處理UI更新和響應使用者操作,使用者就會感覺應用發生了卡頓。因此卡頓發生時嚐嚐伴隨著主執行緒阻塞。如果在主執行緒中進行磁碟讀寫、網路操作或者大量計算時,嚐嚐會導致主執行緒被阻塞,發生介面卡頓。

4.2 MQC提供的介面卡頓分析

 


如上圖所示,在應用執行過程中出現卡頓時,MQC會記錄當前卡頓的時長,例如圖中為1935ms,用於給開發者評估本次卡頓的嚴重性,隨後給出發生卡頓時系統CPU和記憶體的使用情況等資訊輔助開發者分析問題。也會給出卡頓發生時的應用呼叫的完整堆疊,用於定位發生卡頓的程式碼,MQC同時歸納出具體的關鍵程式碼,免去開發者在大量堆疊中尋找關鍵行的麻煩。

 

五、過度繪製

過度繪製一般指的是螢幕上的某些區域在一幀中被多次繪製,一般是在介面的同一個地方疊加了多個控制元件。這樣會加重GPU的工作負擔,可能導致應用執行過程中頻繁掉幀,影響使用者體驗。

5.1 過度繪製詳細介紹

當手機開啟過度繪製時,螢幕上會標記發生過度繪製的區域,並根據不同的繪製次數使用不同的顏色,顏色標識從好到差依次是:藍色-綠色-淡紅色-紅色,分別代表該區域被繪製1次、2次、3次和4次。一般情況下,最好把繪製控制在2次以下,3次繪製有時候是不能避免的,儘量避免,4次的繪製基本上是不允許的。

為了減少過度繪製,開發者應減少複雜的、層級較多的佈局,去掉多餘的背景色。簡單的介面儘量使用線性佈局;較為複雜的介面可以使用相對佈局,避免巢狀過多的線性佈局。可以使用ViewStub來動態載入介面。

5.2 MQC提供的過度繪製分析

MQC實時監測介面的過度繪製指數,當該指數大於1.5時,MQC認為該介面可能需要優化。最終,測試報告中會指出應用每個介面的過度繪製指數,並配合測試視訊將過度繪製指數與Activity關聯起來,並告訴開發者該介面對應的Activity。如下圖所示。


 

六、啟動分析

啟動分析通過分析應用啟動過程產生的trace檔案來得到應用的啟動時間等資訊。通常來說,Android應用的啟動方式分為兩種:冷啟動和熱啟動。

冷啟動:當啟動應用時,後臺沒有該應用的程式,此時系統會建立一個新的程式分配給該應用。冷啟動因為系統會建立一個新的程式分配給它,所以會先建立和初始化Application類,隨後建立和初始化MainActivity類(包括一系列的測量、佈局、繪製),最後顯示在介面上。

熱啟動:當啟動應用時,後臺已有該應用的程式(例:按back鍵、home鍵,應用雖然會退出,但是該應用的程式是依然會保留在後臺,可進入任務列表檢視),這種啟動會從已有的程式中來啟動應用。熱啟動因為會從已有的程式中來啟動,所以熱啟動就不會建立新的Application,而是直接建立和初始化MainActivity,而不必建立和初始化Application,因為一個應用從新程式的建立到程式的銷燬,Application只會初始化一次。一般來講,熱啟動時間都會在一定程度上小於冷啟動時間。

MQC會分析應用的冷啟動時間和熱啟動時間,給開發者作為參考。同時給出啟動分階段耗時分析、耗時方法定位、啟動過程函式呼叫關係等更詳細的資訊,可以幫助開發者快速發現啟動到底卡在哪了。

七、嚴苛模式(StrictMode)

嚴苛模式(StrictMode)是一個開發輔助工具,可以幫助開發者發現那些由於編碼過程中不注意而造成的問題。 

7.1嚴苛模式的詳細介紹

StrictMode經常用於捕獲那些在應用主執行緒中進行的磁碟讀寫操作和網路請求。由於應用主執行緒是接收UI操作訊息和執行介面渲染的地方,為了使應用執行更加流暢和更快響應,請儘量不要在主執行緒執行磁碟操作和網路請求。當然,這也是避免系統彈出ANR對話方塊和提高應用穩定性的好方法。一旦檢測到違反策略(policyviolation),系統將會輸出一條相關的日誌,其一般包含一個呼叫棧,來顯示應用在何處發生違例。

注意:儘管Android裝置的磁碟一般都是快閃記憶體盤,然而實際中很多裝置只能以很有限的併發數來操作檔案系統。雖然磁碟讀寫很快,但是具體過程中可能由於其他程式佔用了I/O介面,等待的過程會導致整個磁碟操作流程比較慢。如果可以,請儘量假設磁碟讀寫是一個比較耗時的操作。

StricMode除了可以檢測主執行緒的磁碟操作和網路請求以外,還可以發現主執行緒中執行時間較長的方法。當應用中有繼承了Closeable介面的物件沒有關閉的時候,例如檔案流等,或者沒有使用HTTPS進行網路請求,或者同一個Activity的例項太多,StrictMode都會給出提示。其能發現的錯誤主要包括:

a.應用在主執行緒中進行磁碟讀寫;

b.應用在主執行緒中進行網路請求;

c.應用在主執行緒中的某些自定義方法的執行時間比較長;

d.SQLCursor物件在使用之後沒有關閉;

e.繼承了Closeable介面的物件在使用之後沒有關閉;

f.  某一Activity有較多的例項;

g.檔案讀取介面暴露給外部應用;

h.註冊某些物件(廣播接收器、觀察者、Listener等)後沒有取消註冊;

i.  沒有使用加密網路(HTTPS)進行網路資料傳輸。

7.2 MQC提供的嚴苛模式檢測

MQC深度效能測試檢測到您的應用存在違反上述要求的時候,MQC首先會指出應用違反了哪些嚴苛模式的策略,隨後分析問題發生時的應用堆疊資訊,指出問題出現在哪兒,並統計該問題出現了多少次。針對在檢測到的主執行緒操作(例如出現主執行緒磁碟操作,主執行緒網路操作等)時,還會給出該操作的持續時間等資訊,輔助開發者評估問題的嚴重程度。

上圖顯示MQC檢測到應用中存在主執行緒IO的情況,具體是在主執行緒中進行了檔案讀取操作,最長的一次持續了2356毫秒,測試過程中一共出現了62次磁碟讀取操作。根據後面的堆疊資訊,可以看到com.stephen.performance.MainActivity類裡面的readFile方法是在主執行緒中執行的,因此這裡我們就可以針對這一資訊來進行修改。

功能決定現在,效能決定未來!歡迎大家來MQC免費體驗深度效能測試


相關文章