LeakCanary 原理分析

本文主要內(nèi)容

  • 1、Reference 簡介
  • 2、LeakCanary 使用
  • 3丹鸿、LeakCanary 源碼分析

LeakCanary ,一種常見的內(nèi)存泄漏分析工具泣栈,它能分析出內(nèi)存泄漏點并以通知形式告訴使用者卜高,使用也比較簡單,但功能強大南片。

筆者第一次見到 LeakCanary 時掺涛,并不清楚它的原理,了解到它只是一個開源工程并不是官方工具之后疼进,覺得作者太牛逼了薪缆,內(nèi)存泄漏都可以檢測。今天我們一起來看 LeakCanary 的源碼伞广,揭開它的神秘面紗拣帽。

1、Reference 簡介

java中存在四種引用嚼锄,重新溫習一遍四種引用的用法及作用:

  • 強引用:最普遍的引用减拭,聲明一個變量就是強引用,比如 obj 区丑,當它被置為null時拧粪,該對象可能會被 JVM 回收修陡,因為還要看是否有其它強引用指向它。

    Object obj = new Object();
    
  • SoftReference可霎,軟引用:當內(nèi)存不夠用的時候魄鸦,才會回收軟引用

  • WeakReference,弱引用: new出來的對象沒有強引用連接時癣朗,下一次GC時拾因,就會回收該對象。

  • PhantomReference旷余,虛引用 : 與要與ReferenceQueue配合使用绢记,它的get()方法永遠返回null

SoftReference、WeakReference荣暮、PhantomReference等類都位于 java.lang.ref 包中庭惜,它們都有一個共同的父類,Reference 穗酥。

java.lang.ref包下主要都是reference相關(guān)的類护赊,主要包括:

  • FinalReference: 代表強引用,使沒法直接使用砾跃。
  • Finalizer:FinalReference的子類骏啰,主要處理finalize相關(guān)的工作
  • PhantomReference: 虛引用
  • Reference: 引用基類,abstract的
  • ReferenceQueue: 引用軌跡隊列
  • SoftReference:軟引用
  • WeakedReference: 弱引用

下面抽高,闡述關(guān)于Reference 相關(guān)的一個結(jié)論:

Reference引用的對象被回收時判耕,Reference 對象將被添加到 ReferenceQueue中,前提是構(gòu)造 Reference 時翘骂,參數(shù)中有 ReferenceQueue壁熄。

如果要監(jiān)聽某個對象是否被回收,有什么辦法呢碳竟?

Object obj = new Object();
ReferenceQueue<Object> queue = new ReferenceQueue<>();
WeakedReference  r = new WeakedReference(ojb, queue);

根據(jù)上面的結(jié)論草丧,如果 obj 對象被回收了,那么 queue 將添加 r莹桅,那么我們可以查找隊列昌执,如果有r,則證明 obj 對象被回收了诈泼,監(jiān)控完成懂拾。

查看下 Reference 的源碼,它里邊的關(guān)鍵代碼如下:

// 靜態(tài)變量,pending 
private static Reference pending = null;
// 線程,不停地回收 pending 
private static class ReferenceHandler extends Thread {

    ReferenceHandler(ThreadGroup g, String name) {
        super(g, name);
    }

    public void run() {
        for (;;) {

            Reference r;
            synchronized (lock) {
                if (pending != null) {
                    r = pending;
                    Reference rn = r.next;
                    pending = (rn == r) ? null : rn;
                    r.next = r;
                } else {
                    try {
                        lock.wait();
                    } catch (InterruptedException x) { }
                    continue;
                }
            }

            // Fast path for cleaners
            if (r instanceof Cleaner) {
                ((Cleaner)r).clean();
                continue;
            }
            // 將 r 添加到 ReferenceQueue 中
            ReferenceQueue q = r.queue;
            if (q != ReferenceQueue.NULL) q.enqueue(r);
        }
    }
}

看到這里穗泵,可能有同學會提問,pending 在哪被賦值的唐断?怎么知道它就是要被回收的 Reference 呢汁汗?確實,這個問題我也沒有從源碼中查到栗涂,從網(wǎng)上找的資料來看,都說 pending 由 JVM 維護祈争。Reference 有個成員變量next斤程,它可以很輕松地變成一個鏈表,而pending 正是一個鏈表的頭節(jié)點菩混,如果某個 Reference 將被回收忿墅,它將被 添加到pending 的鏈表當中,如果它馬上要被回收了沮峡,ReferenceHandler 線程將它添加到 ReferenceQueue 中疚脐。

