1. 程式人生 > >Android 進階第二篇——效能優化

Android 進階第二篇——效能優化

Android 進階第二篇——效能優化

 

一些Android書籍喜歡把效能優化放在最後的章節,簡單提一提作為內容全面的點綴。在這裡我將工具使用和效能優化的一些個人經驗放在進階系列部落格的開始,因為我認為防病永遠比治病有意義重要得多。我們不應該等到一個問題已經發生了,並且到了一定程度才想起來需要重構程式碼或者進行效能優化,通過早早的學習效能優化的思維和工具能避免很多問題,糾正一些不良的編碼習慣,對Coder的編碼能力提高具有很大的意義。

UI介面優化

UI介面就是和使用者打交道的前臺,UI介面的優化直接關乎使用者體驗,因此UI介面優化是重中之重。在這裡,我個人將UI介面優化分兩部分內容,一是啟動速度優化,二是UI渲染優化。 
1.啟動速度優化

啟動速度實際上也是很關乎體驗的,我個人在使用一些app的時候,點起來之後半天才進入正題往往讓我非常反感,下次就不想使用了。當然很多時候這是人為造成的,為了投放廣告,讓廣告顯示一段時間,這種就不在討論之內了。目前很多APP都有一個Splash介面,主要是一個過渡,在載入顯示Splash介面的時候做一些資源載入,全部準備就緒後再進入主介面,這種方案實際上是比較好的,當然,前題是Splash介面不是為了投放廣告而延時,如果只是為了展示廣告,且時間明顯超過2秒,我認為體驗是非常差的。筆者主要從事系統應用開發,Splash介面的情況很少使用,就不討論這種方案,主要探討的是從點選圖示到出現主介面這個過程的優化。

這裡寫圖片描述
如上圖,黃色框中為應用的主Activity顯示到前臺經歷的生命週期回撥,具體的啟動情況將在之後的部落格中詳細分析Activity的啟動流程原始碼。那麼我們要做啟動速度的優化就很簡單了,只需要在這一系列的生命週期回撥中移除所有不必要的耗時程式碼即可。要做到這一點或許並沒有那麼容易,有時候生命週期中會做很多初始化以及業務邏輯的處理,如果你的專案中,在生命週期中做了太多初始化和業務邏輯的處理,那往往說明這個專案的架構設計有問題,是時候重構程式碼了。事實上,我理解的生命週期,應該就是一個主執行緒任務佇列,也就是說當我們需要更新一下介面元素的時候,我們就往任務佇列扔一個更新任務,包括介面的首次初始化也可以理解為一個更新任務。再通俗的講,生命週期回撥以及主執行緒中,與更新介面元素無關的程式碼都應該移除。在這裡應該儘可能的使用執行緒去做載入以及處理各種業務邏輯,包括資料的請求,當資料準備完畢之後再去給主執行緒訊息佇列扔一個更新UI的任務。這樣的設計不僅可以提升啟動速度,還能改善UI卡頓問題,從而徹底避免ANR的發生。

在前一篇部落格已提到了關於Activity的冷啟動測試命令,使用如下命令即可獲取一些啟動時間資料

adb shell am start -W -S <包名/完整類名>
  • ThisTime:通常和TotalTime時間相同
  • TotalTime:應用的啟動時間,包括建立程序+Application初始化+Activity初始化到介面顯示。
  • WaitTime:通常比TotalTime大,包括系統影響的耗時

筆者也接手過超爛的程式碼,是比較老的程式碼,基本上所有的操作全是在主執行緒中完成的,包括常說的資料庫操作、檔案讀寫,使用體驗可想而知。在這裡我建議將所有的與介面元素更新無關的程式碼儘可能全部放入執行緒中操作,斬斷所有的可能阻塞主執行緒的可能。為什麼提出這種建議呢?筆者在系統應用開發過程中,偶爾會收到某些上報的ANR錯誤報告,其中會碰到很多理論上不可能,但實際上確實會發生的ANR情況,比如開發中在方法名前濫用synchronized關鍵字,非同步執行緒中發生某種異常,導致主執行緒呼叫相應的synchronized方法時被阻塞,從而ANR,synchronized關鍵字應該被用來鎖定關鍵部分,且筆者非常不建議偷懶直接在方法上加synchronized,應當主動建立不同的鎖物件,正確合理使用synchronized關鍵字;還有的是呼叫通訊相關的遠端服務時,發生ANR,例如:TelephonyManager tm = (TelephonyManager) getContext().getSystemService(Context.TELEPHONY_SERVICE);類似這種獲取遠端服務的程式碼通常會在主執行緒中,實際上獲取遠端服務在絕大多數時候都是可靠的快速的,但是在某種情況下可能會阻塞,例如重啟手機後,立刻進入應用,這時系統底層的通訊服務可能並未準備好,呼叫上述程式碼就發生阻塞,最後導致報ANR掛掉。還包括其他的某些遠端服務,因此直接在主執行緒中呼叫,並不排除可能發生耗時或阻塞的情況。最簡單可靠的手段就是將一切具有發生耗時可能的程式碼移除到非同步執行緒,保證生命週期回撥以及主執行緒程式碼簡單單一且穩定。

