Android 性能優化(五)Crash治理之OOM,內存泄漏檢測工具

系列推薦:

Android性能優化(一)閃退治理、卡頓優化、耗電優化、APK瘦身

Android 性能優化(二)Handler運行機制原理,源碼分析  

Android 性能優化(三)認識錯誤Error和異常Exception及棧軌跡StackTrace

Android 性能優化(四)Crash治理之路,UncaughtException

前言

性能優化第一篇中的Crash治理說過:“出現Crash應用閃退和崩潰一般有三個原因:ANR(程序無響應)、Exception(異常)、LMK(低內存殺死機制)。”

上一篇Exception異常的捕捉和處理方式主要講解如何打造一款不會Crash的APP,已經講完了,這一篇該說一說程序無法攔截處理的“LMK”問題了。其實它屬於虛擬機拋出的error,這個在系列第三篇講過,error是程序無法進行攔截和處理的。因此,唯一的解決的辦法就只能是防範。

那麼該如何防範?出問題瞭如何檢測?如何解決?這篇文將會帶大家對此方向,有一個恍然開朗的認識。

一、LMK 和 OOM

KMK,(Low Memory Killer )低內存殺死機制。由於Android應用的沙箱機制,每個應用程序都運行在一個獨立的進程中,各自擁有獨立的Dalvik虛擬機實例,系統默認分配給虛擬機的內存是有限度的,當系統內存太低依然會觸發LMK機制,即出現閃退、崩潰現象。

不同廠商不同,如:華爲mate7,192m ;小米4,128m ;紅米,128m 。而在,Android4.0以後,可以通過在application節點中設置屬性android:largeHeap=”true”來設置最大可分配多少內存空間就可以突破一定限制。

 

OOM(OutOfMemoryError)內存溢出錯誤,在常見的Crash疑難排行榜上,OOM絕對可以名列前茅並且經久不衰。因爲它發生時的Crash堆棧信息往往不是導致問題的根本原因,而只是壓死駱駝的最後一根稻草。導致OOM的兩個主要原因:

  1、內存泄漏,大量無用對象未及時回收,導致後續申請內存失敗。

  2、BitMap大對象,幾個大圖同時加載很容易觸發OOM。

爲了更好探究如何避免內存緊張出現Crash的情況,我們就先研究一些內存的回收機制。

 

二、JAVA GC機制

垃圾回收器—GC(Garbage Collection),它與“java面向編程”一樣是Java語言的特性之一。

GC 主要是處理 Java堆(Heap) ,也就是處理在Java虛擬機用於存放對象實例的內存區域。JVM能夠完成內存分配和內存回收,雖然降低了開發難度,避免了像C/C++直接操作內存的危險。但也正因爲太過於依賴JVM去完成內存管理,導致很多Java開發者不再關心內存分配,導致很多程序低效、耗內存問題。

因此開發者需要主動了解GC機制,充分利用有限的內存,才能寫出更高效的程序。

那麼,GC怎麼回收的?

Java垃圾回收機制的最基本的做法就是分代回收。內存中的區域被劃分成不同的世代,對象根據其存活的時間被保存在對應世代的區域中。一般的實現是劃分成三個年代:年輕、年老、永久。內存的分配是發生在年輕世代中的,當一個對象存活的時間夠久的時候,它就會被複制到老年代中。

對於不同世代可以使用不同的垃圾回收算法。進行世代劃分的出發點是對應用中對象存活時間進行研究之後得出的統計規律。一般來說,一個應用中的大部分對象的存活時間都很短。比如局部變量的存活時間就只在方法的執行過程中。因爲年輕世代的對象很快會進入不可達狀態,因此要求回收頻率高且回收速度快,基於這一點,對於年輕世代的垃圾回收算法就可以很有針對性。如下:

年輕代“複製式”回收算法:劃分兩個區域,分別是Eden 區和 Survive 區。大多數對象先分配到Eden區,內存大的對象會直接被分配到老年代中;Survive 區又分Form、To兩個小區,一個用來保存對象,另一個是空。每次進行年輕代垃圾回收的時候,就把E大區和From小區中的可達對象都複製到To區域中,一些生存時間長的就直接複製到了老年代。最後,清理回收E大區和From小區的內存空間,原來的To空間變爲From空間,原來的From空間變爲To空間。

