1. 程式人生 > >使用LeakCanary檢測內存泄露 翻譯

使用LeakCanary檢測內存泄露 翻譯

tps 垃圾回收器 stat string 返回 增加 sele 意義 幸運

使用LeakCanary檢測內存泄露 翻譯
原文:https://academy.realm.io/cn/posts/droidcon-ricau-memory-leaks-leakcanary/
GitHub:https://github.com/square/leakcanary
Nov 18 2015


目錄

中文翻譯
介紹 (0:00)
內存泄漏:非技術講解 (1:40)
LeakCanary 救援 (3:47)
技術講解內存泄漏 (8:06)
分析堆 (10:16)
LeakCanary 救你於水火 (12:04)
LeakCanary API 演練 (13:32)
什麽是弱引用 (14:17)
HAHA 內存分析器 (16:55)
LeakCanary 的實現 (19:19)
Debug 一個真實的例子 (22:12)
忽略 SDK Crashes (28:10)
LeakCanary 的未來 (29:14)
Q&A (31:50)

中文翻譯

我們的 App 曾經遇到很多的內存泄漏導致 OutOfMemoryError 的崩潰,一些甚至是在生產環境。Square 的 Pierre-Yvews Ricau 開發了 LeakCanary 最終解決了這些問題。LeakCanary 是一個幫助你檢測和修復內存泄漏的工具。在這個分享中,Pierre 教授大家如何修復內存泄漏的錯誤,讓你的 App 更穩定和可靠。

介紹 (0:00)

大家好,我是 Pierre-Yvews Ricau (叫我 PY 就行),現在在 Square 工作。

Square 出了一款名為:Square Register 的 App, 幫助你用移動設備完成支付。在用這個 App 的時候,用戶先要登陸他的個人賬號。

不幸的是,在簽名頁面有的時候會因為內存溢出而出現崩潰。老實說,這個崩潰來的太不是時候了 — 用戶和商家都無法確認交易是否完成了,更何況是在和錢打交道的時候。我們也強烈的意識到,我們需要處理下內存溢出或者內存泄露這種事情了。

內存泄漏:非技術講解 (1:40)

我想要聊的內存泄露解決方案是: LeakCanary。

LeakCanary 是一個可以幫助你發現和解決內存泄露的開源工具。

但是到底什麽是內存泄露呢?我們從一個非技術角度來開始,先來舉個例子。
...
有外部的引用指向了本不應該再指向的對象。類似這樣的小規模的內存泄露堆積以後就會造成大麻煩。

LeakCanary 救援 (3:47)

這就是我們為什麽要開發 LeakCanary。

我現在可能已經清楚了 可被回收的 Android 對象應該及時被銷毀。

但是我還是沒法清楚的看到這些對象是否已經被回收掉。有了 LeakCanary 以後,我們:

給可被回收的 Android 對象上打了智能標記,智能標記能知道他們所指向的對象是否被成功釋放掉。

如果過一小段時間對象依然沒有被釋放,他就會給內存做個快照。LeakCanary 隨後會把結果發布出來:

幫助我們查看到內存到底怎麽泄露了,並清晰的向我們展示那些無法被釋放的對象的引用鏈。

舉個具體的例子:在我們的 Square App 裏的簽名頁面。用戶準備簽名的時候,App 因為內存溢出出錯崩潰了。我們不能確認內存錯誤到底出在哪兒了。

簽名頁面持有了一個很大的有用戶簽名的 Bitmap 圖片對象。圖片的大小和用戶手機屏幕大小一致 — 我們猜測這個有可能會造成內存泄露。首先,我們可以配置 Bitmap 為 alpha 8-bit 來節省內存。這是很常見的一種修復方案,而且效果也不錯。但是並沒有徹底解決問題,只是減少了泄露的內存總量。但是內存泄露依然在哪兒。

最主要的問題是我們 App 的堆滿了,應該要留有足夠的空間給我們的簽名圖片,但是由於很多處的內存泄露疊加在一起占用了很多內存。

技術講解內存泄漏 (8:06)

假設,我有一個 App,這個 App 點一下就能買一個法棍面包。

private static Button buyNowButton;

