007 LeakCanary 內(nèi)存泄漏原理完全解析

一驾霜、 什么是LeakCanary

LeakCanary 是大名鼎鼎的 square 公司開源的內(nèi)存泄漏檢測(cè)工具。目前上大部分App在開發(fā)測(cè)試階段都會(huì)接入此工具用于檢測(cè)潛在的內(nèi)存泄漏問題饺汹,做的好一點(diǎn)的可能會(huì)搭建一個(gè)服務(wù)器用于保存各個(gè)設(shè)備上的內(nèi)存泄漏問題再集中處理。

本文首發(fā)于我的微信公眾號(hào):Android開發(fā)實(shí)驗(yàn)室做个,歡迎大家關(guān)注和我一起學(xué)Android淤井,掉節(jié)操。

二工猜、 為什么要使用LeakCanary

我們知道內(nèi)存泄漏問題的排查有很多種方法米诉, 比如說,Android Studio 自帶的 Profile 工具篷帅、MAT(Memory Analyzer Tool)史侣、以及LeakCanary。 選擇 LeakCanary 作為首選的內(nèi)存泄漏檢測(cè)工具主要是因?yàn)樗軐?shí)時(shí)檢測(cè)泄漏并以非常直觀的調(diào)用鏈方式展示內(nèi)存泄漏的原因魏身。

三惊橱、 LeakCanary 做不到的(待定)

雖然 LeakCanary 有諸多優(yōu)點(diǎn),但是它也有做不到的地方箭昵,比如說檢測(cè)申請(qǐng)大容量?jī)?nèi)存導(dǎo)致的OOM問題税朴、Bitmap內(nèi)存未釋放問題,Service 中的內(nèi)存泄漏可能無(wú)法檢測(cè)等家制。

四正林、 LeakCanary 源碼解析

本章內(nèi)容前后依賴關(guān)系強(qiáng)烈,建議順序閱讀颤殴。

4.1 ActivityLifecycleCallbacks 與 FragmentLifeCycleCallbacks

在開始 LeakCanary 原理解析之前觅廓,有必要簡(jiǎn)單說下 ActivityLifecycleCallbacks 與 FragmentLifeCycleCallbacks。

// ActivityLifecycleCallbacks 接口
public interface ActivityLifecycleCallbacks {
    void onActivityCreated(Activity var1, Bundle var2);

    void onActivityStarted(Activity var1);

    void onActivityResumed(Activity var1);

    void onActivityPaused(Activity var1);

    void onActivityStopped(Activity var1);

    void onActivitySaveInstanceState(Activity var1, Bundle var2);

    void onActivityDestroyed(Activity var1);
  }

Application 類提供了 registerActivityLifecycleCallbacksunregisterActivityLifecycleCallbacks 方法用于注冊(cè)和反注冊(cè) Activity 的生命周期監(jiān)聽類涵但,這樣我們就能在 Application 中對(duì)所有的 Activity 生命周期回調(diào)中做一些統(tǒng)一處理杈绸。

public abstract static class FragmentLifecycleCallbacks {

    public void onFragmentCreated(FragmentManager fm, Fragment f, Bundle savedInstanceState) {}

    public void onFragmentViewDestroyed(FragmentManager fm, Fragment f) {}

    public void onFragmentDestroyed(FragmentManager fm, Fragment f) {}

    // 省略其他的生命周期 ...
  }

FragmentManager 類提供了 registerFragmentLifecycleCallbacksunregisterFragmentLifecycleCallbacks 方法用戶注冊(cè)和反注冊(cè) Fragment 的生命周期監(jiān)聽類,這樣我們對(duì)每一個(gè) Activity 進(jìn)行注冊(cè)贤笆,就能獲取所有的 Fragment 生命周期回調(diào)蝇棉。

4.2 LeakCanary 的使用

4.2.1 使用方法

我們直接在 Application 類中讨阻,添加一下代碼即可芥永。

public class ExampleApplication extends Application {
  @Override public void onCreate() {
    super.onCreate();
    setupLeakCanary();
  }

  protected void setupLeakCanary() {
    // 啟用嚴(yán)格模式
    enabledStrictMode();
    // 判斷是否是 HeapAnalyzerService 所屬進(jìn)程
    if (LeakCanary.isInAnalyzerProcess(this)) {
      // This process is dedicated to LeakCanary for heap analysis.
      // You should not init your app in this process.
      return;
    }
    // 注冊(cè) LeakCanary
    LeakCanary.install(this);
  }

  private static void enabledStrictMode() {
    StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() //
        .detectAll() //
        .penaltyLog() //
        .penaltyDeath() //
        .build());
  }
}
<service
    android:name=".internal.HeapAnalyzerService"
    android:process=":leakcanary"
    android:enabled="false"
    />

由于 LeakCanary 的核心 hropf 文件解析服務(wù) HeapAnalyzerService 所屬進(jìn)程是與主進(jìn)程獨(dú)立的一個(gè)進(jìn)程,所以在 setupLeakCanary中钝吮,我們需要排除其他進(jìn)程埋涧,只對(duì) leakcanary 進(jìn)程注冊(cè) LeakCanary 監(jiān)聽處理板辽。

