LeakCanary 傻瓜式的記憶體洩露檢測工具

姜家志發表於2017-04-08

在開發Android應用的過程中如果需要處理圖片或者大量資料的時候,就有可能會遇到OOMjava.lang.OutOfMemoryError),一般出現最多的是在建立Bitmap上,也有可能是在記憶體中處理了大量的資料上。出現OOM應用會直接崩潰,即使沒有出現OOM,記憶體使用過大的時候應用也會出現卡頓。所以記憶體的優化在開發Android應用時是一個比較重要的任務。
一般會針對Bitamp的記憶體優化有下面幾種方式:

1. 增加程式的記憶體
2. 使用Bitmap.Config.ALPHA_8(圖片失真)
3. 顯示的呼叫System.gc()
4. catch Exception
5. 呼叫bitmap.recycle()
6. 縮小bitmap的大小(如果是讀取的原圖是一個大圖應該先採用這種方式,Bitmap如果是剛好適配螢幕的就不需要縮小了)
7. 使用弱引用和軟引用(google已經不建議使用了,Android的GC效率非常高,只要保證物件沒有被引用即可)複製程式碼

但是我們忽略掉一個問題就是什麼造成了OOM
一般來說發生OOM崩潰的地方不一定是記憶體洩露的地方,崩潰的原因有可能是Activity造成的記憶體洩露,也可能是運算元據庫造成的記憶體洩露,當記憶體已經非常接近峰值的時候,這個時候恰巧要建立一個Bitmap物件就會發生OOM(Bitmap物件佔用的記憶體比較大)。
是什麼原因造成了記憶體洩露呢?

記憶體洩露

我們知道Android中每個物件都有自己的生命週期,比如Activity的生命週期最後會呼叫onDestroy方法做銷燬處理,但如果使用Activity中呼叫了類似於Toast這種物件,就會把這個Activity的引用傳給了Toast,而Toast的生命週期不會隨著Activity的銷燬而銷燬,這樣就造成了Activity的記憶體洩露,它會被Toast物件引用,無法被銷燬。
常見的記憶體洩露形成的原因:

  • Toast持有Activity的引用
  • 資料庫遊標Cursor沒有關閉
  • Adapter沒有複用convertView
  • 物件被生命週期更長的物件引用,Activity被靜態集合引用
  • ....

那如何知道應用的記憶體有沒有出現洩露呢?

監控記憶體的方式

Heap Dump:常見的記憶體監控方式是Heap DumpHeap Dump是一種在Java中比較常用的檢測記憶體的方式:

簡單來說就是我們在一個初始狀態A, 在這個時候Dump一次記憶體,在做了一些操作之後回到狀態A,再Dump一次記憶體。
對兩次Dunp的記憶體資料(hprof)使用分析工具做分析(MAT),根據分析的結果就能知道是否存在記憶體洩露,這種方式比較複雜和繁瑣並不是特別易用。

Moitors:這是Android SDK自帶的記憶體監控工具,Monitors能檢測到記憶體的變化,比如記憶體是增加還是減少。
開啟一個Activity會導致記憶體增加,關閉一個Activity會導致記憶體減少,反覆的做這樣的操作,如果每次開啟一個Activity再關閉之後增加的記憶體不會減少就說明這個Activity有可能有記憶體洩露,再借助log輔助進行檢測,就可以發現記憶體洩露的問題,
這種方式的缺點是並不是特別的準確,因為記憶體的釋放和物件的生命週期有關也和GC的排程有關。
另一種方式就是LeakCanary,LeakCanary是一個簡單的,方便的記憶體檢測工具,可以輕易的發現記憶體問題,還會生成更加簡單清晰的報告。

LeakCanary

LeakCanary是一個開源的檢測記憶體洩露的java庫。專案地址:github.com/square/leak…
LeakCanary實際上就是在本機上自動做了Heap dump,對生成的hprof檔案進行分析,展示分析的結果。和手工分析Heap Dump的方式得到的結果是一樣的。只不過這部分的工作完全自動化完成了。
下面是一個LeakCanary的結果截圖:

LeakCanary 傻瓜式的記憶體洩露檢測工具
LeakCanary

從上圖可以看到,LeakCanary能清晰簡單的展示出那裡有記憶體洩露的問題。那LeakCanary如何使用呢?

整合LeakCanary

build.gradle新增依賴:

dependencies {
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3.1'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1'
   testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1'
 }複製程式碼

使用LeakCanary對應用進行檢測它會影響程式的效能,尤其是在做Heap dump分析操作時,因此需要在依賴裡面指定對應的版本,debug的時候才進行分析,release的時候不能進行分析。
debugCompile可以使用檢測版本:

com.squareup.leakcanary:leakcanary-android

releaseCompile使用no-op模式,即No Operation Performed就是不會把對應的類庫編譯,指定類庫為無用的指令:

com.squareup.leakcanary:leakcanary-android-no-op

這樣就可以指定LeakCanary為無用指令,不會在release的時候進行編譯。
Application中加入分析Activity的程式碼:

public class ExampleApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    LeakCanary.install(this);
  }
}複製程式碼

這樣就可以檢測所有Activity的記憶體洩露了。LeakCanary內部實現使用了ActivityLifecycleCallbacks方法監聽所有Activity的生命週期。
除了Activity會發生記憶體洩露以外,其他物件也有可能會出現記憶體洩露,如果對其他物件進行檢測呢?

檢測其他物件

LeakCanary中提供了RefWatcher類,可以用來監控所有的物件。
首先例項化RefWatcher:

public static RefWatcher sRefWatcher=LeakCanary.install(mContext);複製程式碼

對於監控的物件使用:

sRefWatcher.watch(this)複製程式碼

一般我們是在物件銷燬的時候對物件進行監控,比如內部實現的對於Activity的監控的原理如下:

private final ActivityLifecycleCallbacks lifecycleCallbacks = new ActivityLifecycleCallbacks() {
        public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        }

        public void onActivityStarted(Activity activity) {
        }

        public void onActivityResumed(Activity activity) {
        }

        public void onActivityPaused(Activity activity) {
        }

        public void onActivityStopped(Activity activity) {
        }

        public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
        }

        public void onActivityDestroyed(Activity activity) {
            ActivityRefWatcher.this.onActivityDestroyed(activity);
        }
    };複製程式碼

只是在onActivityDestroyed的時候才對於activity進行監控即可。
檢測到了記憶體洩露,如果解決呢?

解決記憶體洩露

一般情況記憶體洩露的原因都是由於引用的使用不當造成的,Android GC能夠保證回收迴圈引用(如果一個迴圈引用沒有外部引用時就會被回收),且Android GC效率很高,當然GC的演算法本身也在不停的改進。
一般情況下只需要儘量避免錯誤的引用方式帶來的記憶體洩露問題即可:

  • 生命週期長的物件引用生命週期短的物件,比如static的物件群引用Activity
  • 使用Application的Context物件,而不是Activity的Context
  • 避免非靜態類的內部類對於類的隱式引用,使用靜態的內部類
  • 使用Android的快取機制,比如ListView的複用機制
  • 手動關閉資源,比如Curous的關閉
  • registerReceiver和unRegisterReceiver成對出現

相關文章