由於某種原因,我把這個 button 設置成了 static 的。問題隨之而來:

這個按鈕除非你設置成了null,不然就內存泄露了!

你也許會說:“只是一個按鈕而已,沒啥大不了”。問題是這個按鈕還有一個成員變量:叫 mContext,這個東西指向了一個 Acitvity,Acitivty 又指向了一個 Window,Window 又擁有整個 View 繼承樹。算下來,那可是一大段的內存空間。

靜態的變量是 GC root 類型的一種。垃圾回收器會嘗試回收所有非 GC root 的對象,或者某些被 GC root 持有的對象。所以如果你創建一個對象,並且移除了這個對象的所有指向,他就會被回收掉。但是一旦你將一個對象設置成 GC root,那他就不會被回收掉。

當你看到類似“法棍按鈕”的時候,很顯然這個按鈕持有了一個 Activity 的引用,所以我們必須清理掉它。當你沈浸在你的代碼的時候,你肯定很難發現這個問題。你可能只看到了引出的引用。你可以知道 Activity 引用了一個 Window,但是誰引用了 Activity?

你可以用像 IntelliJ 這樣的工具做些分析,但是它並不會告訴你所有的東西。通常,你可以把這些 Object 的引用關系組織成圖,但是是個單向圖。

分析堆 (10:16)

我們能做些什麽呢?我們來做個快照。

我們拿出所有的內存然後導出到文件裏,這個文件會被用來分析和解析堆結構。
其中一個工具叫做 Memory Analyzer,也叫 MAT。
它會通過 dump 的內存,然後分析所有存活在內存中的對象和類。

你可以用 SQL 對他做些查詢,類似如下:

SELECT * FROM INSTANCEOF android.app.Activity a WHERE a.mDestroyed = true

這條語句會返回所有的狀態為 destroyed 的實例。一旦你發現了泄露的 Activity,你可以執行 merge_shortest_paths 的操作來計算出最短的 GC root 路徑。從而找出阻止你 Acitivty 釋放的那個對象。

之所以說要 “最短路徑”,是因為通常從一個 GC root 到 Acitivty,有很多條路徑可以到達。比如說:我的按鈕的 parent view,同樣也持有一個 mContext 對象。

當我們看到內存泄露的時候,我們通常不需要去查看所有的這些路徑。我們只需要最短的一條。那樣的話,我們就排除了噪音,很快的找到問題所在。

LeakCanary 救你於水火 (12:04)

有 MAT 這樣一個幫我們發現內存泄露的工具是個很棒的事情。但是在一個正在運行的 App 的上下文中,我們很難像我們的用戶發現泄露那樣發現問題所在。我們不能要求他們在做一遍相同操作,然後留言描述,再把 70MB+ 的文件發回給我們。我們可以在後臺做這個,但是並不 Cool。我們期望的是,我們能夠盡早的發現泄露,比如在我們開發的時候就發現這些問題。這也是 LeakCanary 誕生的意義。

一個 Activity 有自己生命周期。你了解它是如何被創建的,如何被銷毀的,你期望他會在 onDestroy() 函數調用後,回收掉你所有的空閑內存。如果你有一個能夠檢測一個對象是否被正常的回收掉了的工具,那麽你就會很驚訝的喊出:“這個可能造成內存泄露!它本該被回收掉,但卻沒有被垃圾回收掉!”

Activity 無處不在。很多人都把 Activity 當做神級 Object 一般的存在,因為它可以操作 Services,文件系統等等。經常會發生對象泄漏的情況,如果泄漏對象還持有 context 對象,那 context 也就跟著泄漏了。

Resources resources = context.getResources();
LayoutInflater inflater = LayoutInflater.from(context);
File filesDir = context.getFilesDir();
InputMethodManager inputMethodManager =(InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);

LeakCanary API 演練 (13:32)

我們回過頭來再看看智能標記smart pin,我們希望知道的是當生命後期結束後,發生了什麽。幸運的時,LearkCanary有一個很簡單的 API。

第一步:創建 RefWatcher。這裏的Ref 其實是 Reference 的縮寫。給 RefWatcher 傳入一個對象的實例,它會檢測這個對象是否被成功釋放掉。