有沒有看了上面一大堆文字後,兩眼冒金星的感覺?!哎哎哎!~  其實很簡單,大致就是說對象被分配到了堆中,堆中又分了一些小的內存區域,會根據對象自身的存活時長進行分配。

不可達狀態,什麼意思?

  • 可達狀態:在一個對象創建後,有一個以上的引用變量引用它,那它就處於可達狀態。
  • 可恢復狀態:對象不再有任何的引用變量引用它,它將先進入可恢復狀態,系統會調用finalize()方法進行資源整理,發現有一個以上引用變量引用該對象,則這個對象又再次變爲可達狀態,否則會變成不可達狀態。
  • 不可達狀態:當對象的所有引用都被切斷,且系統調用 finalize() 方法進行資源整理後該對象依舊沒變爲可達狀態,則這個對象將永久性失去引用並且變成不可達狀態,系統纔會真正的去回收該對象所佔用的資源。

當某些對象是可達狀態時,但程序以後不會再使用了,它們仍然佔用內存不會被GC所回收,就造成了內存泄漏。一般是由錯誤的程序代碼邏輯引起的。在Android平臺上,最常見也是最嚴重的內存泄漏就是Activity對象泄漏。Activity承載了App的整個界面功能,Activity的泄漏同時也意味着它持有的大量資源對象都無法被回收,極其容易造成OOM。

 

三、防止內存泄漏

 

1. 長生命週期對象持有 Activity

這基本是最常見的內存泄漏了,比如

  • 內部類形式使用 Handler 同時發送延時消息,或者在 Handler 裏面執行耗時任務,在任務還沒完成的時候 Activity 需要銷燬。這時候由於 Handler 持有 Activity 的強引用導致 Activity 無法被回收。
  • 同理內部類形式的使用 AsyncTask 執行耗時任務也會導致內存泄漏的發生。
  • 單例作爲最長生命週期的對象,自然不應該持有 Activity 從而導致內存泄漏發生;

針對上面這種情況,基本不必多說了,不要使用內部類或者匿名內部類做這樣的處理就好了,實際上 IDE 也會彈出警告,我想大家應該還是都知道採用靜態內部類或者在銷燬頁面的時候使用相關方法移除處理的。實際上,使用 Kotlin 或者 Java 8 的 Lambda 表達式同樣不會導致內存泄漏的發生,這是因爲實際上它也是使用的靜態內部類,沒有持有外部引用。

Activity 中匿名使用 Handler 實際上會導致 Handler 內部類持有外部類的引用,而 SendMessage() 的時候 Message 會持有 HandlerenqueueMessage 機制又會導致 MeassageQueue 持有 Message。所以當發送的是延遲消息那麼 Message 並不會立即的遍歷出來處理而是阻塞到對應的 Message 觸發時間以後再處理。那麼阻塞的這段時間中頁面銷燬一定會造成內存泄漏。

2. 各種註冊操作沒有對應的反註冊

這一點基本不必多說,相信大家剛剛開始學習廣播和 Service 的時候一定對此有所接觸,然後就是比如我們常用的第三方框架 EventBus 也是一樣的。平時使用的時候注意在對應的生命週期方法中進行反註冊。

3. Bitmap大對象 使用完沒有注意 recycle()

Bitmap 作爲大對象,在使用完畢一定要注意調用 recycle()方法 進行回收。TypedArray 、Cursor、各種流同理,一定要在最後調用自己的回收關閉方法處理。

隨着手機屏幕尺寸越來越大,屏幕分辨率也越來越高,1080p和更高的2k屏已經佔了大半份額,爲了達到更好的視覺效果,我們往往需要使用大量高清圖片,同時也爲OOM埋下了禍根。對於圖片內存優化,我們有幾個常用的思路:

  儘量使用成熟的圖片庫,比如Glide,圖片庫會提供很多通用方面的保障,減少不必要的人爲失誤。根據實際需要,也就是View尺寸來加載圖片,可以在分辨率較低的機型上儘可能少地佔用內存。

4. WebView 使用不當

WebView 是非常常用的控件,但稍有不注意也會導致內存泄漏。內存泄漏的場景: 很多人使用 Webview 都喜歡採用佈局引用方式, 這其實也是作爲內存泄漏的一個隱患。當 Activity 被關閉時,Webview 不會被 GC 馬上回收,而是提交給事務,進行隊列處理,這樣就造成了內存泄漏, 導致 Webview 無法及時回收。