最後提一點,儘可能避免Activity繼承過於複雜的父類,效能和功能本身就是一對矛盾,有時候只能採用折中的辦法調和矛盾。
某些框架使用起來確實非常簡潔方便,但是需要繼承一些BaseActivity,某些過於複雜的基類可能會導致Activity生命週期耗時。

2.渲染優化 
關於Android渲染的一些基本知識

Android系統每隔16ms發出VSYNC訊號,觸發對UI進行渲染,也就是說每隔16ms就重新繪製一次Activity,那意味著應用必須在16ms內完成螢幕重新整理的全部邏輯操作,這樣才能達到每秒60幀,每秒幀數的引數是由手機硬體所決定,現在大多數手機螢幕重新整理率是60赫茲(國際單位制中頻率的單位,表示每秒中的週期性變動重複次數的計量),也就是說我們有16ms(1000ms/60次=16.66ms)的時間去完成每幀的繪製邏輯操作,如果錯過了,比如說我們花費34ms才完成計算,那麼就會出現稱之為丟幀的情況。

最理想情況 
這裡寫圖片描述 
丟幀情況 

安卓系統嘗試在螢幕上繪製新的一幀,如果這一幀還沒準備好,畫面就不會重新整理。如果使用者盯著同一張圖看了32ms而不是16ms,使用者會很容易察覺出卡頓感,哪怕僅僅出現一次掉幀,使用者都會發現動畫不是很順暢,如果出現多次掉幀,使用者就會開始抱怨卡頓,如果此時使用者正在和系統進行互動操作,例如滑動列表或者輸入資料,那麼卡頓感就會更加明顯,現在對繪製每幀花費的時間有了清晰瞭解,接下來看看是什麼原因導致了卡頓,如何去解決些問題

Android把經過測量、佈局、繪製後的surface快取資料,通過SurfaceFlinger渲染到顯示螢幕上,通過Android的重新整理機制來重新整理資料。應用層負責繪製,系統層負責渲染,通過程序間通訊把應用層需要繪製的資料傳遞到系統層服務,系統層服務通過重新整理機制把資料更新到螢幕。在Android的每個View繪製中有三個核心步驟,即通過Measure和Layout來確定當前需要繪製的View所在的大小和位置,通過Draw方法繪製到surface。對於每一個ViewGroup繪製來說,系統會首先遍歷它的每一個子View,即深度優先原則。因此隨著佈局深度的加深,遍歷每一個子View的時間會呈現指數級上升。

從Android系統的顯示原理中可以看出,UI卡頓的原因有兩方面
  • 主執行緒忙碌。我們知道繪製工作是由主執行緒即UI執行緒來負責,主執行緒忙就會導致VSYNC訊號到來時還沒有準備好資料產生丟幀

  • 繪製任務過重,繪製一幀內容耗時過長

繪製耗時過長需從UI佈局和繪製上進行優化,主執行緒忙碌則需要避免任何阻礙主執行緒的操作。關於主執行緒的的耗時操作我們前面已經講了基本思路,儘可能將一切與UI更新無關的程式碼移到非同步執行緒。這裡稍微補充一個細節,在使用非同步機制時,很多人會使用簡單方便的AsyncTask機制,特別是AsyncTask.execute(Runnable)簡化的靜態方法使用。AsyncTask會給每一個程序開闢一個全域性唯一的執行緒池和訊息佇列,直接new物件使用和呼叫execute靜態方法都是序列的,也就是說它是排隊的一個一個執行。在某個地方執行極為耗時的任務時,訊息佇列就是阻塞的狀態,這時在另外一個地方啟動一個任務並不會立刻執行,而是會排隊,因此使用時需要小心,在專案裡到處使用可能造成不可預知的BUG,建議在執行比較重要的任務時使用並行方式啟動new AsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR),或者使用其他的非同步方案。

使用工具分析優化

啟用嚴格模式(Strict mode enabled)

該工具在上一篇部落格工具的使用中已經提過,在開發者選項中啟動該模式,該模式可以用來初步檢查主執行緒是否存在耗時問題,依照之前關於啟動速度優化的建議,嚴格模式下不應出現閃爍螢幕現象 
這裡寫圖片描述 

啟用GPU呈現模式分析(Profile GPU Rendering)

它是一個圖形監測工具,能實時反應當前繪製的耗時。在Android 4.1系統開始提供,同樣在開發者選項中開啟。 