RefWatcher refWatcher = LeakCanary.install(this);

第二步:監聽 Activity 生命周期。然後,當 onDestroy 被調用的時候,我們傳入 Activity。

refWatcher.watch(this);// Make sure you don’t get installed twice.

什麽是弱引用 (14:17)

想要了解這個是怎麽工作的,我得先跟大家聊聊弱引用Weak References

我剛才提到過靜態域的變量會持有Activity 的引用。所以剛才說的“下單”按鈕就會持有 mContext 對象,導致 Activity 無法被釋放掉。這個被稱作強引用Strong Reference

一個對象可以有很多的強引用,在垃圾回收過程中,當這些強引用的個數總和為零的時候,垃圾回收器就會釋放掉它。
弱引用就是一種不增加引用總數的持有引用方式,垃圾回收期是否決定要回收一個對象,只取決於它是否還存在強引用。

所以說,如果我們:

將我們的 Activity 持有為弱引用,一旦我們發現弱引用持有的對象已經被銷毀了,那麽這個 Activity 就已經被垃圾回收器回收了。
否則,那可以大概確定這個 Activity 已經被泄露了。

弱引用的主要目的是為了做 Cache,而且非常有用。主要就是告訴 GC,盡管我持有了這個對象,但是如果一旦沒有對象在用這個對象的時候,GC 就可以在需要的時候銷毀掉。

在下面的例子中,我們繼承了 WeakReference:

final class KeyedWeakReference extends WeakReference<Object> {
  public final String key; //唯一標識符
  public final String name;

  KeyedWeakReference(Object referent, String key, String name, ReferenceQueue<Object> referenceQueue) {
    super(checkNotNull(referent, "referent"), checkNotNull(referenceQueue, "referenceQueue"));
    this.key = checkNotNull(key, "key");
    this.name = checkNotNull(name, "name");
  }
}

你可以看到,我們給弱引用添加了一個 Key,這個 Key 是一個唯一字符串。想法是這樣的:當我們解析一個heap dump文件的時候,我們可以遍歷所有的 KeyedWeakReference 實例,然後找到對應的 Key。

首先,我們創建一個 weakReference,然後我們寫入『一會兒,我需要檢查弱引用』。(盡管一會兒可能就是幾秒後)。當我們調用 watch 函數的時候,其實就是發生了這些事情。

public void watch(Object watchedReference, String referenceName) {
  checkNotNull(watchedReference, "watchedReference");
  checkNotNull(referenceName, "referenceName");
  if (debuggerControl.isDebuggerAttached()) return;
  final long watchStartNanoTime = System.nanoTime();
  String key = UUID.randomUUID().toString();
  retainedKeys.add(key);
  final KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, queue);

  watchExecutor.execute(() -> ensureGone(reference, watchStartNanoTime));
}

在這一切的背後,我們調用了 System.GC (免責聲明— 我們本不應該去做這件事情)。然而,這是一種告訴垃圾回收器:『Hey,垃圾回收器,現在是一個不錯的清理垃圾的時機。』,然後我們再檢查一遍,如果發現有些對象依然存活著,那麽可能就有問題了。我們就要觸發 heap dump 操作了。

HAHA 內存分析器 (16:55)

親手做 heap dump 是件超酷的事情。當我親手做這些的時候,花了很多時間和功夫。我每次都是做相同的操作:

下載 heap dump 文件,在內存分析工具裏打開它,找到實例,然後計算最短路徑。

但是我很懶,我根本不想一次次的做這個。(我們都很懶對吧,因為我們是開發者啊!)

我本可以為內存分析器寫一個 Eclipse 插件,但是 Eclipse 插件機制太糟糕了。後來我靈機一動,我其實可以把某個 Eclipse 的插件,移除 UI,利用它的代碼。

HAHA 是一個無 UI Android 內存分析器。基本上就是把另一個人寫的代碼重新打包。開始的時候,我就是 fork 了一份別的代碼然後移除了UI部分。兩年前,有人重新 fork 了我的代碼,然後添加了 Android 支持。又過了兩年,我才發現這個人的倉儲,然後我又重新打包上傳到了 maven center

