深入分析LeakCanary原理

1.LeakCanary簡介

LeakCanary是Square公司開源的內存泄露檢測工具。
github:LeakCanary

2.源碼分析

LeakCanary源碼分析我們從install方法開始帽芽,大家應該知道引入LeakCanary的時候install方法時在Application#onCreate的方法中執(zhí)行的删掀。

我們首先看isInAnalyzerProcess方法,看注釋如果是LeakCanary進程就不允許初始化其他任務导街。這個進程是為LeakCanary分析堆內存用的披泪。

if (LeakCanary.isInAnalyzerProcess(this)) {
            // This process is dedicated to LeakCanary for heap analysis.
            // You should not init your app in this process.
            return;
        }
LeakCanary.install(this);

我們知道引入LeakCanary在你的項目編譯成功之后會自動安裝一個Leaks apk“峁澹看到上面LeakCanary.install(this)這行代碼款票,是不是安裝Leaks apk?是不是Leaks通過跨進程來檢測我項目呢泽论?檢測原理又是怎樣的艾少?帶著這些問題,接下來看看具體做了什么事情翼悴。會調用LeakCanary#install方法

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

首先我們看看buildAndInstall方法具體做了什么事情

public RefWatcher buildAndInstall() {
    RefWatcher refWatcher = build();
    if (refWatcher != DISABLED) {
      LeakCanary.enableDisplayLeakActivity(context);
      ActivityRefWatcher.installOnIcsPlus((Application) context, refWatcher);
    }
    return refWatcher;
  }

看ActivityRefWatcher類名應該是Activity 引用監(jiān)控缚够,我們跟進這個installOnIcsPlus方法看看

public static void installOnIcsPlus(Application application, RefWatcher refWatcher) {
    if (SDK_INT < ICE_CREAM_SANDWICH) {
      // If you need to support Android < ICS, override onDestroy() in your base activity.
      return;
    }
    ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
    activityRefWatcher.watchActivities();
  }

然后installOnIcsPlus方法又調用了ActivityRefWatcher#watchActivities方法,我們再跟進分析

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

        @Override public void onActivityStarted(Activity activity) {
        }

        @Override public void onActivityResumed(Activity activity) {
        }

        @Override public void onActivityPaused(Activity activity) {
        }

        @Override public void onActivityStopped(Activity activity) {
        }

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

        @Override public void onActivityDestroyed(Activity activity) {
          ActivityRefWatcher.this.onActivityDestroyed(activity);
        }
      };
public void watchActivities() {
    // Make sure you don't get installed twice.確保不要注冊兩次
    stopWatchingActivities();
    application.registerActivityLifecycleCallbacks(lifecycleCallbacks);
  }

我們首先看看Application#registerActivityLifecycleCallbacks方法鹦赎,我們知道原理registerActivityLifecycleCallbacks是注冊Activity生命周期回調的方法谍椅。管理生命周期的方法,每個Activity都會回調到這里古话。我們看lifecycleCallbacks這個變量對應著Activity的生命周期雏吭。
到這里所以我們是不是可以猜想到LeakCanary其實就是通過注冊Activity生命周期回調來監(jiān)控檢查是否有內存泄露的。當Activity界面關閉回調用對應的onActivityDestroyed方法煞额。
接下來我們看看Application.ActivityLifecycleCallbacks#onActivityDestroyed方法干了些什么事情思恐。

ActivityRefWatcher.this.onActivityDestroyed(activity);
void onActivityDestroyed(Activity activity) {
    refWatcher.watch(activity);
  }

接下來會調用到com.squareup.leakcanary.RefWatcher#watch(java.lang.Object)方法沾谜。

public void watch(Object watchedReference) {
    watch(watchedReference, "");
  }

  /**
   * Watches the provided references and checks if it can be GCed. This method is non blocking,
   * the check is done on the {@link WatchExecutor} this {@link RefWatcher} has been constructed
   * with.
   *
   * @param referenceName An logical identifier for the watched object.
   */
  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引用包裝成一個KeyedWeakReference弱引用。被WeakReference包裝的引用如果被回收會添加到ReferenceQueue隊列中胀莹,WeakReference和ReferenceQueue將在接下來的文章里分析基跑。WeakReference和ReferenceQueue
通過檢查ReferenceQueue里的Activity引用來檢測是否能夠被回收。下面具體看看檢測方法:
ensureGoneAsync(watchStartNanoTime, reference)--->ensureGone(reference, watchStartNanoTime)

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;
    }
    if (gone(reference)) {
      return DONE;
    }
    gcTrigger.runGc();
    removeWeaklyReachableReferences();
    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);
      heapdumpListener.analyze(
          new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
              gcDurationMs, heapDumpDurationMs));
    }
    return DONE;
  }