android:enabled="false" 這是什么?
這里簡(jiǎn)單說下棘催,AndroidManifest文件中的 enabled 屬性劲弦,可以看到 HeapAnalyzerService 這個(gè)組件默認(rèn)是不可用的,所以如果在代碼中動(dòng)態(tài)啟用這個(gè)組件醇坝,可以使用以下方法:

public static void setEnabledBlocking(Context appContext, Class<?> componentClass,
      boolean enabled) {
  ComponentName component = new ComponentName(appContext, componentClass);
  PackageManager packageManager = appContext.getPackageManager();
  int newState = enabled ? COMPONENT_ENABLED_STATE_ENABLED : COMPONENT_ENABLED_STATE_DISABLED;
  // Blocks on IPC.
  packageManager.setComponentEnabledSetting(component, newState, DONT_KILL_APP);
}

4.3 LeakCanary.install(this) 干了什么

LeakCanary 的 install 方法實(shí)際上構(gòu)造了一個(gè) RefWatcher,

/**
   * Creates a {@link RefWatcher} that works out of the box, and starts watching activity
   * references (on ICS+).
   */
public static @NonNull RefWatcher install(@NonNull Application application) {
  return refWatcher(application).listenerServiceClass(DisplayLeakService.class)
      .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
      .buildAndInstall();
}

我們一個(gè)個(gè)來看這個(gè)注冊(cè)方法邑跪。首先是 refWatcher 方法構(gòu)造了一個(gè) AndroidRefWatcherBuilder, 傳入?yún)?shù)是當(dāng)前Application 的 Context.

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

listenerServiceClass 和 excludedRefs 方法是基于建造者模式傳入分析Service 和 排除已知的泄漏問題 AndroidExcludedRefs,這里我就不貼代碼了呼猪。

重點(diǎn)看下 buildAndInstall 方法画畅,這個(gè)方法很形象的表示將要進(jìn)行建造者模式的最后一步 build 和 注冊(cè)一些監(jiān)聽器,下面我們來看具體代碼:

public @NonNull RefWatcher buildAndInstall() {
  // 只允許 install 一次
  if (LeakCanaryInternals.installedRefWatcher != null) {
    throw new UnsupportedOperationException("buildAndInstall() should only be called once.");
  }
  // 建造者模式的最后一步宋距,構(gòu)造對(duì)象
  RefWatcher refWatcher = build();
  // 判斷是否開啟了 LeakCanary轴踱,沒有開啟默認(rèn)會(huì)返回 DISABLED 對(duì)象
  if (refWatcher != DISABLED) {
    // 手動(dòng)開啟 DisplayLeakActivity 組件,會(huì)在桌面上顯示一個(gè)查看內(nèi)存泄漏結(jié)果的入口
    LeakCanaryInternals.setEnabledAsync(context, DisplayLeakActivity.class, true);
    // 是否檢測(cè) Activity 的 內(nèi)存泄漏谚赎,默認(rèn)開啟
    if (watchActivities) {
      ActivityRefWatcher.install(context, refWatcher);
    }
    // 是否檢測(cè) Fragment 的 內(nèi)存泄漏淫僻,默認(rèn)開啟
    if (watchFragments) {
      FragmentRefWatcher.Helper.install(context, refWatcher);
    }
  }
  // 復(fù)制給全局靜態(tài)變量,防止二次調(diào)用
  LeakCanaryInternals.installedRefWatcher = refWatcher;
  return refWatcher;
}

以上代碼作用大部分都在代碼中注釋了壶唤,剩下 ActivityRefWatcher.install 和 FragmentRefWatcher.Helper.install 方法沒有注釋雳灵。下面我們就來具體看看這兩個(gè)方法究竟干了什么。

(1). ActivityRefWatcher.install

ActivityRefWatcher 的靜態(tài)方法 install 獲取到了當(dāng)前 Application闸盔,然后添加了一個(gè)生命周期監(jiān)聽器 ActivityLifecycleCallbacks细办,這里的 lifecycleCallbacks 僅僅關(guān)注了 Activity 銷毀的回調(diào) onActivityDestroyed,在這里將傳入的對(duì)象 activity 監(jiān)聽起來蕾殴, refWatcher.watch(activity); 的具體代碼我們稍后分析笑撞。

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);
}

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

(2). FragmentRefWatcher.Helper.install
FragmentRefWatcher.Helper 的靜態(tài)方法 install 里同樣會(huì)注冊(cè)一個(gè) ActivityLifecycleCallbacks 用于監(jiān)聽 Activity 生命周期中的 onActivityCreated 的創(chuàng)建完成的回調(diào),在 Activity 創(chuàng)建完成后钓觉,會(huì)對(duì)這個(gè) Activity 注冊(cè) Fragment 的生命周期監(jiān)聽器茴肥。install 方法首先會(huì)判斷系統(tǒng)是否大于等于 Android O, 如果是那么會(huì)使用 android.app.FragmentManager 進(jìn)行注冊(cè)荡灾,如果需要兼容 Android O 以下需要自行在依賴中添加對(duì) leakcanary-support-fragment 組件的依賴瓤狐,然后通過反射構(gòu)造出SupportFragmentRefWatcher; 然后將fragmentRefWatchers所有監(jiān)聽器取出,在 Activity 創(chuàng)建完成后批幌,添加 Fragment 的生命監(jiān)聽础锐,主要關(guān)注 Fragment 的 onFragmentViewDestroyedonFragmentDestroyed 方法。具體代碼如下:

public static void install(Context context, RefWatcher refWatcher) {
      List<FragmentRefWatcher> fragmentRefWatchers = new ArrayList<>();
      // 系統(tǒng)是否大于等于 Android O荧缘,如果是皆警,添加 AndroidOFragmentRefWatcher
      if (SDK_INT >= O) {
        fragmentRefWatchers.add(new AndroidOFragmentRefWatcher(refWatcher));
      }
      // 如果添加了leakcanary-support-fragment的依賴,通過反射可以構(gòu)造SupportFragmentRefWatcher
      try {
        Class<?> fragmentRefWatcherClass = Class.forName(SUPPORT_FRAGMENT_REF_WATCHER_CLASS_NAME);
        Constructor<?> constructor =
            fragmentRefWatcherClass.getDeclaredConstructor(RefWatcher.class);
        FragmentRefWatcher supportFragmentRefWatcher =
            (FragmentRefWatcher) constructor.newInstance(refWatcher);
        fragmentRefWatchers.add(supportFragmentRefWatcher);
      } catch (Exception ignored) {
      }

      if (fragmentRefWatchers.size() == 0) {
        return;
      }

      Helper helper = new Helper(fragmentRefWatchers);
      // 先監(jiān)聽 Activity 的創(chuàng)建完成回調(diào)
      Application application = (Application) context.getApplicationContext();
      application.registerActivityLifecycleCallbacks(helper.activityLifecycleCallbacks);
    }

    private final Application.ActivityLifecycleCallbacks activityLifecycleCallbacks =
      new ActivityLifecycleCallbacksAdapter() {
        @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
          // Activity 創(chuàng)建完成后截粗,對(duì)Activity中的Fragment注冊(cè)生命周期監(jiān)聽
          for (FragmentRefWatcher watcher : fragmentRefWatchers) {
            watcher.watchFragments(activity);
          }
        }
    };
// AndroidOFragmentRefWatcher.java

private final FragmentManager.FragmentLifecycleCallbacks fragmentLifecycleCallbacks =
  new FragmentManager.FragmentLifecycleCallbacks() {
    // Fragment 中的View 視圖銷毀時(shí)
    @Override public void onFragmentViewDestroyed(FragmentManager fm, Fragment fragment) {
      View view = fragment.getView();
      if (view != null) {
        refWatcher.watch(view);
      }
    }
    // Fragment 銷毀時(shí)
    @Override
    public void onFragmentDestroyed(FragmentManager fm, Fragment fragment) {
      refWatcher.watch(fragment);
    }
  };

@Override public void watchFragments(Activity activity) {
  // 通過FragmentManager 注冊(cè) FragmentLifecycleCallbacks
  FragmentManager fragmentManager = activity.getFragmentManager();
  fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true);
}

上述流程我們已經(jīng)完全搞清楚了信姓,用一個(gè)流程圖可以表示為:

[圖片上傳失敗...(image-f5fe25-1543852247130)]

4.4 LeakCanary 內(nèi)存泄漏檢測(cè)原理

從行文結(jié)構(gòu)上鸵隧,本小節(jié)應(yīng)屬于上一節(jié)后半部分內(nèi)容,但是RefWatcher 的 watch 方法足夠重要和復(fù)雜意推,所以有必要單獨(dú)列一節(jié)仔細(xì)講解內(nèi)部原理豆瘫。

4.4.1 基礎(chǔ)知識(shí)——弱引用 WeakReference 和 引用隊(duì)列 ReferenceQueue

關(guān)于引用類型和引用隊(duì)列相關(guān)知識(shí),讀者可以參考白話 JVM——深入對(duì)象引用菊值,這篇文章我認(rèn)為講解的比較清晰外驱。

這里,我簡(jiǎn)單舉個(gè)例子腻窒,弱引用在定義的時(shí)候可以指定引用對(duì)象和一個(gè) ReferenceQueue略步,弱引用對(duì)象在垃圾回收器執(zhí)行回收方法時(shí),如果原對(duì)象只有這個(gè)弱引用對(duì)象引用著定页,那么會(huì)回收原對(duì)象趟薄,并將弱引用對(duì)象加入到 ReferenceQueue,通過 ReferenceQueue 的 poll 方法典徊,可以取出這個(gè)弱引用對(duì)象杭煎,獲取弱引用對(duì)象本身的一些信息∽渎洌看下面這個(gè)例子羡铲。

