LeakCanary實現(xiàn)原理淺析

LeakCanary是一個在安卓平臺上檢測內存泄漏的工具庫换淆。

粗略的看了以下LeakCanary的實現(xiàn)原理。

LeakCanary地址

工程目錄

工程目錄
  • leakcanary-analyzer

    負責分析內存泄漏,主要使用了com.squareup.haha:haha庫來分析

  • leakcanary-android

    負責android的接入

  • leakcanary-android-no-op

    空實現(xiàn)忘晤,就2個類,release后引用的空包

  • leakcanary-sample

    如何使用LeakCanary的示例

  • leackcanary-watcher
    負責監(jiān)視對象是否泄漏

工作流程

  1. 安裝LeakCanary
    安裝LeakCanary過程中注冊監(jiān)聽Activity的生命周期。
  2. 監(jiān)聽Activity生命周期寄摆,當Activity發(fā)生destroyed的時候,弱引用Activity為KeyedWeakReference修赞。
  3. 當主線程空閑的時候執(zhí)行GC操作婶恼,判斷弱引用是否釋放桑阶。
  4. 弱引用沒有釋放,則找到內存泄漏勾邦,進行內存泄漏分析蚣录,之后通知和展示。

源碼解析

看源碼的時候眷篇,從初始化入手萎河,然后找到核心鏈路。

  1. 安裝過程

初始化的安裝流程最終調用的是

   /**
   *
   * @param application
   * @param listenerServiceClass 默認傳遞 DisplayLeakService.class
   * @param excludedRefs 排除的情況 默認為AndroidExcludedRefs.createAppDefaults().build() 
   * @return
   */
  public static RefWatcher install(Application application,
      Class<? extends AbstractAnalysisResultService> listenerServiceClass,
      ExcludedRefs excludedRefs) {
    //是否在分析的進程(HeapAnalyzerService進程)
    if (isInAnalyzerProcess(application)) {
      return RefWatcher.DISABLED;
    }
    //在桌面顯示內存泄漏Activity(DisplayLeakActivity)的圖標
    enableDisplayLeakActivity(application);
    //啟用分析的回調 結果會啟用HeapAnalyzerService進行HeapDump分析來找出泄漏的源頭
    HeapDump.Listener heapDumpListener =
        new ServiceHeapDumpListener(application, listenerServiceClass);
    //監(jiān)視器 leakcanary核心部分 后面會分析
    RefWatcher refWatcher = androidWatcher(application, heapDumpListener, excludedRefs);
    //把Activity列為監(jiān)視器的監(jiān)視對象 通過監(jiān)聽Activity發(fā)生destroyed
    ActivityRefWatcher.installOnIcsPlus(application, refWatcher);
    return refWatcher;
  }

install主要做了3件事情

1. 在桌面啟用DisplayLeakActivity的圖標
2. 初始化監(jiān)聽器RefWatcher蕉饼,并監(jiān)聽Activity
3. 在監(jiān)聽到有內存泄漏后調用heapDumpListener來啟用HeapAnalyzerService
  1. RefWatcher
    RefWatcher是leackcanary的核心虐杯,他負責監(jiān)聽內存泄漏是否發(fā)生。

RefWatcher的成員變量

  //監(jiān)聽執(zhí)行器 實現(xiàn)類 AndroidWatchExecutor 核心代碼 Looper.myQueue().addIdleHandler(IdleHandler)
  private final Executor watchExecutor;
  //負責日志輸出 實現(xiàn)類 AndroidDebuggerControl 通過Debug.isDebuggerConnected()來判斷是否輸出日志
  private final DebuggerControl debuggerControl;
  //GC觸發(fā)器 抄AOSP代碼 https://android.googlesource.com/platform/libcore/+/master/support/src/test/java/libcore/java/lang/ref/FinalizationTester.java
  private final GcTrigger gcTrigger;
  //進行headDump操作 實現(xiàn)類 AndroidHeapDumper 核心代碼 Debug.dumpHprofData(heapDumpFile.getAbsolutePath()); 另外還做了一個5s超時處理 超時實現(xiàn)方法可以參考下^_^
  private final HeapDumper heapDumper;
  //保存在監(jiān)聽的對象 如果GC后還存在里面 說明內存泄漏了
  private final Set<String> retainedKeys;
  //內存被成功回收會進入該隊列 然后會更新retainedKeys
  private final ReferenceQueue<Object> queue;
  //在install的時候傳入的ServiceHeapDumpListener 負責dump后的回調
  private final HeapDump.Listener heapdumpListener;
  //排除項
  private final ExcludedRefs excludedRefs;

此處需要一個圖來解釋RefWatcher工作流程

  1. 泄漏分析

找到泄漏點后開始啟用HeapAnalyzerService進行泄漏分析昧港。