通過 Reference 的學習,感覺 LeakCanary 最大的難題解決了邢疙,如何判定一個對象是否被回收了棍弄。

2、LeakCanary 使用

LeakCanary 使用非常簡單疟游,首先在你項目app下的build.gradle中配置:

dependencies {
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.2'
  releaseImplementation   'com.squareup.leakcanary:leakcanary-android-no-op:1.6.2'
  // 可選呼畸,如果你使用支持庫的fragments的話
  debugImplementation   'com.squareup.leakcanary:leakcanary-support-fragment:1.6.2'
}

然后在你的Application中配置:

public class WanAndroidApp extends Application {

@Override public void onCreate() {
  super.onCreate();
  if (LeakCanary.isInAnalyzerProcess(this)) {
    // 1
    return;
  }
  // 2
  refWatcher = LeakCanary.install(this);
}

使用就是這么得簡單。

怎么判斷一個對象已死呢颁虐?內(nèi)存可達性算法蛮原,即從一個根對象出發(fā),如果無法尋到一條路徑指向該對象另绩,則對象已死儒陨。

假設(shè)是我們自己來設(shè)計 LeakCanary ,我們會怎么去設(shè)計呢笋籽?目前已經(jīng)可以監(jiān)控某個對象是否已經(jīng)被回收了蹦漠。我們也不可能去監(jiān)聽所有的對象吧,這樣不現(xiàn)實干签,肯定是去找特定對象來監(jiān)控津辩,在Android中,常見的內(nèi)存泄漏都會導(dǎo)致 activity 無法被回收容劳,activity 就是最特定的對象喘沿,所以,可以監(jiān)聽 activity竭贩。

LeakCanary 正是這樣設(shè)計的蚜印,它目前可以監(jiān)聽 activity 和 fragment。

3留量、LeakCanary 源碼分析

先看看 install 方法:

public static @NonNull RefWatcher install(@NonNull Application application) {
return refWatcher(application).listenerServiceClass(DisplayLeakService.class)
    .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
    .buildAndInstall();
  }

install 方法返回 RefWatcher 對象窄赋,這是一個鏈式調(diào)用哟冬,我們一步步地來看。

public static @NonNull AndroidRefWatcherBuilder refWatcher(@NonNull Context context) {
return new AndroidRefWatcherBuilder(context);
}

refWatcher 方法返回 AndroidRefWatcherBuilder 對象忆绰,從名字可知它是一個構(gòu)造器浩峡,builder,它的構(gòu)造函數(shù)也很簡單错敢,保存context

AndroidRefWatcherBuilder(@NonNull Context context) {
this.context = context.getApplicationContext();
}

繼續(xù)回到 install 方法翰灾,查看 listenerServiceClass 方法:

public @NonNull AndroidRefWatcherBuilder listenerServiceClass(
  @NonNull Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
enableDisplayLeakActivity = DisplayLeakService.class.isAssignableFrom(listenerServiceClass);
return heapDumpListener(new ServiceHeapDumpListener(context, listenerServiceClass));
}

public final T heapDumpListener(HeapDump.Listener heapDumpListener) {
this.heapDumpListener = heapDumpListener;
return self();
}

AndroidRefWatcherBuilder 繼承自 RefWatcherBuilder ,上面的代碼就是為 AndroidRefWatcherBuilder 賦值一個成員變量稚茅,heapDumpListener 纸淮。繼續(xù)回到 install 方法

excludedRefs(AndroidExcludedRefs.createAppDefaults().build())

這句話的意思是,排除各種已經(jīng)的不是內(nèi)存泄漏的情況亚享。其實為什么要在第一步中指出咽块,這是一個 Builder呢,我們常見的 builder 代碼里欺税,有各種各樣的賦值侈沪,但最關(guān)鍵的代碼往往只是它的 build 方法,我們別被這種鏈式調(diào)用弄暈了魄衅,這些不重要峭竣,別死摳細節(jié)不放,大概明白意思就行晃虫,接下來我們看最重要的方法:

public @NonNull RefWatcher buildAndInstall() {
if (LeakCanaryInternals.installedRefWatcher != null) {
  throw new UnsupportedOperationException("buildAndInstall() should only be called once.");
}
//調(diào)用build方法皆撩,構(gòu)建 RefWatcher 
RefWatcher refWatcher = build();
if (refWatcher != DISABLED) {
  if (enableDisplayLeakActivity) {
    LeakCanaryInternals.setEnabledAsync(context, DisplayLeakActivity.class, true);
  }
  if (watchActivities) {
    //監(jiān)聽activity
    ActivityRefWatcher.install(context, refWatcher);
  }
    //監(jiān)聽 fragment
  if (watchFragments) {
    FragmentRefWatcher.Helper.install(context, refWatcher);
  }
}
LeakCanaryInternals.installedRefWatcher = refWatcher;
return refWatcher;
}

build 方法返回 RefWatcher ,比較簡單哲银,其實就是將之前鏈式調(diào)用賦值的各個對象扛吞,賦值給 RefWatcher

public final RefWatcher build() {
if (isDisabled()) {
  return RefWatcher.DISABLED;
}

if (heapDumpBuilder.excludedRefs == null) {
  heapDumpBuilder.excludedRefs(defaultExcludedRefs());
}

HeapDump.Listener heapDumpListener = this.heapDumpListener;
if (heapDumpListener == null) {
  heapDumpListener = defaultHeapDumpListener();
}

DebuggerControl debuggerControl = this.debuggerControl;
if (debuggerControl == null) {
  debuggerControl = defaultDebuggerControl();
}

HeapDumper heapDumper = this.heapDumper;
if (heapDumper == null) {
  heapDumper = defaultHeapDumper();
}

WatchExecutor watchExecutor = this.watchExecutor;
if (watchExecutor == null) {
  watchExecutor = defaultWatchExecutor();
}

GcTrigger gcTrigger = this.gcTrigger;
if (gcTrigger == null) {
  gcTrigger = defaultGcTrigger();
}

if (heapDumpBuilder.reachabilityInspectorClasses == null) {
  heapDumpBuilder.reachabilityInspectorClasses(defaultReachabilityInspectorClasses());
}

return new RefWatcher(watchExecutor, debuggerControl, gcTrigger, heapDumper, heapDumpListener,
    heapDumpBuilder);
}

最最關(guān)鍵的還得是 install 方法,本文中我們只分析 activity 的邏輯荆责,fragment 暫不分析滥比。

public static void install(@NonNull Context context, @NonNull RefWatcher refWatcher) {
Application application = (Application) context.getApplicationContext();
ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
application.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks);
}

