性能優(yōu)化(2.1)-LeakCanary原理分析

主目錄見:Android高級進(jìn)階知識(這是總目錄索引)
?性能優(yōu)化很重要的一個(gè)環(huán)節(jié)就是檢測有沒有內(nèi)存泄漏沦泌,以前我們內(nèi)存泄漏會(huì)借助MAT,androidstudio Monitor(androidstudio 3.0改成Android profiler)等工具孝情,檢測過程會(huì)比較麻煩一點(diǎn)龄广,而LeakCanary作為一個(gè)自動(dòng)內(nèi)存泄漏工具出現(xiàn)帅腌,應(yīng)該說它的簡單易用給我們省了好多工作量绿满,提升了我們的代碼質(zhì)量亿胸。也許大家會(huì)說汹想,java不是有自動(dòng)垃圾回收機(jī)制嗎追葡?但是其實(shí)一些持有外部引用超過他應(yīng)有的生命周期的話腺律,那么這個(gè)時(shí)候垃圾回收機(jī)制是不會(huì)去回收的奕短,這時(shí)候就會(huì)出現(xiàn)不可預(yù)期地內(nèi)存暴走。

一.目標(biāo)

今天的目標(biāo)就是為了能明白LeakCanary大致的原理過程匀钧,然后大家能更放心使用翎碑,具體目標(biāo)如下:
1.明白LeakCanary版本的差異;
2.LeakCanary的內(nèi)存檢測思路之斯。

二.源碼分析

具體的使用我們就不在這邊說了日杈,因?yàn)間ithub上面都有,而且現(xiàn)在4.0版本以上的使用變得簡單很多佑刷,我們來在代碼中的使用:

public class ExampleApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    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);
    // Normal app init code...
  }
}

很簡單莉擒,但是其實(shí)在4.0以上其實(shí)才變得如此易用,這是為什么呢瘫絮?這跟Application.ActivityLifecycleCallbacks這個(gè)方法有關(guān)(這個(gè)接口在Android 4.0才有)涨冀,這個(gè)方法其實(shí)我們在前面的換膚框架實(shí)現(xiàn)解析(二)這篇文章有講解過,這個(gè)方法可以監(jiān)測到Activity的各個(gè)生命周期麦萤,然后在LeakCanary就可以在onActivityDestroyed方法中為所有的Activity調(diào)用refWatcher.watch(activity)鹿鳖。

總體流程

在分析LeakCanary之前,我們先來明確一下總體流程壮莹,檢測主要分為三個(gè)步驟:

  • 1.分析是否有可疑的泄漏對象栓辜,主要是通過弱引用機(jī)制來檢測;
  • 2.如果第一步發(fā)現(xiàn)了可疑泄漏對象垛孔,那么就會(huì)dump內(nèi)存快照,然后分析.hprof文件確定是否真的泄漏了施敢。
  • 3.展示分析的結(jié)果周荐。

1.分析可疑泄漏對象

我們知道,因?yàn)槲覀儜?yīng)用了Application.ActivityLifecycleCallbacks(Activity)方法僵娃,所以我們程序會(huì)在ActivityRefWatcher類中注冊一個(gè)lifecycleCallbacks對象來監(jiān)測Activity的生命周期概作,LeakCanary就是在onActivityDestroyed里面調(diào)用了refWatcher.watch(activity)方法,這里的refWatcher是在前面build()方法中初始化的默怨,我們現(xiàn)在直接看RefWatcher的watch()方法:

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

這里的watchedReference就是我們的每個(gè)activity對象讯榕,我們看到這個(gè)方法又調(diào)用了內(nèi)部兩個(gè)參數(shù)的watch方法:

 public void watch(Object watchedReference, String referenceName) {
    if (this == DISABLED) {
      return;
    }
    checkNotNull(watchedReference, "watchedReference");
    checkNotNull(referenceName, "referenceName");
    final long watchStartNanoTime = System.nanoTime();
//對一個(gè)引用產(chǎn)生一個(gè)唯一的Key
    String key = UUID.randomUUID().toString();
//放到key集合中
    retainedKeys.add(key);
//將要監(jiān)測的對象添加一個(gè)弱引用
    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue);