mReferenceQueue = new ReferenceQueue<>();
// 定義一個(gè)對(duì)象
o = new Object();
// 定義一個(gè)弱引用對(duì)象引用 o,并指定引用隊(duì)列為 mReferenceQueue
weakReference = new WeakReference<Object>(o, mReferenceQueue);
// 去掉強(qiáng)引用
o = null;
// 觸發(fā)應(yīng)用進(jìn)行垃圾回收
Runtime.getRuntime().gc();
// hack: 延時(shí)100ms,等待gc完成
try {
    Thread.sleep(100);
} catch (InterruptedException e) {
    e.printStackTrace();
}
Reference ref = null;
// 遍歷 mReferenceQueue,取出所有弱引用
while ((ref = mReferenceQueue.poll()) != null) {
    System.out.println("============ \n ref in queue");
}

打印結(jié)果為:

============
ref in queue

4.4.2 基礎(chǔ)知識(shí)——hprof文件

hprof 文件可以展示某一時(shí)刻java堆的使用情況儡毕,根據(jù)這個(gè)文件我們可以分析出哪些對(duì)象占用大量?jī)?nèi)存和未在合適時(shí)機(jī)釋放也切,從而定位內(nèi)存泄漏問題。

Android 生成 hprof 文件整體上有兩種方式:

  1. 使用 adb 命令
adb shell am dumpheap <processname> <FileName>
  1. 使用 android.os.Debug.dumpHprofData 方法
    直接使用 Debug 類提供的 dumpHprofData 方法即可腰湾。
Debug.dumpHprofData(heapDumpFile.getAbsolutePath());

Android Studio 自帶 Android Profiler 的 Memory 模塊的 dump 操作使用的是方法一雷恃。這兩種方法生成的 .hprof 文件都是 Dalvik 格式,需要使用 AndroidSDK 提供的 hprof-conv 工具轉(zhuǎn)換成J2SE HPROF格式才能在MAT等標(biāo)準(zhǔn) hprof 工具中查看费坊。

hprof-conv dump.hprof converted-dump.hprof  

至于hprof內(nèi)部格式如何倒槐,本文不做具體介紹,以后有機(jī)會(huì)再單獨(dú)寫一篇文章來仔細(xì)講解附井。LeakCanary 解析 .hprof 文件用的是 square 公司開源的另一項(xiàng)目:haha.

4.4.3 watch方法

終于到了 LeakCanary 關(guān)鍵部分了讨越。我們從 watch 方法入手,前面的代碼都是為了增強(qiáng)魯棒性永毅,我們直接從生成唯一id開始把跨,LeakCanary 構(gòu)造了一個(gè)帶有 key 的弱引用對(duì)象,并且將 queue 設(shè)置為弱引用對(duì)象的引用隊(duì)列沼死。

這里解釋一下着逐,為什么需要?jiǎng)?chuàng)建一個(gè)帶有 key 的弱引用對(duì)象,不能直接使用 WeakReference 么?
舉個(gè)例子滨嘱,假設(shè) OneActivity 發(fā)生了內(nèi)存泄漏峰鄙,那么執(zhí)行 GC 操作時(shí)浸间,肯定不會(huì)回收 Activity 對(duì)象太雨,這樣 WeakReference 對(duì)象也不會(huì)被回收。假設(shè)當(dāng)前啟動(dòng)了 N 個(gè) OneActivity魁蒜,Dump內(nèi)存時(shí)我們可以獲取到內(nèi)存中的所有 OneActivity囊扳,但是當(dāng)我們準(zhǔn)備去檢測(cè)其中某一個(gè) Activity 的泄漏問題時(shí),我們就無(wú)法匹配兜看。但是如果使用了帶有 key 的 WeakReference 對(duì)象锥咸,發(fā)生泄露時(shí)泄漏時(shí),key 的值也會(huì) dump 保存下來细移,這樣我們根據(jù) key 的一一對(duì)應(yīng)關(guān)系就能映射到某一個(gè) Activity搏予。

然后,LeakCanary 調(diào)用了 ensureGoneAsync 方法去檢測(cè)內(nèi)存泄漏弧轧。

public void watch(Object watchedReference, String referenceName) {
  if (this == DISABLED) {
    return;
  }
  checkNotNull(watchedReference, "watchedReference");
  checkNotNull(referenceName, "referenceName");
  final long watchStartNanoTime = System.nanoTime();
  // 對(duì)當(dāng)前監(jiān)視對(duì)象設(shè)置一個(gè)唯一 id
  String key = UUID.randomUUID().toString();
  // 添加到 Set<String> 中
  retainedKeys.add(key);
  // 構(gòu)造一個(gè)帶有id 的 WeakReference 對(duì)象
  final KeyedWeakReference reference =
      new KeyedWeakReference(watchedReference, key, referenceName, queue);
  // 檢測(cè)對(duì)象是否被回收了
  ensureGoneAsync(watchStartNanoTime, reference);
}

4.4.4 ensureGoneAsync 方法

ensureGoneAsync 方法構(gòu)造了一個(gè) Retryable 對(duì)象雪侥,并將它傳給 watchExecutor 的 execute 方法。

private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
  // watchExecutor 是 AndroidWatchExecutor的一個(gè)實(shí)例
  watchExecutor.execute(new Retryable() {
    @Override public Retryable.Result run() {
      return ensureGone(reference, watchStartNanoTime);
    }
  });
}