//獲取泄漏分析結果 核心代碼 ShortestPathFinder.findPath(Snapshot snapshot, Instance leakingRef) 
AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey);
//交給DisplayLeakService進行展示處理
AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);

找到內存泄漏的路徑的核心代碼
大概思路是從GCRoot出發(fā)擎椰,廣度優(yōu)先搜索到leakingRef就返回,其中利用excludedRefs進行剪枝慨飘。

Result findPath(Snapshot snapshot, Instance leakingRef) {
    clearState();
    canIgnoreStrings = !isString(leakingRef);
    //搜索隊列里增加GCRoot
    enqueueGcRoots(snapshot);

    boolean excludingKnownLeaks = false;
    LeakNode leakingNode = null;
    //優(yōu)先找toVisitQueue隊列中的 找完再找toVisitIfNoPathQueue确憨,而路徑中包含toVisitIfNoPathQueue里的元素則標示excludingKnownLeaks為true
    while (!toVisitQueue.isEmpty() || !toVisitIfNoPathQueue.isEmpty()) {
      LeakNode node;
      if (!toVisitQueue.isEmpty()) {
        node = toVisitQueue.poll();
      } else {
        node = toVisitIfNoPathQueue.poll();
        if (node.exclusion == null) {
          throw new IllegalStateException("Expected node to have an exclusion " + node);
        }
        excludingKnownLeaks = true;
      }

      // 找到泄漏點 跳出循環(huán)
      if (node.instance == leakingRef) {
        leakingNode = node;
        break;
      }
      //判斷是否搜索過了 看了代碼 按我的理解 這里沒必要搞toVisitSet,toVisitIfNoPathSet瓤的,visitedSet 保留visitedSet就夠了
      if (checkSeen(node)) {
        continue;
      }
      
      if (node.instance instanceof RootObj) {
        visitRootObj(node);
      } else if (node.instance instanceof ClassObj) {
        visitClassObj(node);
      } else if (node.instance instanceof ClassInstance) {
        visitClassInstance(node);
      } else if (node.instance instanceof ArrayInstance) {
        visitArrayInstance(node);
      } else {
        throw new IllegalStateException("Unexpected type for " + node.instance);
      }
    }
    return new Result(leakingNode, excludingKnownLeaks);
  }
  1. 內存泄漏通知和展示

在拿到泄漏路徑后休弃,交給DisplayLeakService進行處理。代碼很簡單就發(fā)了個通知圈膏。

 @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;
    }
    showNotification(this, contentTitle, contentText, pendingIntent);
    afterDefaultHandling(heapDump, result, leakInfo);
  }

DisplayLeakActivity就不分析了塔猾,主要負責內存泄漏的展示。

總結

本文只是粗略的梳理LeakCanary流程稽坤,其中還有許多細節(jié)沒有提及丈甸。

本文分析的是master分支上的代碼,只支持監(jiān)聽Activity泄漏尿褪,不過了解了整個流程后睦擂,我們可以加入更多的監(jiān)聽對象,如WebView Fragment等杖玲。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末顿仇,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子摆马,更是在濱河造成了極大的恐慌臼闻,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件囤采,死亡現(xiàn)場離奇詭異述呐,居然都是意外死亡,警方通過查閱死者的電腦和手機蕉毯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門乓搬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來思犁,“玉大人,你說我怎么就攤上這事缤谎∈阋校” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵坷澡,是天一觀的道長托呕。 經常有香客問我,道長频敛,這世上最難降的妖魔是什么项郊? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮斟赚,結果婚禮上着降,老公的妹妹穿的比我還像新娘。我一直安慰自己拗军,他們只是感情好任洞,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著发侵,像睡著了一般交掏。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上刃鳄,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天盅弛,我揣著相機與錄音,去河邊找鬼叔锐。 笑死挪鹏,一個胖子當著我的面吹牛,可吹牛的內容都是我干的愉烙。 我是一名探鬼主播讨盒,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼步责!你這毒婦竟也來了返顺?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤勺择,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后伦忠,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體省核,經...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年昆码,在試婚紗的時候發(fā)現(xiàn)自己被綠了气忠。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片邻储。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖旧噪,靈堂內的尸體忽然破棺而出吨娜,到底是詐尸還是另有隱情,我是刑警寧澤淘钟,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布宦赠,位于F島的核電站,受9級特大地震影響米母,放射性物質發(fā)生泄漏勾扭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一铁瞒、第九天 我趴在偏房一處隱蔽的房頂上張望妙色。 院中可真熱鬧,春花似錦慧耍、人聲如沸身辨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽煌珊。三九已至,卻和暖如春师枣,著一層夾襖步出監(jiān)牢的瞬間怪瓶,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工践美, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留洗贰,地道東北人。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓陨倡,卻偏偏與公主長得像敛滋,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子兴革,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345