//在子線程中分析這個(gè)弱引用
    ensureGoneAsync(watchStartNanoTime, reference);
  }

從上面的代碼可以知道,其實(shí)就是給監(jiān)測對象添加一個(gè)弱引用匙睹,然后使用ReferenceQueue來監(jiān)測它的可達(dá)性的改變愚屁,其中key是一個(gè)唯一的uuid,而最后的ensureGoneAsync()是我們主要的分析方法了痕檬,我們來看看:

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

我們看到程序中watchExecutor是個(gè)什么東西呢霎槐?LeakCanary為我們實(shí)現(xiàn)了AndroidWatchExecutor,這里面利用HandlerThread的機(jī)制梦谜,在子線程中來處理分析這個(gè)邏輯(如果這個(gè)地方不懂丘跌,推薦看HandlerThread源碼分析)袭景,我們主要的分析方法是在ensureGone中,我們直接來看:

 Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
//刪除所有已經(jīng)在ReferenceQueue中的弱引用
    removeWeaklyReachableReferences();
//如果當(dāng)前處于調(diào)試狀態(tài)則返回
    if (debuggerControl.isDebuggerAttached()) {
      // The debugger can create false leaks.
      return RETRY;
    }
//如果當(dāng)前的對象只有弱引用了闭树,那么說明不會(huì)泄露
    if (gone(reference)) {
      return DONE;
    }
//如果當(dāng)前的對象還沒有改變?nèi)蹩蛇_(dá)狀態(tài)耸棒,則我們手動(dòng)調(diào)用GC
    gcTrigger.runGc();
//再次刪除,確認(rèn)對象是不是已經(jīng)在ReferenceQueue中
    removeWeaklyReachableReferences();
//如果當(dāng)前對象還沒有在ReferenceQueue报辱,說明可能泄露了与殃,則dump內(nèi)存快照
    if (!gone(reference)) {
      long startDumpHeap = System.nanoTime();
      long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
//我們開始dump內(nèi)存快照
      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;
  }

這里我們看到,我們?yōu)樯蹲詈筮€有dump內(nèi)存快照捏肢,然后進(jìn)行分析.hprof文件呢重斑,其實(shí)我們這里的GC只是建議虛擬機(jī)說進(jìn)行一次內(nèi)存回收,但是最終要不要進(jìn)行內(nèi)存回收是JVM說了算烁焙,如果這里建議沒被通過的時(shí)候宵蕉,那么我們的可達(dá)性就不會(huì)發(fā)生改變,我們就需要第二個(gè)步驟dump內(nèi)存快照來分析辩棒。

2.dump內(nèi)存快照

我們看到程序的最后調(diào)用了heapdumpListeneranalyze方法狼忱,那么這里的heapdumpListener是什么呢?這里要從LeakCanary類中的install()方法看起:

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

我們這里有個(gè)方法listenerServiceClass一睁,這個(gè)方法我們跟進(jìn)去看下:

  public AndroidRefWatcherBuilder listenerServiceClass(
      Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
    return heapDumpListener(new ServiceHeapDumpListener(context, listenerServiceClass));
  }

從這里我們可以看到我們的heapdumpListener其實(shí)就是我們的ServiceHeapDumpListener類對象钻弄,所以我們看到這個(gè)類的analyze方法:

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

HeapAnalyzerService是個(gè)IntentService的子類(同樣的,不懂IntentService的話推薦IntentService源碼分析)者吁,所以我們的主要方法是在onHandIntent方法中分析的:

 @Override protected void onHandleIntent(Intent intent) {
    if (intent == null) {
      CanaryLog.d("HeapAnalyzerService received a null intent, ignoring.");
      return;
    }
    String listenerClassName = intent.getStringExtra(LISTENER_CLASS_EXTRA);
    HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAPDUMP_EXTRA);

    HeapAnalyzer heapAnalyzer = new HeapAnalyzer(heapDump.excludedRefs);

    AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey);
    AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
  }