目前所知的比較安全的方案是:

  • 在佈局中動態添加 WebView。
  • 採用下面的方法。
override fun onDestroy() {
    webView?.apply {
        val parent = parent
        if (parent is ViewGroup) {
            parent.removeView(this)
        }
        stopLoading()
        // 退出時調用此方法,移除綁定的服務,否則某些特定系統會報錯
        settings.javaScriptEnabled = false
        clearHistory()
        removeAllViews()
        destroy()
    }
}

5. 循環引用

循環引用導致內存泄漏比較少見,正常來講不會有人寫出 A 持有 B,B 持有 C,C 又持有A 這樣的代碼,不過總還是需要注意。

總的來說,內存泄漏很常見,但分析App內存的詳細情況是解決問題的第一步,我們需要對App運行時到底佔用了多少內存、哪些類型的對象有多少個有大致瞭解,並根據實際情況做出預測,這樣才能在分析時做到有的放矢。Android Studio也提供了非常好用的Memory Profiler,堆轉儲和分配跟蹤器功能可以幫我們迅速定位問題。

四、Memory Monitor 工具

Android Studio自帶的一個內存監視工具,它可以很好地幫助我們進行內存實時分析。通過點擊Android Studio右下角的Memory Monitor標籤,打開工具可以看見較淺藍色代表free的內存,而深色的部分代表使用的內存從內存變換的走勢圖變換,可以判斷關於內存的使用狀態,例如當內存持續增高時,可能發生內存泄漏;當內存突然減少時,可能發生GC等,如下圖所示。

五、Memory Analyzer 工具:

    MAT(Memory Analyzer Tool) 是一個快速,功能豐富的 Java Heap 分析工具,通過分析 Java 進程的內存快照 HPROF 分析,從衆多的對象中分析,快速計算出在內存中對象佔用的大小,查看哪些對象不能被垃圾收集器回收,並可以通過視圖直觀地查看可能造成這種結果的對象。

    檢測步驟如下:案例參考

(a)屏幕多次翻轉,出現內存持續增高時。點擊 Dump java Heap就會生成運行內存快照hprof文件。

(b)然後將APP完全退出,重新啓動,打開Android Monitor 再次點擊Dump java Heap 生成一份還沒操作(旋轉屏幕)前的內存快照hprof文件。現在就已經生成好了2份hprof文件, 一份是沒有旋轉過屏幕的 ,一份是旋轉過屏幕多次的。


(c)然後選中Android Studio 最左邊的Captures 進行將hprof文件導出。導出的時候需要選擇保存的目錄以及文件名。

 d)打開MAT ,導入我們的2個hprof文件 Open File-->選擇文件-->Leak Suspects Report-->Finish:*

可以通過檢索包名,查看某個類的實例個數和所在內存數據,還可以查看被引用的內存數據。如下:

 

Objects:實例個數
Shallow Heap:所佔內存大小
Retained Heap:釋放後能回收多少內存


六、LeakCanary工具:

    簡單,傻瓜式操作,最重要的是LeakCanary 只在debug版本下檢測,正版先上線後自動跳過檢測這就方便開發者無需操作每次上線時註釋檢測代碼。這個工具是Square公司在Github開源的。行業內不是有一句話嘛,Square出品必屬精品,主流的庫像okhttp、Picasso、retrofit、Dagger等都出自Square之手。說到這不得不讓我聯想到一位在Android開發領域神一般存在的人物,他就是大名鼎鼎的Jake Wharton(傑克.沃頓),ButterKnife的創造者,也參與貢獻了Retrofit, okhttp等。

如何使用?GitHup官網https://github.com/square/leakcanary,首先在Gradle文件裏添加依賴。

在Application中寫方法:
 

    private RefWatcher setupLeakCanary() {
        if (LeakCanary.isInAnalyzerProcess(this)) {
            return RefWatcher.DISABLED;
        }
        return LeakCanary.install(this);
    }
 
    public static RefWatcher getRefWatcher(Context context) {
        MyApplication leakApplication = (MyApplication) context.getApplicationContext();
        return leakApplication.refWatcher;
    }

然後onCreate()中調用即可:refWatcher = setupLeakCanary();

在Activity中單獨調用檢測:

@Override
    protected void onDestroy() {
        super.onDestroy();
        RefWatcher refWatcher = MyApplication.getRefWatcher(this);//1
        refWatcher.watch(this);
    }

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章