watchExecutor 是 AndroidWatchExecutor 的一個(gè)實(shí)例精绎, AndroidWatchExecutor 的 execute 方法的作用就是判斷當(dāng)前線程是否是主線程速缨,如果是主線程,那么直接執(zhí)行 waitForIdle 方法代乃,否則通過 Handler 的 post 方法切換到主線程再執(zhí)行 waitForIdle 方法旬牲。

@Override public void execute(@NonNull Retryable retryable) {
  // 判斷當(dāng)前線程是否是主線程
  if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
    waitForIdle(retryable, 0);
  } else {
    postWaitForIdle(retryable, 0);
  }
}

waitForIdle 方法通過調(diào)用 addIdleHandler 方法,指定當(dāng)主進(jìn)程中沒有需要處理的事件時(shí)搁吓,在這個(gè)空閑期間執(zhí)行 postToBackgroundWithDelay 方法原茅。

private void waitForIdle(final Retryable retryable, final int failedAttempts) {
  // 由于上面的 execute 方法,已經(jīng)保證了此方法在主線程中執(zhí)行堕仔,所以Looper.myQueue()獲取的主線程的消息隊(duì)列
  Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
    @Override public boolean queueIdle() {
      postToBackgroundWithDelay(retryable, failedAttempts);
      // return false 表示執(zhí)行完之后员咽,就立即移除這個(gè)事件
      return false;
    }
  });
}

postToBackgroundWithDelay 方法首先會(huì)計(jì)算延遲時(shí)間 delayMillis,這個(gè)延時(shí)是有 exponentialBackoffFactor(指數(shù)因子) 乘以初始延時(shí)時(shí)間得到的贮预, exponentialBackoffFactor(指數(shù)因子)會(huì)在2^n 和 Long.MAX_VALUE / initialDelayMillis 中取較小值贝室,也就說延遲時(shí)間delayMillis = initialDelayMillis * 2^n,且不能超過 Long.MAX_VALUE仿吞。

private void postToBackgroundWithDelay(final Retryable retryable, final int failedAttempts) {
   long exponentialBackoffFactor = (long) Math.min(Math.pow(2, failedAttempts), maxBackoffFactor);
   // 計(jì)算延遲時(shí)間
   long delayMillis = initialDelayMillis * exponentialBackoffFactor;
   // 切換到子線程中執(zhí)行
   backgroundHandler.postDelayed(new Runnable() {
     @Override public void run() {
       // 執(zhí)行 retryable 里的 run 方法
       Retryable.Result result = retryable.run();
       // 如果需要重試滑频,那么再添加到主線程的空閑期間執(zhí)行
       if (result == RETRY) {
         postWaitForIdle(retryable, failedAttempts + 1);
       }
     }
   }, delayMillis);
}

postToBackgroundWithDelay 方法每次執(zhí)行會(huì)指數(shù)級(jí)增加延時(shí)時(shí)間,延時(shí)時(shí)間到了后唤冈,會(huì)執(zhí)行 Retryable 里的方法峡迷,如果返回為重試,那么會(huì)增加延時(shí)時(shí)間并執(zhí)行下一次。

retryable.run() 的run 方法又執(zhí)行了什么呢绘搞?別忘了我們ensureGoneAsync中的代碼彤避,一直在重試的代碼正式 ensureGone 方法。

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

4.4.5 ensureGone 方法

我現(xiàn)在講ensureGone方法的完整代碼貼出來夯辖,我們逐行分析:

Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
  long gcStartNanoTime = System.nanoTime();
  // 前面不是有一個(gè)重試的機(jī)制么琉预,這里會(huì)計(jì)下這次重試距離第一次執(zhí)行花了多長(zhǎng)時(shí)間
  long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
  // 移除所有弱引用可達(dá)對(duì)象,后面細(xì)講
  removeWeaklyReachableReferences();
  // 判斷當(dāng)前是否正在開啟USB調(diào)試蒿褂,LeakCanary 的解釋是調(diào)試時(shí)可能會(huì)觸發(fā)不正確的內(nèi)存泄漏
  if (debuggerControl.isDebuggerAttached()) {
    // The debugger can create false leaks.
    return RETRY;
  }
  // 上面執(zhí)行 removeWeaklyReachableReferences 方法圆米,判斷是不是監(jiān)視對(duì)象已經(jīng)被回收了,如果被回收了啄栓,那么說明沒有發(fā)生內(nèi)存泄漏娄帖,直接結(jié)束
  if (gone(reference)) {
    return DONE;
  }
  // 手動(dòng)觸發(fā)一次 GC 垃圾回收
  gcTrigger.runGc();
  // 再次移除所有弱引用可達(dá)對(duì)象
  removeWeaklyReachableReferences();
  // 如果對(duì)象沒有被回收
  if (!gone(reference)) {
    long startDumpHeap = System.nanoTime();
    long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
    // 使用 Debug 類 dump 當(dāng)前堆內(nèi)存中對(duì)象使用情況
    File heapDumpFile = heapDumper.dumpHeap();
    // dumpHeap 失敗的話,會(huì)走重試機(jī)制
    if (heapDumpFile == RETRY_LATER) {
      // Could not dump the heap.
      return RETRY;
    }
    long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
    // 將hprof文件昙楚、key等屬性構(gòu)造一個(gè) HeapDump 對(duì)象
    HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key)
        .referenceName(reference.name)
        .watchDurationMs(watchDurationMs)
        .gcDurationMs(gcDurationMs)
        .heapDumpDurationMs(heapDumpDurationMs)
        .build();
    // heapdumpListener 分析 heapDump 對(duì)象
    heapdumpListener.analyze(heapDump);
  }
  return DONE;
}