其中橫軸表示時間,縱軸表示每一幀的耗時(單位為ms),隨著時間推移,從左到右的重新整理呈現,如果高於標準耗時,表示當前這一幀丟失 
柱狀圖主要由4種顏色組成:紅、黃、藍、紫,這些線對應每一幀在不同階段的實際耗時。

  • 藍色:測量繪製的時間,可以理解為執行每一個View的onDraw方法,建立或者更新每一個View的Display List物件。在藍色的線很高時,有可能是因為需要重新繪製,或者自定義檢視的onDraw函式處理事情太多。
  • 紅色:是Android進行2D渲染Display List的時間。當紅色的線非常高時,可能是由重新提交了檢視而導致
  • 橙色:表示處理時間。如果柱狀圖很高,就意味著GPU太繁忙
  • 紫色:表示將資源轉移到渲染執行緒的時間

任何時候超過綠橫線(警戒線,時長16ms),就有可能丟失一幀的內容,為保持UI流暢,應儘可能讓這些垂直的柱狀條保持在綠橫線之下。

除了在手機上直觀的分析,還可以使用命令匯出具體資料分析
adb shell dumpsys gfxinfo (包名) > gfx.txt
adb shell dumpsys gfxinfo (包名)framestats > gfx.txt  #新增framestats引數獲取更詳細資訊

TraceView

該工具可以分析應用具體每一個方法的執行時間。我們可以用它來分析出現卡頓時在方法的呼叫上有沒有很耗時的操作。總的來說,使用它能找到頻繁被呼叫的方法,也能找到執行非常耗時的方法,前者可能會造成CPU頻繁呼叫,手機發燙的問題,後者則可能造成卡頓問題。 

使用TraceVeiw分析問題之前需要得到一個.trace的檔案,我們可以使用如上圖方式抓取一個,在第二步開啟後操作應用,之後再次點選第二步中的按鈕停止抓取,這時會自動開啟剛剛抓取的.trace進行分析 

在時間面板中,X軸表示時間消耗,單位為毫秒(ms),Y軸表示各個執行緒,每個執行緒中的不同方法使用了不同的顏色來表示,顏色佔用面積越寬,表示該方法佔用CPU時間越長

時間面板是可以放大檢視的 

當需要檢視該方法詳細資訊時,可雙擊該立柱,分析面板就會自動跳轉到該方法

接下來看一下分析面板

解釋
Name 所有的呼叫項
InclCpu Time CPU執行該方法該方法及其子方法所花費的時間
InclCpu Time % CPU執行該方法該方法及其子方法所花費佔CPU總執行時間的百分比
ExclCpu Time CPU執行該方法所花費的時間
ExclCpu Time % CPU執行該方法所花費的時間佔CPU總時間的百分比
Incl Real Time 該方法及其子方法執行所花費的實際時間,從執行該方法到結束一共花了多少時間
Incl Real Time % 上述時間佔總的執行時間的百分比
Excl Real Time 該方法自身的實際允許時間
Excl Real Time % 上述時間佔總的允許時間的百分比
Calls+Recur/Total 該方法呼叫次數加遞迴次數
Cpu Time/Call CPU執行時間和呼叫次數的百分比,代表該函式消耗CPU的平均時間
Real Time/Call 實際時間於呼叫次數的百分比,該表該函式平均執行時間

在分析時,可以主要關注Calls+Recur Calls/Total和Cpu Time/Call這兩個值,即呼叫次數多和耗時久的方法,優化這些方法的邏輯和呼叫次數,減少耗時

Hierarchy Viewer

仍在使用eclipse的人建議淘汰掉,在Android Stuidio中開啟Android Device Monitor 

該工具主要用來檢視層級和耗時,通過樹狀圖可以很清晰的分析佈局的元素

除錯GPU過度繪製(Show GPU Overdraw)

過度繪製指螢幕上的某個畫素在同一幀的時間內被繪製了多次。在多層重疊的UI結構中,如果不可見的UI也在做繪製的操作,就會導致某些畫素區域被繪製了多次,從而浪費多餘的CPU以及GPU資源。該功能也在開發者選項中開啟 
 
可以看到,在該項中,微信優化還行,我們看到如下圖示 
這裡寫圖片描述 
如圖,藍色、4種顏色代表不同程度的Overdraw情況。對於過度繪製優化的目標,就是儘量減少紅色,更多呈現藍色區域

  • 無色:沒有過度繪製
  • 藍色:多繪製了1次。藍色是可以接受的
  • 綠色:多繪製了2次。
  • 淡紅:多繪製了3次。該區域不超過螢幕的1/4是可接受的
  • 深紅:多繪製了4次或者更多。嚴重影響效能,需要優化,避免深紅色區域

在做過度繪製分析時,還可以使用前面提到的Hierarchy Viewer工具,詳細檢查每個View的背景使用情況

過度繪製的主要優化辦法就是去除View中不需要的背景,舉個簡單例子,假如佈局中的父控制元件是灰色背景,子控制元件也設定了一個灰色背景,則子控制元件的背景是不必要的,會造成過度繪製,應當去除,同時在設計介面時,也可以考慮儘可能共用背景色。除了我們自己佈局中設定的背景,在Activity中往往會被設定一個預設的背景,這個背景由DecorView持有,通常我們自己的應用都會設定一個背景,而這個預設背景就是多此一舉,從而造成了過度繪製,應當手動去除