我們看到程序new了一個(gè)HeapAnalyzer對象窘俺,這個(gè)類主要負(fù)責(zé)分析hprof文件的。然后程序會(huì)調(diào)用HeapAnalyzercheckForLeak方法:

  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 {
//加載hprof文件
      HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
      HprofParser parser = new HprofParser(buffer);
//解析
      Snapshot snapshot = parser.parse();
//精簡gcroots,把重復(fù)的路徑刪除复凳,重新封裝成不重復(fù)的路徑的容器
      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));
      }
 //查找從這個(gè)對象的引用到GC ROOT的最短路徑
      return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef);
    } catch (Throwable e) {
      return failure(e, since(analysisStartNanoTime));
    }
  }

上面的代碼邏輯應(yīng)該算是比較簡單瘤泪,具體細(xì)節(jié)大家也不需要硬摳,我們知道育八,我們上面的代碼主要就是為了尋找到hprof文件中泄漏對象的引用路徑(泄漏對象到gcroot的最短路徑)对途,如果能找到說明我們的對象確實(shí)泄漏了,最后會(huì)調(diào)用AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result)將發(fā)送出去髓棋。

3.泄漏結(jié)果展示

泄漏結(jié)果主要是在DisplayLeakService類中實(shí)現(xiàn)的实檀,實(shí)現(xiàn)方法也不是很麻煩,大家可以自行查看按声,以為不在于主流程中膳犹,我們暫時(shí)就不講了。

總結(jié):我們知道我們android系統(tǒng)中可能自身存在一些泄漏情況儒喊,所以我們LeakCanary提供了AndroidExcludedRefs類來進(jìn)行排除監(jiān)測镣奋,這樣我們不需要在乎Framework層本身的泄漏問題。現(xiàn)在LeakCanary的使用越來越多了怀愧,希望我們也能適當(dāng)在代碼中引入來檢測自己寫的代碼是否有泄漏的風(fēng)險(xiǎn)侨颈,進(jìn)而提升我們的代碼質(zhì)量余赢,當(dāng)然我們平時(shí)也要關(guān)注一些常見的內(nèi)存泄漏情況,我們可以參考MAT內(nèi)存泄漏分析(一)MAT內(nèi)存泄漏分析(二)哈垢,最后祝大家性能優(yōu)化之路愉快妻柒。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市耘分,隨后出現(xiàn)的幾起案子举塔,更是在濱河造成了極大的恐慌,老刑警劉巖求泰,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件央渣,死亡現(xiàn)場離奇詭異,居然都是意外死亡渴频,警方通過查閱死者的電腦和手機(jī)芽丹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來卜朗,“玉大人拔第,你說我怎么就攤上這事〕《ぃ” “怎么了蚊俺?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長逛万。 經(jīng)常有香客問我泳猬,道長,這世上最難降的妖魔是什么宇植? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任暂殖,我火速辦了婚禮,結(jié)果婚禮上当纱,老公的妹妹穿的比我還像新娘。我一直安慰自己踩窖,他們只是感情好坡氯,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著洋腮,像睡著了一般箫柳。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上啥供,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天悯恍,我揣著相機(jī)與錄音,去河邊找鬼伙狐。 笑死涮毫,一個(gè)胖子當(dāng)著我的面吹牛瞬欧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播罢防,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼艘虎,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了咒吐?” 一聲冷哼從身側(cè)響起野建,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎恬叹,沒想到半個(gè)月后候生,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡绽昼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年唯鸭,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绪励。...
    茶點(diǎn)故事閱讀 39,688評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡肿孵,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出疏魏,到底是詐尸還是另有隱情停做,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布大莫,位于F島的核電站蛉腌,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏只厘。R本人自食惡果不足惜烙丛,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望羔味。 院中可真熱鬧河咽,春花似錦、人聲如沸赋元。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽搁凸。三九已至媚值,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間护糖,已是汗流浹背褥芒。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留嫡良,地道東北人锰扶。 一個(gè)月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓献酗,卻偏偏與公主長得像,于是被迫代替她去往敵國和親少辣。 傳聞我的和親對象是個(gè)殘疾皇子凌摄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評論 2 353

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