通過ensureGone這個方法名可以看出應該是確保引用釋放的意思描焰。具體看看ensureGone方法做了些什么:
我們主要看removeWeaklyReachableReferences(),gcTrigger.runGc(),heapdumpListener.analyze()這三個方法媳否。
1.通過removeWeaklyReachableReferences移除所有ReferenceQueue隊列的WeakReference對象的Activity引用。如果可以回收就直接返回荆秦。

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

2.Activity引用如果通過removeWeaklyReachableReferences還是沒有移除就通過gcTrigger.runGc()來GC觸發(fā)回收篱竭。
3.接下來再觸發(fā)步驟1的removeWeaklyReachableReferences方法來移除Activity引用,判斷是否回收步绸,如果可以回收就直接返回,反之就說明對象泄露了就會觸發(fā)heapdumpListener.analyze()方法來分析掺逼,看到HeapDump對象,我們可以通過heapDumper.dumpHeap()方法看到dump當前的heap hprof快照文件瓤介。
具體通過Debug.dumpHprofData(heapDumpFile.getAbsolutePath())方法去dump內存文件的吕喘。實現(xiàn)如下:

@Override public File dumpHeap() {
    File heapDumpFile = leakDirectoryProvider.newHeapDumpFile();

    if (heapDumpFile == RETRY_LATER) {
      return RETRY_LATER;
    }

    FutureResult<Toast> waitingForToast = new FutureResult<>();
    showToast(waitingForToast);

    if (!waitingForToast.wait(5, SECONDS)) {
      CanaryLog.d("Did not dump heap, too much time waiting for Toast.");
      return RETRY_LATER;
    }

    Toast toast = waitingForToast.get();
    try {
      Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
      cancelToast(toast);
      return heapDumpFile;
    } catch (Exception e) {
      CanaryLog.d(e, "Could not dump heap");
      // Abort heap dump
      return RETRY_LATER;
    }
  }

接下來會調用com.squareup.leakcanary.ServiceHeapDumpListener#analyze方法將內存文件目錄等相關信息通過HeapAnalyzerService#runAnalysis方法來進行對象內存泄露的分析。實現(xiàn)如下:

@Override public void analyze(HeapDump heapDump) {
    checkNotNull(heapDump, "heapDump");
    HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass);
  }

接著會啟動HeapAnalyzerService IntentService服務刑桑,具體IntentService的分析在我的IntentService源碼分析中有講過IntentService源碼分析
氯质。

public static void runAnalysis(Context context, HeapDump heapDump,
      Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
    Intent intent = new Intent(context, HeapAnalyzerService.class);
    intent.putExtra(LISTENER_CLASS_EXTRA, listenerServiceClass.getName());
    intent.putExtra(HEAPDUMP_EXTRA, heapDump);
    context.startService(intent);
  }

具體在HeapAnalyzerService#onHandleIntent方法中通過HeapAnalyzer#checkForLeak來檢測是否內存泄露的。實現(xiàn)如下:

public AnalysisResult checkForLeak(File heapDumpFile, String referenceKey) {
    long analysisStartNanoTime = System.nanoTime();

    if (!heapDumpFile.exists()) {
      Exception exception = new IllegalArgumentException("File does not exist: " + heapDumpFile);
      return failure(exception, since(analysisStartNanoTime));
    }

    try {
      HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
      HprofParser parser = new HprofParser(buffer);
      Snapshot snapshot = parser.parse();
      deduplicateGcRoots(snapshot);

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

      return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef);
    } catch (Throwable e) {
      return failure(e, since(analysisStartNanoTime));
    }
  }

接下來啟動AbstractAnalysisResultService這個IntentService祠斧,啟動后會調用onHandleIntent方法闻察。

@Override protected final void onHandleIntent(Intent intent) {
    HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAP_DUMP_EXTRA);
    AnalysisResult result = (AnalysisResult) intent.getSerializableExtra(RESULT_EXTRA);
    try {
      onHeapAnalyzed(heapDump, result);
    } finally {
      //noinspection ResultOfMethodCallIgnored
      heapDump.heapDumpFile.delete();
    }
  }

最終通過onHeapAnalyzed這個抽象方法將heapDump,AnalysisResult信息傳入到子類實現(xiàn)琢锋。