在Activity的onCreate方法中呼叫

getWindow().setBackgroundDrawable(null);

關於佈局優化的建議

佈局優化最主要考慮的目標是減少佈局的層級,當無法減少時,則可以考慮延遲載入。
  • 使用Merge標籤 
    Merge標籤可以有效優化某些情況下的多餘層級。在自定義View中,如果父元素是FrameLayout或者LinearLayout,則子元素中可以使用;在Activity整體佈局中,根元素需要是FrameLayout時,推薦使用。如上圖中,系統給Activity預設的content也是FrameLayout,當我們自己應用的根佈局也是FrameLayout時,就應該使用Merge標籤,那麼就會將其中的子元素新增到Merge標籤的父佈局中。

  • 合理的使用RelativeLayout和LinearLayout 
    實際開發中並不能說這兩種佈局誰的效能更好,要在特定條件下做最合適的選擇。總的來說,RelativeLayout在測量時會進行多次測量才能確定子View大小,因此RelativeLayout不應巢狀使用,否則將會明顯增加耗時,而LinearLayout在使用權重屬性時,也會發生兩次測量,增加耗時。根據這些特點,使用RelativeLayout時應避免巢狀,其本意也是為了讓佈局更扁平,而使用LinearLayout時,儘量避免使用權重以及巢狀太深。

  • 祭出終極大招,使用ConstraintLayout佈局 
    ConstraintLayout是一個Support庫,因此使用時需要新增依賴,該佈局可以用來徹底解決層級巢狀過深的問題,視覺化使用也極其簡單,建議可以首先學習郭大神的部落格,想要更深入瞭解,推薦另一篇部落格

  • 使用ViewStub標籤延遲載入 
    只要不是介面上最首要的元素,實際上都可以延遲載入。我們可以為ViewStub標籤指定一個佈局,當整體佈局載入時,只會對ViewStub初始化,只有當ViewStub被設定為可見時或是呼叫了ViewStub.inflate()時,ViewStub所代表的佈局才會被載入並例項化,而ViewStub的佈局屬性都會傳給它指向的佈局,這樣就達到了將一些非首要的元素延遲載入到記憶體的目的,實際上進入應用主介面,很多其他的二級介面使用者很可能並不會開啟使用,對於這種非首要的介面元素,並不需要在一起動的時候去載入。

  • 其他的一些優化 
    避免使用wrap_content寫法,當有明確的數值時,不要偷懶使用wrap_content,避免不必要的測量耗時,養成高效能編碼意識和習慣。 
    還可以使用include標籤複用佈局。

處理記憶體洩露

  • 記憶體洩露

    指已經不需要再使用的記憶體物件,但垃圾回收時不能及時將它們回收,仍然保留在記憶體中,佔用一定的空間

  • 記憶體回收

    垃圾回收器在GC時會將處於不可達階段的物件進行記憶體回收。不可達階段則是指該物件不再被任何強引用持有

  • 為什麼要處理記憶體洩露 
    1.減少UI卡頓。 
    發生記憶體洩露時,系統在堆上為應用分配的記憶體空間可能會不斷變小,從而導致頻繁觸發GC,只要GC消耗的時間超過了16ms的閾值,就會有丟幀的情況出現,導致卡頓。這是因為Dalvik虛擬機器在GC時,會暫停應用的執行緒,這在ART虛擬機器上有所改進,ART虛擬使用併發的GC,但仍然存在掛起線的操作。參見ART執行時垃圾收集(GC)過程分析 
    2.減少OOM的發生,確保APP穩定 
    當請求的記憶體超出系統為應用分配的堆記憶體空間時,發生記憶體溢位,導致應用Crash

在分析記憶體洩露問題時,其實有諸多工具可用,但是分析的過程仍然十分麻煩,因此我個人認為,在處理記憶體洩露時,應當首先從程式碼入手,排除所有常見的記憶體洩露情景,在主觀上對記憶體洩露做出一個推斷,然後再使用工具去驗證分析,從而解決問題。讓我們先看結論

  • 常見記憶體洩露情景的總結

1.資源物件未關閉 
主要指Cursor、File檔案等,它們往往都用了一些緩衝,不使用時應及時關閉,以便快取資料能夠及時回收,對於這種情況,可以多留意log資訊,往往會在log資訊中報警告

2.註冊了物件,在不用時需要登出 
Android中典型的有廣播、Provider觀察器等,其次是帶有add字首的註冊監聽方法,如addOnWindowFocusChangeListener等,這些大多都是按照發布訂閱模式(觀察者模式)設計的,內部維護一個註冊物件集合,會一直持有該物件引用,導致無法垃圾回收