我最近根據 Android Studio 修改了代碼實現。代碼還說的過去,還會繼續維護。

LeakCanary 的實現 (19:19)

我們有自己的庫去解析 heap dump 文件,而且實現的很容易。我們打開 heap dump,加載進來,然後解析。然後我們根據 key 找到我們的引用。然後我們根據已有的 Key 去查看擁有的引用。我們拿到實例,然後得到對象圖,再反向推導發現泄漏的引用。

以上(下)所有的工作都發生在 Android 設備上。
  • 當 LeakCanary 探測到一個 Activity 已經被銷毀掉,而沒有被垃圾回收器回收掉的時候,它就會強制導出一份 heap dump 文件存在磁盤上。
  • 然後開啟另外一個進程去分析這個文件得到內存泄漏的結果。如果在同一進程做這件事的話,可能會在嘗試分析堆內存結構的時候而發生內存不足的問題。
  • 最後,你會得到一個通知,點擊一下就會展示出詳細的內存泄漏鏈。而且還會展示出內存泄漏的大小,你也會很明確自己解決掉這個內存泄漏後到底能夠解救多少內存出來。

LeakCanary 也是支持 API 的,這樣你就可以添加內存泄漏的回調,比方說可以把內存泄漏問題傳到服務器上
用上 API 以後,我們的程序崩潰率降低了 94%!簡直棒呆!

Debug 一個真實的例子 (22:12)

這個是 Android 4年前的一次代碼修改留下的問題,當時是為了修復另一個 bug,然而帶來了無法避免的內存泄漏。我們也不知道何時能被修復。

忽略 SDK Crashes (28:10)

通常來說,總是有些內存泄漏是你無法修復的。我們某些時候需要忽略掉這些無法修復的內存泄漏提醒。在 LeakCanary 裏,有內置的方法去忽略無法修復的問題。

我想要重申一下,LeakCanary 只是一個開發工具。不要將它用到生產環境中。一旦有內存泄漏,就會展示一個通知給用戶,這一定不是用戶想看到的。

我們即便用上了 LeakCanary 依然有內存溢出的錯誤出現。我們的內存泄露依然有多個。有沒有辦法改變這些呢?

LeakCanary 的未來 (29:14)

public class OomExceptionHandler implements Thread.UncaughtExceptionHandler {
  private final Thread.UncaughtExceptionHandler defaultHandler;
  private final Context context;

  public OomExceptionHandler(Thread.UncaughtExceptionHandler defaultHandler, Context context) {...}

  @Override 
  public void UncaughtException(Thread thread, Throwable ex) {
    if (containsOom(ex)) {
      File heapDumpFile = new File(context.getFilesDir(), "out-of-memory.hprof");
      Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
    }
    defaultHandler.uncaughtException(thread, ex);
  }

  private boolean containsOom(Throwable ex) {...}
}

這是一個 Thread.UncaughtExceptionHandler,你可以將線程崩潰委托給它,它會導出 heap dump 文件,並且在另一個進程裏分析內存泄漏情況。

有了這個以後,我們就能做一些好玩兒的事情了,比如:列出所有的應該被銷毀卻依然在內存裏存活的 Activity,然後列出所有的 Detached View。我們可以依此來為泄漏的內存按重要性排序。

我實際上已經有一個很簡單的 Demo 了,是我在飛機上寫的。還沒有發布,因為還有些問題,最嚴重的問題是沒有足夠的內存去解析 heap dump 文件。想要修復這個問題,得想想別的辦法。比如采用 stream 的方法去加載文件等等。

Q&A (31:50)

Q: LeakCanary 能用於 Kotlin 開發的 App?
PY: 我不知道,但是應該是可以的,畢竟到最後他們都是字節碼,而且 Kotlin 也有引用。

Q:你們是在 Debug 版本一直開啟 LeakCanary 麽?還是只在最後的某些版本開啟做做測試
PY: 不同的人有不同的方法,我們通常是一直都開著的。

備註:這篇文章是2015年作者視頻內容的中文翻譯,有一些內容可能已經改變了。

使用LeakCanary檢測內存泄露 翻譯