@Override protected final void onHeapAnalyzed(HeapDump heapDump, AnalysisResult result) {
    String leakInfo = leakInfo(this, heapDump, result, true);
    CanaryLog.d(leakInfo);

    boolean resultSaved = false;
    boolean shouldSaveResult = result.leakFound || result.failure != null;
    if (shouldSaveResult) {
      heapDump = renameHeapdump(heapDump);
      resultSaved = saveResult(heapDump, result);
    }

    PendingIntent pendingIntent;
    String contentTitle;
    String contentText;

    if (!shouldSaveResult) {
      contentTitle = getString(R.string.leak_canary_no_leak_title);
      contentText = getString(R.string.leak_canary_no_leak_text);
      pendingIntent = null;
    } else if (resultSaved) {
      pendingIntent = DisplayLeakActivity.createPendingIntent(this, heapDump.referenceKey);

      if (result.failure == null) {
        String size = formatShortFileSize(this, result.retainedHeapSize);
        String className = classSimpleName(result.className);
        if (result.excludedLeak) {
          contentTitle = getString(R.string.leak_canary_leak_excluded, className, size);
        } else {
          contentTitle = getString(R.string.leak_canary_class_has_leaked, className, size);
        }
      } else {
        contentTitle = getString(R.string.leak_canary_analysis_failed);
      }
      contentText = getString(R.string.leak_canary_notification_message);
    } else {
      contentTitle = getString(R.string.leak_canary_could_not_save_title);
      contentText = getString(R.string.leak_canary_could_not_save_text);
      pendingIntent = null;
    }
    // New notification id every second.
    int notificationId = (int) (SystemClock.uptimeMillis() / 1000);
    showNotification(this, contentTitle, contentText, pendingIntent, notificationId);
    afterDefaultHandling(heapDump, result, leakInfo);
  }

分析上面邏輯其實就是通過DisplayLeakService做內存泄露相關展示辕漂。

3.總結

到目前為止,我想大家應該知道LeakCanary是怎樣實現(xiàn)內存泄露的檢測吩蔑。
利用Application#registerActivityLifecycleCallbacks Activity生命周期回調onActivityDestroyed方法通過調用RefWatcher#wather方法來檢測對象是否回收钮热,通過removeWeaklyReachableReferences--->gcTrigger.runGc--->removeWeaklyReachableReferences--->heapdumpListener.analyze 三步二次檢測來確定內存泄露,最終dump 內存信息來分析到最終顯示分析出的泄露信息烛芬。

4.LeakCanary原理實踐

分析完LeakCanary原理隧期,其實我們也可以在自己的項目中利用LeakCanary原理在打開每個Actvitiy或者Fragment時定義一個弱引用實例,然后關聯(lián)在一個ReferenceQueue隊列中赘娄,然后通過Application#registerActivityLifecycleCallbacks#onActivityDestroyed回調去關聯(lián)的ReferenceQueue中檢測當前的Actvitiy或者Fragment是否被回收來檢測是否泄漏仆潮,這樣就是一個精簡版的LeakCanary。當發(fā)現(xiàn)內存泄漏遣臼,然后dump hprof內存快照文件到服務器性置,然后分析內存文件,從而可以發(fā)現(xiàn)內存泄漏揍堰。

That's All

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末鹏浅,一起剝皮案震驚了整個濱河市嗅义,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌隐砸,老刑警劉巖之碗,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異季希,居然都是意外死亡褪那,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進店門式塌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來博敬,“玉大人,你說我怎么就攤上這事峰尝∑眩” “怎么了?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵境析,是天一觀的道長囚枪。 經(jīng)常有香客問我,道長劳淆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任默赂,我火速辦了婚禮沛鸵,結果婚禮上,老公的妹妹穿的比我還像新娘缆八。我一直安慰自己曲掰,他們只是感情好,可當我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布奈辰。 她就那樣靜靜地躺著栏妖,像睡著了一般。 火紅的嫁衣襯著肌膚如雪奖恰。 梳的紋絲不亂的頭發(fā)上吊趾,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天,我揣著相機與錄音瑟啃,去河邊找鬼论泛。 笑死,一個胖子當著我的面吹牛蛹屿,可吹牛的內容都是我干的屁奏。 我是一名探鬼主播,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼错负,長吁一口氣:“原來是場噩夢啊……” “哼坟瓢!你這毒婦竟也來了勇边?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤折联,失蹤者是張志新(化名)和其女友劉穎粒褒,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體崭庸,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡怀浆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了怕享。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片执赡。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖函筋,靈堂內的尸體忽然破棺而出沙合,到底是詐尸還是另有隱情,我是刑警寧澤跌帐,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布首懈,位于F島的核電站,受9級特大地震影響谨敛,放射性物質發(fā)生泄漏究履。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一脸狸、第九天 我趴在偏房一處隱蔽的房頂上張望最仑。 院中可真熱鬧,春花似錦炊甲、人聲如沸泥彤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽吟吝。三九已至,卻和暖如春颈娜,著一層夾襖步出監(jiān)牢的瞬間剑逃,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工揭鳞, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留炕贵,地道東北人。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓野崇,卻偏偏與公主長得像称开,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,927評論 2 355