3.非靜態內部類的濫用(特別是匿名內部類) 
根據Java的設計,非靜態內部類會隱式持有外部類的引用,如果非靜態內部類建立了一個靜態例項或者做了耗時處理,會一直持有外部類,導致無法被垃圾回收

public class MyActivity extends Activity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        int a;

        //1、建立非靜態內部類的靜態例項,將始終持有對MyActivity的強引用,導致MyActivity結束時無法被回收
        static Inner in = new Inner();

        //2、匿名內部類
        new Thread(){
            @Override
            public void run() {
                a = 1;
                Thread.sleep(5000);//執行緒釋放前,導致MyActivity結束時一直不能被回收
            }
        }; 
    }

    class Inner {
        //非靜態內部類
    }
}

4.AlertDialog記憶體洩露 
Android 5.0 以下版本中,在AlertDialog中儲存了Activity的引用,則Activity退出前應當dismiss 掉對話方塊,最好在Activity失去焦點的時候關閉它

5.靜態變數的濫用 
靜態變數的生命週期可以等同於整個程序,如果在靜態變數中儲存了一些資料,在程序退出前,這些資料是無法被回收的。比較常見的一種情況是單例的使用沒有處理好,比如在單例中儲存了非全域性的Context引用,導致被引用的物件無法被回收

6.Handler阻塞導致物件無法及時回收 
通常會在主執行緒建立Handler用於訊息通訊,當Handler被阻塞時,發出的Message就會一直儲存在MessageQueue中,導致Handler無法被及時回收,從而導致Activity或Service不能及時回收,,因此在元件退出時應當清空任務佇列

7.使用MVP架構模式不當。將UI上的邏輯介面進行抽象化封裝,使得Presenter一直持有UI的例項,導致UI元件例項不能被垃圾回收,因此良好的MVP設計,應當在元件退出時解綁,並使用弱引用儲存對元件的引用。

使用工具分析記憶體洩露

  • Memory Monitors (Android Studio中的工具)

  • Heap Viewer

  • Allocation Tracker

  • MAT (Memory Analyzer Tool,Eclipse中的外掛,可下載獨立版)

  • LeakCanary

來看一種比較隱蔽但開發中比較常用的情景

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d("com.study","MainActivity onCreate ...");
        mHandler.postDelayed(new Runnable() {
            @Override public void run() {
                //do something
            }
        }, Integer.MAX_VALUE);
    }

    Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };
}

以上程式碼模擬了一種情形,即建立一個匿名內部類Runnable並放到Handler任務佇列中,假設Runnable需要執行一個耗時的操作,那麼它將阻塞當前的任務佇列,我們這裡使用postDelayed來模擬耗時,使任務一直存放在佇列而不被消費掉,這樣就會使得Runnable的外部類一直被持有而無法被回收,正如我們前面說的,匿名內部類會隱式的持有外部類的引用,那麼Activity就會記憶體洩露。

Memory Monitors

Memory Monitors是Android Studio中的工具,首先使用該工具來看一下如何分析記憶體洩露,並驗證我們上面的結論是否成立。

開啟Android Studio執行上面的示例程式碼,並看到Memory Monitors介面 

如上圖,該工具可以生動的展現應用的記憶體使用情況,特別是記憶體抖動,首先我們點選小貨車按鈕手動GC幾次,使記憶體趨於平緩穩定,然後看到黃色框中的內容,表示當前應用佔用的堆記憶體大小,這個時候點選GC,佔用記憶體大小不會有太大幅度變化了 
接下來我們我們旋轉模擬器的螢幕,我們知道當配置發生改變時,Activity會重新建立,這裡切換系統語言也可以達到相同效果,為了方便操作,直接旋轉螢幕,可以看到日誌中會不斷有Log.d("com.study","MainActivity onCreate ...")相關輸出,說明不斷有新的Activity被建立,檢視Monitors中的情況 

我們再次不斷的手動GC,發現佔用的記憶體仍然沒有大幅減少,開始時的資料是2.98M,現在始終停留在4.25M,可以看到我們只是旋轉了螢幕,但是記憶體開銷明顯增長了,且GC無法回收,Memory Monitors很直觀的就反映了記憶體洩露情況。

  • 檢視記憶體快照 
    這裡寫圖片描述 
    點選小貨車後面的按鈕dump記憶體快照,之後看到如下介面 

    從這個記憶體快照中可以很清晰的看出來,MainActivity被它的匿名內部類引用,而它的匿名內部類又被Message引用,而Message則被MessageQueue引用
解釋
Total Count 記憶體中該類的物件個數
Heap Count 堆記憶體中該類的物件個數
Sizeof 物理大小
Shallow size 該物件本身佔有記憶體大小
Retained Size 釋放該物件後,節省的記憶體大小

物件例項區