install 方法中只有三行,第一步做院,獲取 Application 對象盲泛,第二步,生成一個 ActivityRefWatcher 對象键耕,第三步寺滚,看方法名,貌似是注冊了一個監(jiān)聽屈雄,一個activity生命周期的監(jiān)聽村视。看看 lifecycleCallbacks 這個回調(diào)對象:

private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
  new ActivityLifecycleCallbacksAdapter() {
    @Override public void onActivityDestroyed(Activity activity) {
      refWatcher.watch(activity);
    }
  };

關(guān)鍵代碼終于出現(xiàn)酒奶,當activity 調(diào)用 onDestroyed 方法時蚁孔,會調(diào)用lifecycleCallbacks 中的方法奶赔,從而可以拿到 activity 的引用。結(jié)合之前 Reference 的分析杠氢,要監(jiān)聽對象是否被回收站刑,首先得拿到它的引用,現(xiàn)在 activity 的引用拿到了鼻百。繼續(xù)往下分析笛钝。

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

ensureGoneAsync(watchStartNanoTime, reference);
}

通過拿到的 activity 引用,構(gòu)造 KeyedWeakReference 對象愕宋,其實它繼承自 WeakReference ,它是一個弱引用结榄。構(gòu)建弱引用的同時中贝,在構(gòu)造函數(shù)中添加 ReferenceQueue,當 activity 被回收時臼朗,KeyedWeakReference 對象會被添加到ReferenceQueue當中邻寿。如果出現(xiàn)內(nèi)存泄漏,則 ReferenceQueue 找不到對應(yīng)的 KeyedWeakReference 對象视哑,那么就可以判斷發(fā)生內(nèi)存泄漏了绣否。

private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
watchExecutor.execute(new Retryable() {
  @Override public Retryable.Result run() {
    return ensureGone(reference, watchStartNanoTime);
  }
});
}

ensureGoneAsync方法中,通過 watchExecutor 執(zhí)行一個 run 方法挡毅,watchExecutor 只會在主線程中執(zhí)行它蒜撮,繼續(xù)查看 ensureGone方法

Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
long gcStartNanoTime = System.nanoTime();
long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);

removeWeaklyReachableReferences();