看完上述代碼近速,基本把檢測(cè)泄漏的大致過程走了一遍,下面我們來看一些具體的細(xì)節(jié)堪旧。

(1). removeWeaklyReachableReferences 方法

removeWeaklyReachableReferences 移除所有弱引用可達(dá)對(duì)象是怎么工作的削葱?

private void removeWeaklyReachableReferences() {
  KeyedWeakReference ref;
  while ((ref = (KeyedWeakReference) queue.poll()) != null) {
    retainedKeys.remove(ref.key);
  }
}

還記得我們?cè)?refWatcher.watch 方法保存了當(dāng)前監(jiān)視對(duì)象的 ref.key 了么,如果這個(gè)對(duì)象被回收了崎场,那么對(duì)應(yīng)的弱引用對(duì)象會(huì)在回收時(shí)被添加到queue中佩耳,通過 poll 操作就可以取出這個(gè)弱引用,這時(shí)候我們從retainedKeys中移除這個(gè) key谭跨, 代表這個(gè)對(duì)象已經(jīng)被正掣珊瘢回收,不需要再被監(jiān)視了螃宙。

那么現(xiàn)在來看蛮瞄,判斷這個(gè)對(duì)象是否被回收就比較簡(jiǎn)單了?

private boolean gone(KeyedWeakReference reference) {
  // retainedKeys 中不包含 reference.key 的話谆扎,就代表這個(gè)對(duì)象已經(jīng)被回收了
  return !retainedKeys.contains(reference.key);
}

(2). dumpHeap 方法

heapDumper.dumpHeap() 是執(zhí)行生成hprof的方法挂捅,heapDumper 是 AndroidHeapDumper 的一個(gè)對(duì)象,我們來具體看看它的 dump 方法堂湖。

public File dumpHeap() {
  // 生成一個(gè)存儲(chǔ) hprof 的文件
  File heapDumpFile = leakDirectoryProvider.newHeapDumpFile();
  // 文件創(chuàng)建失敗
  if (heapDumpFile == RETRY_LATER) {
    return RETRY_LATER;
  }
  // FutureResult 內(nèi)部有一個(gè) CountDownLatch闲先,用于倒計(jì)時(shí)
  FutureResult<Toast> waitingForToast = new FutureResult<>();
  // 切換到主線程顯示 toast
  showToast(waitingForToast);
  // 等待5秒,確保 toast 已完成顯示
  if (!waitingForToast.wait(5, SECONDS)) {
    CanaryLog.d("Did not dump heap, too much time waiting for Toast.");
    return RETRY_LATER;
  }
  // 創(chuàng)建一個(gè)通知
  Notification.Builder builder = new Notification.Builder(context)
      .setContentTitle(context.getString(R.string.leak_canary_notification_dumping));
  Notification notification = LeakCanaryInternals.buildNotification(context, builder);
  NotificationManager notificationManager =
      (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
  int notificationId = (int) SystemClock.uptimeMillis();
  notificationManager.notify(notificationId, notification);

  Toast toast = waitingForToast.get();
  try {
    // 開始 dump 內(nèi)存到指定文件
    Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
    cancelToast(toast);
    notificationManager.cancel(notificationId);
    return heapDumpFile;
  } catch (Exception e) {
    CanaryLog.d(e, "Could not dump heap");
    // Abort heap dump
    return RETRY_LATER;
  }
}

這段代碼里我們需要看看 showToast() 方法无蜂,以及它是如何確保 toast 已完成顯示(有點(diǎn)黑科技的感覺)伺糠。

private void showToast(final FutureResult<Toast> waitingForToast) {
  mainHandler.post(new Runnable() {
    @Override public void run() {
      // 當(dāng)前 Activity 已經(jīng) paused的話,直接返回
      if (resumedActivity == null) {
        waitingForToast.set(null);
        return;
      }
      // 構(gòu)建一個(gè)toast 對(duì)象
      final Toast toast = new Toast(resumedActivity);
      toast.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
      toast.setDuration(Toast.LENGTH_LONG);
      LayoutInflater inflater = LayoutInflater.from(resumedActivity);
      toast.setView(inflater.inflate(R.layout.leak_canary_heap_dump_toast, null));
      // 將toast加入顯示隊(duì)列
      toast.show();
      // Waiting for Idle to make sure Toast gets rendered.
      // 主線程中添加空閑時(shí)操作斥季,如果主線程是空閑的训桶,會(huì)將CountDownLatch執(zhí)行 countDown 操作
      Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
        @Override public boolean queueIdle() {
          waitingForToast.set(toast);
          return false;
        }
      });
    }
  });
}