名稱 意義
depth 深度
Shallow Size 物件本身記憶體大小
Dominating Size 管轄的記憶體大小
  • 檢視總體記憶體使用情況分析記憶體洩露 
    除了dump記憶體快照分析,還有一種更為簡單有效的辦法初步分析是否存在記憶體洩露,其操作也在Memory Monitors中 
    首先操作應用旋轉螢幕,然後手動GC,最後按返回鍵退出應用,當我們退出應用時,理論上Activity應該是結束掉了,並且會被回收,那麼我們點選如下按鈕,檢視記憶體的使用情況 
    這裡寫圖片描述 
    點選之後,會生成一個文字檔案,我們檢視該檔案 
    這裡寫圖片描述
    看到物件一欄,在記憶體中仍然存在45個View,5個Activity,基本上能清晰的反映記憶體洩露的情況,只是該操作有較大限制,基本上只能用來檢查View和Activity的記憶體洩露情況。

Heap Viewer

Heap Viewer是SDK自帶的工具,主要用於實時檢視App分配的記憶體大小和空閒記憶體大小,功能跟Memory Monitors是一致的,但是沒有Memory Monitors直觀,在Eclipse中比較常用,在Android Studio中也可以使用,但不是很推薦,因為會發生adb埠搶佔問題,經常導致AS的工具掉線後無法恢復,無法除錯無法使用LogCat等,最後可能要重啟模擬器,如果是真機有時候還要重啟電腦,否則無法識別adb埠,這是AS無法與SDK中的工具相容,很是蛋疼。不過如果你使用Android Studio作為主要IDE,完全沒有使用Heap Viewer的理由,它唯一的一點優勢是可以直觀顯示堆記憶體中儲存的資料的具體型別。

簡單說一下使用,選擇Tools->Android->Android Device Monitor 

看到右側的中間部分,顯示出了記憶體中資料的具體型別

Type 解釋
free 空閒的物件
data object 資料物件,類型別物件,最主要的觀察物件
class object 類型別的引用物件
1-byte array(byte[],boolean[]) 一個位元組的陣列物件
2-byte array(short[],char[]) 兩個位元組的陣列物件
4-byte array(long[],double[]) 4個位元組的陣列物件
non-Java object 非Java物件
解釋
Count 數量
Total Size 佔用的總記憶體大小
Smallest 佔用記憶體最小的物件的大小
Largest 佔用記憶體最大的物件的大小
Median 拍在中間的物件佔用的記憶體大小
Average 平均值

Allocation Tracker

記憶體分配跟蹤器,該工具也是SDK中帶有的,它可以跟蹤記錄應用程式的記憶體分配,並列出了它們的呼叫堆疊,可以檢視所有物件記憶體分配的週期。它主要用於分析較短一段時間內的記憶體使用情況,在使用Allocation Tracker前,應當先用Memory Monitor或者Heap Viewer找到記憶體異常情況,然後使用Allocation Tracker分析具體的使用情況。

該工具可以在Android Device Monitor中開啟,也可以直接在Android Studio中使用,建議直接使用Android Studio內部的Allocation Tracker,因為更清晰更方便

看到下圖,Allocation Tracker和Memory Monitors是在一起的 
這裡寫圖片描述 
步驟: 
1.單擊上圖追蹤按鈕 
2.在疑似記憶體洩漏的地方反覆操作應用復現 
3.再次點選追蹤按鈕結束跟蹤,隨後自動生成一個alloc結尾的檔案

自動進入如下介面 

上圖中的count列表示分配次數,Total Size表示總共分配的大小,上圖可以看到我們的MainActivity分配了13次

MAT

它是一個快速、功能豐富的Java Heap分析工具,通過分析Java程序的記憶體快照HPROF檔案,從眾多的物件中分析,快速計算出在記憶體中物件的佔用大小,檢視哪些物件不能被垃圾收集器回收,並可以通過檢視直觀地檢視可能造成這種結果的物件

我們選擇之前在Memory Monitors處dump的記憶體快照檔案 

開啟我們下載的獨立版MAT工具,選擇File,並open file我們之前匯出的記憶體快照檔案 

  • OverView檢視 
    是一個總體概覽,顯示總體的記憶體消耗情況。Biggest Objects by Retained Size 
    則會列舉出Retained Size值最大的幾個值,你可以將滑鼠放到餅圖中的扇葉上,可以在右側看出詳細資訊

  • Histogram檢視 
    點選工具欄的快捷按鈕或OverView檢視下方的Actions區域開啟。列出記憶體中的所有例項型別物件、物件的個數以及大小,並支援正則表示式查詢 
    這裡寫圖片描述

  • Dominator Tree檢視 
    列出最大的物件及其依賴存活的Object。分析流程和Histogram類似,但Dominator Tree能更方便地看出引用關係。這個檢視主要就是用來發現大記憶體物件的,這些物件都可以展開檢視更詳細的資訊,可以看到該物件內部引用的物件 
    這裡寫圖片描述

  • Leak Suspects 
    自動分析洩漏的原因,實際上並不是很準確,該工具只是列出了懷疑的記憶體洩漏點,以及洩漏的記憶體大小,在後面有問題列表和所有物件,單擊對應的檢視詳情。該資訊可作為一種參考