if (debuggerControl.isDebuggerAttached()) {
  // The debugger can create false leaks.
  return RETRY;
}
//gone方法即是查看到 activity 已經(jīng)被回收,則返回 DONE跪呈,表示內(nèi)存無漏泄
if (gone(reference)) {
  return DONE;
}
//調(diào)用 gc
gcTrigger.runGc();
removeWeaklyReachableReferences();
//調(diào)用gc之后段磨,再次檢查 ,如果 activity 還沒被回收耗绿,則是有內(nèi)存泄漏了
if (!gone(reference)) {
  long startDumpHeap = System.nanoTime();
  long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);

  File heapDumpFile = heapDumper.dumpHeap();
  if (heapDumpFile == RETRY_LATER) {
    // Could not dump the heap.
    return RETRY;
  }
  long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);

  HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key)
      .referenceName(reference.name)
      .watchDurationMs(watchDurationMs)
      .gcDurationMs(gcDurationMs)
      .heapDumpDurationMs(heapDumpDurationMs)
      .build();
  // 分析 heap 的prof文件苹支,找出內(nèi)存泄漏點
  heapdumpListener.analyze(heapDump);
}
return DONE;
}

如果 activity 已經(jīng)被回收,那么 ReferenceQueue 中將添加指向它的 Reference误阻,這是一條大原則债蜜,一定要記住。在 watch 方法時究反,為每個 activity構(gòu)造對應(yīng)的Reference時寻定,還添加了一個key,并把key添加到一個set當中奴紧。

private void removeWeaklyReachableReferences() {
// WeakReferences are enqueued as soon as the object to which they point to becomes weakly
// reachable. This is before finalization or garbage collection has actually happened.
KeyedWeakReference ref;
while ((ref = (KeyedWeakReference) queue.poll()) != null) {
  retainedKeys.remove(ref.key);
}
}

遍歷 ReferenceQueue 中得到的Reference特姐,拿到 Reference,同時刪除 set 中對應(yīng)的key黍氮。所以唐含,set中不包含某個 key浅浮,則說明對應(yīng)的 activity已經(jīng)被回收,反之則是沒有回收捷枯。

private boolean gone(KeyedWeakReference reference) {
return !retainedKeys.contains(reference.key);
}

gone方法正好驗證這點滚秩,如果gone返回為true,那么整個過程也結(jié)束了淮捆。如果gone返回為false郁油,則表明可能有內(nèi)存泄漏,所以執(zhí)行一次gc之后 攀痊,再次調(diào)用gone方法桐腌,查看是否有無泄漏。如果還有泄漏苟径,則分析生成的prof文件案站,找出關(guān)鍵的泄漏路徑。關(guān)于如何分析 prof 文件棘街,此處不展開了蟆盐,其實 LeakCanary 也是借用其它的開源庫來分析的。

最后總結(jié)下整個過程:

在一個Activity執(zhí)行完onDestroy()之后遭殉,將它放入WeakReference中石挂,然后將這個WeakReference類型的Activity對象與ReferenceQueque關(guān)聯(lián)。這時再從ReferenceQueque中查看是否有沒有該對象险污,如果沒有痹愚,執(zhí)行g(shù)c,再次查看蛔糯,還是沒有的話則判斷發(fā)生內(nèi)存泄露了里伯。最后用HAHA這個開源庫去分析dump之后的heap內(nèi)存。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末渤闷,一起剝皮案震驚了整個濱河市疾瓮,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌飒箭,老刑警劉巖狼电,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異弦蹂,居然都是意外死亡肩碟,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門凸椿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來削祈,“玉大人,你說我怎么就攤上這事∷枰郑” “怎么了咙崎?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長吨拍。 經(jīng)常有香客問我褪猛,道長,這世上最難降的妖魔是什么羹饰? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任伊滋,我火速辦了婚禮,結(jié)果婚禮上队秩,老公的妹妹穿的比我還像新娘笑旺。我一直安慰自己,他們只是感情好馍资,可當我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布燥撞。 她就那樣靜靜地躺著,像睡著了一般迷帜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上色洞,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天戏锹,我揣著相機與錄音,去河邊找鬼火诸。 笑死锦针,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的置蜀。 我是一名探鬼主播奈搜,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼盯荤!你這毒婦竟也來了馋吗?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤秋秤,失蹤者是張志新(化名)和其女友劉穎宏粤,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體灼卢,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡绍哎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了鞋真。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片崇堰。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出海诲,到底是詐尸還是另有隱情繁莹,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布饿肺,位于F島的核電站蒋困,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏敬辣。R本人自食惡果不足惜雪标,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望溉跃。 院中可真熱鬧村刨,春花似錦、人聲如沸撰茎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽龄糊。三九已至逆粹,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間炫惩,已是汗流浹背僻弹。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留他嚷,地道東北人蹋绽。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像筋蓖,于是被迫代替她去往敵國和親卸耘。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內(nèi)容