首先我們需要知道所有的Toast對(duì)象累驮,并不是我們一調(diào)用 show 方法就立即顯示的。NotificationServiceManager會(huì)從mToastQueue中輪詢?nèi)コ齌oast對(duì)象進(jìn)行顯示舵揭。如果Toast的顯示不是實(shí)時(shí)的谤专,那么我們?nèi)绾沃繲oast是否已經(jīng)顯示完成了呢?我們?cè)?Toast 調(diào)用 show 方法后調(diào)用 addIdleHandler午绳, 在主進(jìn)程空閑時(shí)執(zhí)行 CountDownLatch 的減一操作置侍。由于我們知道我們順序加入到主線程的消息隊(duì)列中的操作:先顯示Toast,再執(zhí)行 CountDownLatch 減一操作箱叁。所以在 if (!waitingForToast.wait(5, SECONDS)) 的判斷中墅垮,我們最多等待5秒惕医,如果超時(shí)會(huì)走重試機(jī)制耕漱,如果我們的 CountDownLatch 已經(jīng)執(zhí)行了減一操作,則會(huì)正常走后續(xù)流程抬伺,同時(shí)我們也能推理出它前面 toast 肯定已經(jīng)顯示完成了螟够。

Debug.dumpHprofData(heapDumpFile.getAbsolutePath());是系統(tǒng)Debug類提供的方法,我就不做具體分析了峡钓。

(3). heapdumpListener.analyze(heapDump) 方法

heapdumpListener 是 ServiceHeapDumpListener 的一個(gè)對(duì)象妓笙,最終執(zhí)行了HeapAnalyzerService.runAnalysis方法。

@Override public void analyze(@NonNull HeapDump heapDump) {
  checkNotNull(heapDump, "heapDump");
  HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass);
}
// 啟動(dòng)前臺(tái)服務(wù)
public static void runAnalysis(Context context, HeapDump heapDump,
      Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
  setEnabledBlocking(context, HeapAnalyzerService.class, true);
  setEnabledBlocking(context, listenerServiceClass, true);
  Intent intent = new Intent(context, HeapAnalyzerService.class);
  intent.putExtra(LISTENER_CLASS_EXTRA, listenerServiceClass.getName());
  intent.putExtra(HEAPDUMP_EXTRA, heapDump);
  ContextCompat.startForegroundService(context, intent);
}

HeapAnalyzerService 繼承自 IntentService能岩,IntentService的具體原理我就不多做解釋了寞宫。IntentService會(huì)將所有并發(fā)的啟動(dòng)服務(wù)操作,變成順序執(zhí)行 onHandleIntent 方法拉鹃。

@Override protected void onHandleIntentInForeground(@Nullable Intent intent) {
  if (intent == null) {
    CanaryLog.d("HeapAnalyzerService received a null intent, ignoring.");
    return;
  }
  // 監(jiān)聽 hprof 文件分析結(jié)果的類
  String listenerClassName = intent.getStringExtra(LISTENER_CLASS_EXTRA);
  // hprof 文件類
  HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAPDUMP_EXTRA);

  HeapAnalyzer heapAnalyzer =
      new HeapAnalyzer(heapDump.excludedRefs, this, heapDump.reachabilityInspectorClasses);
  // checkForLeak 會(huì)調(diào)用 haha 組件中的工具辈赋,分析 hprof 文件
  AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey,
      heapDump.computeRetainedHeapSize);
  // 將分析結(jié)果發(fā)送給監(jiān)聽器 listenerClassName
  AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
}

我們來看下 checkForLeak 方法,我們一起來看下吧膏燕。

public @NonNull AnalysisResult checkForLeak(@NonNull File heapDumpFile,
      @NonNull String referenceKey,
      boolean computeRetainedSize) {
  long analysisStartNanoTime = System.nanoTime();
  // 文件不存在的話钥屈,直接返回
  if (!heapDumpFile.exists()) {
    Exception exception = new IllegalArgumentException("File does not exist: " + heapDumpFile);
    return failure(exception, since(analysisStartNanoTime));
  }

  try {
    // 更新進(jìn)度回調(diào)
    listener.onProgressUpdate(READING_HEAP_DUMP_FILE);
    // 將 hprof 文件解析成 Snapshot
    HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
    HprofParser parser = new HprofParser(buffer);
    listener.onProgressUpdate(PARSING_HEAP_DUMP);
    Snapshot snapshot = parser.parse();
    listener.onProgressUpdate(DEDUPLICATING_GC_ROOTS);
    // 移除相同 GC root項(xiàng)
    deduplicateGcRoots(snapshot);
    listener.onProgressUpdate(FINDING_LEAKING_REF);
    // 查找內(nèi)存泄漏項(xiàng)
    Instance leakingRef = findLeakingReference(referenceKey, snapshot);

    // False alarm, weak reference was cleared in between key check and heap dump.
    // 沒有找到,就代表沒有泄漏
    if (leakingRef == null) {
      return noLeak(since(analysisStartNanoTime));
    }
    // 找到泄漏處的引用關(guān)系鏈
    return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, computeRetainedSize);
  } catch (Throwable e) {
    return failure(e, since(analysisStartNanoTime));
  }
}