接下來看一下列資訊

列名 解釋
Objects 例項物件個數
Shallow Heap 物件自身佔用的記憶體大小,不包括它引用的物件
Retained Heap 當前物件大小與當前物件可直接或間接引用到的物件的大小總和

大致瞭解了一些MAT工具,接下來就要使用工具定位到具體記憶體洩露的地方,看到Histogram檢視 
這裡寫圖片描述
我們選中MainActivity其中的一個匿名內部類,這裡要注意,選擇incoming references檢視被引用情況 
這裡寫圖片描述
如圖,我們再次右鍵選擇Merge Shortest Paths to GC Root->exclude all phantom/weak/soft etc refereneces,排除一些軟引用、弱引用等干擾 
這裡寫圖片描述

最後我們可以很精準的檢視到具體的引用情況,這裡是被Message的佇列引用,完全符合我們之前的推斷。

關於MAT工具,最後補充說明一點,我們除了可以分析一份記憶體快照,還可以將未發生記憶體洩露前的快照和發生記憶體洩露之後的快照進行對比分析

對比步驟也很簡單,使用MAT將兩份記憶體快照開啟,然後按如下操作 
這裡寫圖片描述 
分別將兩份快照都新增到對比中,然後點選感嘆號按鈕就可以開始對比了 
這裡寫圖片描述

LeakCanary

LeakCanary是一個檢測記憶體洩漏的開源類庫,使用可以說是最簡單的了,可以直接點選進入GitHub檢視

  • 新增依賴
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.4'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
  • 初始化 
    重寫application
public class ExampleApplication extends Application {
    @Override public void onCreate() {
        super.onCreate();
        if (LeakCanary.isInAnalyzerProcess(this)) {
            // This process is dedicated to LeakCanary for heap analysis.
            // You should not init your app in this process.
            return;
        }
        LeakCanary.install(this);
        // Normal app init code...
    }
}

使用自定義的application

<application
        android:name=".ExampleApplication"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
  • 部署應用 
    在真機或模擬器部署應用之後,會自動安裝一個檢測的apk 
    這裡寫圖片描述 
    操作本應用,產生記憶體洩露後,等待通知欄的通知 
    這裡寫圖片描述 
    點選進入檢視詳細報告 
    這裡寫圖片描述

應用的穩定性優化

應用的穩定性實際上比前面的效能體驗方面的優化更加重要,可以想象,一個經常發生Crash或者ANR的應用,是否還有使用者會願意使用?

使用靜態程式碼檢查工具

使用靜態程式碼檢查工具的好處是,將潛在的隱患暴露在編譯期間,從而提升程式碼的質量,儘可能的排除不穩定的因素。

Android Lint

Android Lint是SDK中提供的比較常用的檢測工具,但是很多時候只是將它作為對xml資源的一個檢測工具簡單使用,實際上Android Lint是功能非常強大的工具,靈活熟練的掌握它,能明顯的提高工程中的程式碼質量

在AndroidStudio中如下使用 
這裡寫圖片描述 
之後可以選擇整個工程或者某個模組,完成之後可以看到分析結果 

如圖,記憶體洩露也檢測出來了

在使用Lint工具時,如果全部項都檢查,可能會花費較多時間,這裡也可以指定關注的內容進行檢查

這裡寫圖片描述 
在搜尋框輸入檢查項 
這裡寫圖片描述

在IDE裡面的使用比較簡單,下面著重說明一下命令列如何使用。為什麼要強調在命令列使用Lint呢?在大型的專案中,為了確保app的穩定,在Android Jenkins自動編譯打包伺服器上,可以對待編譯打包的工程進行Lint檢測,對於不符合Lint檢測報告的任務,不允許其打包釋出。在多人團體開發中,每個人通常只負責某個部分,為確保APP的穩定性避免人為因素造成的疏漏,這是非常有必要的措施。

Lint命令列的使用 
這裡寫圖片描述 
Lint的檢查結果分為6大類 
1·Correctness (正確性) 
2·Security (安全性) 
3·Performance (效能) 
4·Usability (可用性) 
5·Accessibility (可達性) 
6·I18n (國際化)

問題的嚴重級別(severity)從高到低依次是: 
1·Fatal 
2·Error 
3·Warning 
4·Information 
5·Ignore

在Android Gradle工程的根目錄下通過命令執行Lint檢測 
Windows 上:

gradlew lint

Linux 或 Mac 上:

./gradlew lint

通過以上命令啟動檢測使用的是預設的配置項,我們可以手動編寫配置檔案,實際上配置檔案是一個XML檔案,下面是一個官方示例