hprof 文件的解析是由開源項(xiàng)目 haha 完成的坝辫,我這里不做過多展開篷就。

findLeakingReference 方法是查找泄漏的引用處,我們看下代碼:

private Instance findLeakingReference(String key, Snapshot snapshot) {
  // 從 hprof 文件保存的對(duì)象中找到所有 KeyedWeakReference 的實(shí)例
  ClassObj refClass = snapshot.findClass(KeyedWeakReference.class.getName());
  if (refClass == null) {
    throw new IllegalStateException(
        "Could not find the " + KeyedWeakReference.class.getName() + " class in the heap dump.");
  }
  List<String> keysFound = new ArrayList<>();
  // 對(duì) KeyedWeakReference 實(shí)例列表進(jìn)行遍歷
  for (Instance instance : refClass.getInstancesList()) {
    // 獲取每個(gè)實(shí)例里的所有字段
    List<ClassInstance.FieldValue> values = classInstanceValues(instance);
    // 找到 key 字段對(duì)應(yīng)的值
    Object keyFieldValue = fieldValue(values, "key");
    if (keyFieldValue == null) {
      keysFound.add(null);
      continue;
    }
    // 將 keyFieldValue 轉(zhuǎn)為 String 對(duì)象
    String keyCandidate = asString(keyFieldValue);
    // 如果這個(gè)對(duì)象的 key 和 我們查找的 key 相同近忙,那么返回這個(gè)弱對(duì)象持有的原對(duì)象
    if (keyCandidate.equals(key)) {
      return fieldValue(values, "referent");
    }
    keysFound.add(keyCandidate);
  }
  throw new IllegalStateException(
      "Could not find weak reference with key " + key + " in " + keysFound);
}

到現(xiàn)在為止竭业,我們已經(jīng)把 LeakCanary 檢測(cè)內(nèi)存泄漏的全部過程的源碼看完了。個(gè)人認(rèn)為 LeakCanary 源碼寫的不錯(cuò)及舍,可讀性很高未辆,查找調(diào)用關(guān)系也比較方便(這里黑一下 bilibili 的 DanmakusFlameMaster)。

五. 版權(quán)聲明

本文首發(fā)于我的微信公眾號(hào):Android開發(fā)實(shí)驗(yàn)室击纬,歡迎大家關(guān)注和我一起學(xué)Android鼎姐,掉節(jié)操。未經(jīng)允許,不得轉(zhuǎn)載炕桨。


http://orzangleli.com/qrcode_for_gh_d251874cf21a_258.jpg
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末饭尝,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子献宫,更是在濱河造成了極大的恐慌钥平,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件姊途,死亡現(xiàn)場(chǎng)離奇詭異涉瘾,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)捷兰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門立叛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人贡茅,你說我怎么就攤上這事秘蛇。” “怎么了顶考?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵赁还,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我驹沿,道長(zhǎng)艘策,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任渊季,我火速辦了婚禮朋蔫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘梭域。我一直安慰自己斑举,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布病涨。 她就那樣靜靜地躺著富玷,像睡著了一般。 火紅的嫁衣襯著肌膚如雪既穆。 梳的紋絲不亂的頭發(fā)上赎懦,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音幻工,去河邊找鬼励两。 笑死,一個(gè)胖子當(dāng)著我的面吹牛囊颅,可吹牛的內(nèi)容都是我干的当悔。 我是一名探鬼主播傅瞻,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼盲憎!你這毒婦竟也來了嗅骄?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤饼疙,失蹤者是張志新(化名)和其女友劉穎溺森,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體窑眯,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡屏积,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了磅甩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片炊林。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖更胖,靈堂內(nèi)的尸體忽然破棺而出铛铁,到底是詐尸還是另有隱情隔显,我是刑警寧澤却妨,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站括眠,受9級(jí)特大地震影響彪标,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜掷豺,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一捞烟、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧当船,春花似錦题画、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至壹置,卻和暖如春竞思,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背钞护。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工盖喷, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人难咕。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓课梳,卻偏偏與公主長(zhǎng)得像距辆,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子暮刃,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

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

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,520評(píng)論 25 707
  • LeakCanary原理淺析 1.LeakCanary簡(jiǎn)介 LeakCanary是一個(gè)Android和Java的內(nèi)...
    chewbee閱讀 6,785評(píng)論 2 38
  • 用兩張圖告訴你挑格,為什么你的 App 會(huì)卡頓? - Android - 掘金 Cover 有什么料? 從這篇文章中你...
    hw1212閱讀 12,693評(píng)論 2 59
  • 我所理解的孤單 可能就是 在所有的社交網(wǎng)絡(luò)平臺(tái)上發(fā)表不一樣的心情挫望,除了朋友圈和qq空間,因?yàn)槟抢镉信f友狂窑。 對(duì)著屏幕...
    洛文閱讀 216評(píng)論 0 0