<?xml version="1.0" encoding="UTF-8"?>
<lint>
    <!-- Disable the given check in this project -->
    <issue id="IconMissingDensityFolder" severity="ignore" />

    <!-- Ignore the ObsoleteLayoutParam issue in the specified files -->
    <issue id="ObsoleteLayoutParam">
        <ignore path="res/layout/activation.xml" />
        <ignore path="res/layout-xlarge/activation.xml" />
    </issue>

    <!-- Ignore the UselessLeaf issue in the specified file -->
    <issue id="UselessLeaf">
        <ignore path="res/layout/main.xml" />
    </issue>

    <!-- Change the severity of hardcoded strings to "error" -->
    <issue id="HardcodedText" severity="error" />
</lint>

接下來在gradle指令碼中指定我們編寫的配置檔案

android {
  ...
  lintOptions {
    // 重置 lint 配置
    lintConfig file("my-lint.xml")

    // 指定生成報告的路徑,它是可選的(預設為構建目錄下的 lint-results.html )
    htmlOutput file("lint-report.html")
  }
}

再次通過命令執行,則會使用我們制定的配置項進行檢查,下面列出gradle中lintOptions的所有可用項

lintOptions {
        // 設定為 true時lint將不報告分析的進度
        quiet true
        // 如果為 true,則當lint發現錯誤時停止 gradle構建
        abortOnError false
        // 如果為 true,則只報告錯誤
        ignoreWarnings true
        // 如果為 true,則當有錯誤時會顯示檔案的全路徑或絕對路徑 (預設情況下為true)
        //absolutePaths true
        // 如果為 true,則檢查所有的問題,包括預設不檢查問題
        checkAllWarnings true
        // 如果為 true,則將所有警告視為錯誤
        warningsAsErrors true
        // 不檢查給定的問題id
        disable 'TypographyFractions','TypographyQuotes'
        // 檢查給定的問題 id
        enable 'RtlHardcoded','RtlCompat', 'RtlEnabled'
        // * 僅 * 檢查給定的問題 id
        check 'NewApi', 'InlinedApi'
        // 如果為true,則在錯誤報告的輸出中不包括原始碼行
        noLines true
        // 如果為 true,則對一個錯誤的問題顯示它所在的所有地方,而不會截短列表,等等。
        showAll true
        // 重置 lint 配置(使用預設的嚴重性等設定)。
        lintConfig file("default-lint.xml")
        // 如果為 true,生成一個問題的純文字報告(預設為false)
        textReport true
        // 配置寫入輸出結果的位置;它可以是一個檔案或 “stdout”(標準輸出)
        textOutput 'stdout'
        // 如果為真,會生成一個XML報告,以給Jenkins之類的使用
        xmlReport false
        // 用於寫入報告的檔案(如果不指定,預設為lint-results.xml)
        xmlOutput file("lint-report.xml")
        // 如果為真,會生成一個HTML報告(包括問題的解釋,存在此問題的原始碼,等等)
        htmlReport true
        // 寫入報告的路徑,它是可選的(預設為構建目錄下的 lint-results.html )
        htmlOutput file("lint-report.html")

       // 設定為 true, 將使所有release 構建都以issus的嚴重性級別為fatal(severity=false)的設定來執行lint
       // 並且,如果發現了致命(fatal)的問題,將會中止構建(由上面提到的 abortOnError 控制)
        checkReleaseBuilds true
        // 設定給定問題的嚴重級別(severity)為fatal (這意味著他們將會
        // 在release構建的期間檢查 (即使 lint 要檢查的問題沒有包含在程式碼中)
        fatal 'NewApi', 'InlineApi'
        // 設定給定問題的嚴重級別為error
        error 'Wakelock', 'TextViewEdits'
        // 設定給定問題的嚴重級別為warning
        warning 'ResourceAsColor'
        // 設定給定問題的嚴重級別(severity)為ignore (和不檢查這個問題一樣)
        ignore 'TypographyQuotes'
    }

獲取檢查列表

上面的配置檔案中,只是使用了很少的檢查項,實際上可配置的檢查項非常多,我們可以通過命令列獲取所有的檢查項

首先將Lint命令配置到全域性的環境變數中,也可以直接進入sdk根目錄下的/tools/bin下面執行如下命令

lint --show   #可獲得詳細列表

lint --list   #僅可獲得Issue的id和summary的簡表

通過學習使用Lint命令,我們可以編寫shell指令碼來自動化定製化的實現Lint檢查,並將報告生成到指定的位置便於觀看,更多關於Lint工具的使用方式,請進入Lint官方的中文文件學習

FindBugs

FindBugs也是本人使用過的一個Java程式碼靜態分析工具,還不錯,推薦使用。在AndroidStudio中的安裝與使用也非常簡單 
這裡寫圖片描述
如圖,進入外掛商店搜尋並安裝,重啟後使用 
這裡寫圖片描述
成功安裝後就會出現如上的選單,檢測分析整個工程會非常耗時,可以按需使用