LeakCanary 內(nèi)存泄露監(jiān)測(cè)原理研究

"Read the fucking source code" -- linus一句名言體現(xiàn)出了閱讀源碼的重要性,學(xué)習(xí)別人得代碼是提升自己的重要途徑。最近用到了LeakCanary,順便看一下其代碼,學(xué)習(xí)一下。
LeakCanary是安卓中用來(lái)檢測(cè)內(nèi)存泄露的小工具,它能幫助我們提早發(fā)現(xiàn)代碼中隱藏的bug, 降低應(yīng)用中內(nèi)存泄露以及OOM產(chǎn)生的概率。

廢話不多說(shuō),關(guān)于LeakCanary的使用方法,其實(shí)很簡(jiǎn)單,如果我們只想檢測(cè)Activity的內(nèi)存泄露,而且只想使用其默認(rèn)的報(bào)告方式,我們只需要在Application中加一行代碼,

LeakCanary.install(this);

那我們今天閱讀源碼的切入點(diǎn),就從這個(gè)靜態(tài)方法開(kāi)始。

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

這個(gè)函數(shù)內(nèi)部直接調(diào)用了另外一個(gè)重載的函數(shù)

/**
   * Creates a {@link RefWatcher} that reports results to the provided service, and starts watching
   * activity references (on ICS+).
   */
  public static RefWatcher install(Application application,
      Class<? extends AbstractAnalysisResultService> listenerServiceClass,
      ExcludedRefs excludedRefs) {
    //判斷是否在Analyzer進(jìn)程里
    if (isInAnalyzerProcess(application)) {
      return RefWatcher.DISABLED;
    }
    enableDisplayLeakActivity(application);
    HeapDump.Listener heapDumpListener =
        new ServiceHeapDumpListener(application, listenerServiceClass);
    RefWatcher refWatcher = androidWatcher(application, heapDumpListener, excludedRefs);
    ActivityRefWatcher.installOnIcsPlus(application, refWatcher);
    return refWatcher;
  }

因?yàn)閘eakcanay會(huì)開(kāi)啟一個(gè)遠(yuǎn)程service用來(lái)分析每次產(chǎn)生的內(nèi)存泄露,而安卓的應(yīng)用每次開(kāi)啟進(jìn)程都會(huì)調(diào)用Applicaiton的onCreate方法,因此我們有必要預(yù)先判斷此次Application啟動(dòng)是不是在analyze service啟動(dòng)時(shí),

public static boolean isInServiceProcess(Context context, Class<? extends Service> serviceClass) {
    PackageManager packageManager = context.getPackageManager();
    PackageInfo packageInfo;
    try {
      packageInfo = packageManager.getPackageInfo(context.getPackageName(), GET_SERVICES);
    } catch (Exception e) {
      Log.e("AndroidUtils", "Could not get package info for " + context.getPackageName(), e);
      return false;
    }
    String mainProcess = packageInfo.applicationInfo.processName;

    ComponentName component = new ComponentName(context, serviceClass);
    ServiceInfo serviceInfo;
    try {
      serviceInfo = packageManager.getServiceInfo(component, 0);
    } catch (PackageManager.NameNotFoundException ignored) {
      // Service is disabled.
      return false;
    }

    if (serviceInfo.processName.equals(mainProcess)) {
      Log.e("AndroidUtils",
          "Did not expect service " + serviceClass + " to run in main process " + mainProcess);
      // Technically we are in the service process, but we're not in the service dedicated process.
      return false;
    }

    //查找當(dāng)前進(jìn)程名
    int myPid = android.os.Process.myPid();
    ActivityManager activityManager =
        (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
    ActivityManager.RunningAppProcessInfo myProcess = null;
    for (ActivityManager.RunningAppProcessInfo process : activityManager.getRunningAppProcesses()) {
      if (process.pid == myPid) {
        myProcess = process;
        break;
      }
    }
    if (myProcess == null) {
      Log.e("AndroidUtils", "Could not find running process for " + myPid);
      return false;
    }

    return myProcess.processName.equals(serviceInfo.processName);
  }

判斷Application是否是在service進(jìn)程里面啟動(dòng),最直接的方法就是判斷當(dāng)前進(jìn)程名和service所屬的進(jìn)程是否相同。當(dāng)前進(jìn)程名的獲取方式是使用ActivityManager的getRunningAppProcessInfo方法,找到進(jìn)程pid與當(dāng)前進(jìn)程pid相同的進(jìn)程,然后從中拿到processName. service所屬進(jìn)程名。獲取service應(yīng)處進(jìn)程的方法是用PackageManager的getPackageInfo方法。

RefWatcher

ReftWatcher是leakcancay檢測(cè)內(nèi)存泄露的發(fā)起點(diǎn)。使用方法為,在對(duì)象生命周期即將結(jié)束的時(shí)候,調(diào)用

RefWatcher.watch(Object object)

為了達(dá)到檢測(cè)內(nèi)存泄露的目的,RefWatcher需要

  private final Executor watchExecutor;
  private final DebuggerControl debuggerControl;
  private final GcTrigger gcTrigger;
  private final HeapDumper heapDumper;
  private final Set<String> retainedKeys;
  private final ReferenceQueue<Object> queue;
  private final HeapDump.Listener heapdumpListener;
  private final ExcludedRefs excludedRefs;
  • watchExecutor: 執(zhí)行內(nèi)存泄露檢測(cè)的executor
  • debuggerControl :用于查詢是否正在調(diào)試中,調(diào)試中不會(huì)執(zhí)行內(nèi)存泄露檢測(cè)
  • queue : 用于判斷弱引用所持有的對(duì)象是否已被GC。
  • gcTrigger: 用于在判斷內(nèi)存泄露之前,再給一次GC的機(jī)會(huì)
  • headDumper: 用于在產(chǎn)生內(nèi)存泄露室執(zhí)行dump 內(nèi)存heap
  • heapdumpListener: 用于分析前面產(chǎn)生的dump文件,找到內(nèi)存泄露的原因
  • excludedRefs: 用于排除某些系統(tǒng)bug導(dǎo)致的內(nèi)存泄露
  • retainedKeys: 持有那些呆檢測(cè)以及產(chǎn)生內(nèi)存泄露的引用的key。

接下來(lái),我們來(lái)看看watch函數(shù)背后是如何利用這些工具,生成內(nèi)存泄露分析報(bào)告的。

public void watch(Object watchedReference, String referenceName) {
    checkNotNull(watchedReference, "watchedReference");
    checkNotNull(referenceName, "referenceName");
    //如果處于debug模式,則直接返回
    if (debuggerControl.isDebuggerAttached()) {
      return;
    }
    //記住開(kāi)始觀測(cè)的時(shí)間
    final long watchStartNanoTime = System.nanoTime();
    //生成一個(gè)隨機(jī)的key,并加入set中
    String key = UUID.randomUUID().toString();
    retainedKeys.add(key);
    //生成一個(gè)KeyedWeakReference
    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue);
    //調(diào)用watchExecutor,執(zhí)行內(nèi)存泄露的檢測(cè)
    watchExecutor.execute(new Runnable() {
      @Override public void run() {
        ensureGone(reference, watchStartNanoTime);
      }
    });
  }

所以最后的核心函數(shù)是在ensureGone這個(gè)runnable里面。要理解其工作原理,就得從keyedWeakReference說(shuō)起

WeakReference與ReferenceQueue

從watch函數(shù)中,可以看到,每次檢測(cè)對(duì)象內(nèi)存是否泄露時(shí),我們都會(huì)生成一個(gè)KeyedReferenceQueue,這個(gè)類(lèi)其實(shí)就是一個(gè)WeakReference,只不過(guò)其額外附帶了一個(gè)key和一個(gè)name

final class KeyedWeakReference extends WeakReference<Object> {
  public final String key;
  public final String name;

  KeyedWeakReference(Object referent, String key, String name,
      ReferenceQueue<Object> referenceQueue) {
    super(checkNotNull(referent, "referent"), checkNotNull(referenceQueue, "referenceQueue"));
    this.key = checkNotNull(key, "key");
    this.name = checkNotNull(name, "name");
  }
}

在構(gòu)造時(shí)我們需要傳入一個(gè)ReferenceQueue,這個(gè)ReferenceQueue是直接傳入了WeakReference中,關(guān)于這個(gè)類(lèi),有興趣的可以直接看Reference的源碼。我們這里需要知道的是,每次WeakReference所指向的對(duì)象被GC后,這個(gè)弱引用都會(huì)被放入這個(gè)與之相關(guān)聯(lián)的ReferenceQueue隊(duì)列中。

我們這里可以貼下其核心代碼

private static class ReferenceHandler extends Thread {

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

        public void run() {
            for (;;) {
                Reference<Object> r;
                synchronized (lock) {
                    if (pending != null) {
                        r = pending;
                        pending = r.discovered;
                        r.discovered = null;
                    } else {
                        //....
                        try {
                            try {
                                lock.wait();
                            } catch (OutOfMemoryError x) { }
                        } catch (InterruptedException x) { }
                        continue;
                    }
                }

                // Fast path for cleaners
                if (r instanceof Cleaner) {
                    ((Cleaner)r).clean();
                    continue;
                }

                ReferenceQueue<Object> q = r.queue;
                if (q != ReferenceQueue.NULL) q.enqueue(r);
            }
        }
    }

    static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        Thread handler = new ReferenceHandler(tg, "Reference Handler");
        /* If there were a special system-only priority greater than
         * MAX_PRIORITY, it would be used here
         */
        handler.setPriority(Thread.MAX_PRIORITY);
        handler.setDaemon(true);
        handler.start();
    }

在reference類(lèi)加載的時(shí)候,java虛擬機(jī)會(huì)創(chuàng)建一個(gè)最大優(yōu)先級(jí)的后臺(tái)線程,這個(gè)線程的工作原理就是不斷檢測(cè)pending是否為null,如果不為null,就將其放入ReferenceQueue中,pending不為null的情況就是,引用所指向的對(duì)象已被GC,變?yōu)椴豢蛇_(dá)。

那么只要我們?cè)跇?gòu)造弱引用的時(shí)候指定了ReferenceQueue,每當(dāng)弱引用所指向的對(duì)象被內(nèi)存回收的時(shí)候,我們就可以在queue中找到這個(gè)引用。如果我們期望一個(gè)對(duì)象被回收,那如果在接下來(lái)的預(yù)期時(shí)間之后,我們發(fā)現(xiàn)它依然沒(méi)有出現(xiàn)在ReferenceQueue中,那就可以判定它的內(nèi)存泄露了。LeakCanary檢測(cè)內(nèi)存泄露的核心原理就在這里。

其實(shí)Java里面的WeakHashMap里也用到了這種方法,來(lái)判斷hash表里的某個(gè)鍵值是否還有效。在構(gòu)造WeakReference的時(shí)候給其指定了ReferenceQueue.

監(jiān)測(cè)時(shí)機(jī)

什么時(shí)候去檢測(cè)能判定內(nèi)存泄露呢?這個(gè)可以看AndroidWatchExecutor的實(shí)現(xiàn)


public final class AndroidWatchExecutor implements Executor {

    //....
    
    private void executeDelayedAfterIdleUnsafe(final Runnable runnable) {
        // This needs to be called from the main thread.
        Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
          @Override public boolean queueIdle() {
            backgroundHandler.postDelayed(runnable, DELAY_MILLIS);
            return false;
          }
        });
      }
  }

這里又看到一個(gè)比較少的用法,IdleHandler,IdleHandler的原理就是在messageQueue因?yàn)榭臻e等待消息時(shí)給使用者一個(gè)hook。那AndroidWatchExecutor會(huì)在主線程空閑的時(shí)候,派發(fā)一個(gè)后臺(tái)任務(wù),這個(gè)后臺(tái)任務(wù)會(huì)在DELAY_MILLIS時(shí)間之后執(zhí)行。LeakCanary設(shè)置的是5秒。

二次確認(rèn)保證內(nèi)存泄露準(zhǔn)確性

為了避免因?yàn)間c不及時(shí)帶來(lái)的誤判,leakcanay會(huì)進(jìn)行二次確認(rèn)進(jìn)行保證。

void ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    //計(jì)算從調(diào)用watch到進(jìn)行檢測(cè)的時(shí)間段
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
    //根據(jù)queue移除已被GC的對(duì)象的弱引用
    removeWeaklyReachableReferences();
    //如果內(nèi)存已被回收或者處于debug模式,直接返回
    if (gone(reference) || debuggerControl.isDebuggerAttached()) {
      return;
    }
    //如果內(nèi)存依舊沒(méi)被釋放,則再給一次gc的機(jī)會(huì)
    gcTrigger.runGc();
    //再次移除
    removeWeaklyReachableReferences();
    if (!gone(reference)) {
      //走到這里,認(rèn)為內(nèi)存確實(shí)泄露了
      long startDumpHeap = System.nanoTime();
      long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
      //dump出heap報(bào)告
      File heapDumpFile = heapDumper.dumpHeap();

      if (heapDumpFile == HeapDumper.NO_DUMP) {
        // Could not dump the heap, abort.
        return;
      }
      long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
      heapdumpListener.analyze(
          new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
              gcDurationMs, heapDumpDurationMs));
    }
  }

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

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

Dump Heap

監(jiān)測(cè)到內(nèi)存泄露后,首先做的就是dump出當(dāng)前的heap,默認(rèn)的AndroidHeapDumper調(diào)用的是

Debug.dumpHprofData(filePath);

到處當(dāng)前內(nèi)存的hprof分析文件,一般我們?cè)贒eviceMonitor中也可以dump出hprof文件,然后將其從dalvik格式轉(zhuǎn)成標(biāo)準(zhǔn)jvm格式,然后使用MAT進(jìn)行分析。

那么LeakCanary是如何分析內(nèi)存泄露的呢?

HaHa

LeakCanary 分析內(nèi)存泄露用到了一個(gè)和Mat類(lèi)似的工具叫做HaHa,使用HaHa的方法如下:

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

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

關(guān)于HaHa的原理,感興趣的同學(xué)可以深究,這里就不深入介紹了。

返回的ActivityResult對(duì)象中包含了對(duì)象到GC root的最短路徑。LeakCanary在dump出hprof文件后,會(huì)啟動(dòng)一個(gè)IntentService進(jìn)行分析:HeapAnalyzerService在分析出結(jié)果之后會(huì)啟動(dòng)DisplayLeakService用來(lái)發(fā)起Notification 以及將結(jié)果記錄下來(lái)寫(xiě)在文件里面。以后每次啟動(dòng)LeakAnalyzerActivity就從文件里讀取歷史結(jié)果。

ExcludedRef

由于某些系統(tǒng)的bug,以及某些廠商rom的bug,Activity在finish之后仍然會(huì)被某些系統(tǒng)組件給hold住。LeakCanary列出了一些很常見(jiàn)的,比如三星的手機(jī)activity會(huì)被audioManager給hold住,試了一下huawei的系統(tǒng)貌似也會(huì)出現(xiàn),還有比如activity中如果有會(huì)獲取鍵盤(pán)焦點(diǎn)的view,在activity finish之后view會(huì)被InputMethodManager給hold住,因?yàn)関iew會(huì)持有activity 造成activity泄漏,除非有新的view獲取鍵盤(pán)焦點(diǎn)。

LeakCanary中有一個(gè)AndroidExcludedRefs枚舉類(lèi),其中枚舉了很多特定版本系統(tǒng)issue引起的內(nèi)存泄漏,因?yàn)檫@種問(wèn)題 不是開(kāi)發(fā)者導(dǎo)致的,因此HeapAnalyzerService在分析內(nèi)存泄露時(shí),會(huì)將這些GC Root排除在外。而且每個(gè)ExcludedRef通常都跟特定廠商或者Android版本有關(guān),這些枚舉類(lèi)都加了一個(gè)適用條件。

AndroidExcludedRefs(boolean applies) {  this.applies = applies;}


   AUDIO_MANAGER__MCONTEXT_STATIC(SAMSUNG.equals(MANUFACTURER) && SDK_INT == KITKAT) {
    @Override void add(ExcludedRefs.Builder excluded) {
      // Samsung added a static mContext_static field to AudioManager, holds a reference to the
      // activity.
      // Observed here: https://github.com/square/leakcanary/issues/32
      excluded.staticField("android.media.AudioManager", "mContext_static");
    }
  },

比如上面這個(gè)AudioManager引起的問(wèn)題,只有在Build中的MANUFACTURER表明是三星以及sdk版本是KITKAT(4.4, 19)時(shí)才適用。

手動(dòng)釋放資源

然后并不是leakCanary不報(bào)錯(cuò)我們就不用管,activity內(nèi)存泄露了,大部分情況下沒(méi)多大事,但是有些占用內(nèi)存很多的頁(yè)面,比如圖庫(kù),webview頁(yè)面,因?yàn)閍citivity不能回收,它所指向的view以及view下面的bitmap都不能被回收,這是會(huì)造成很不好的后果的,很可能會(huì)導(dǎo)致OOM,因此我們需要手動(dòng)在Activity結(jié)束時(shí)回收資源。

Under 4.0 & Fragment

LeakCanary只支持4.0以上,原因是其中在watch 每個(gè)Activity時(shí)適用了Application的registerActivityLifecycleCallback函數(shù),這個(gè)函數(shù)只在4.0上才支持,但是在4.0以下也是可以用的,可以在Application中將返回的RefWatcher存下來(lái),然后在基類(lèi)Activity的onDestroy函數(shù)中調(diào)用。

同理,如果我們想檢測(cè)Fragment的內(nèi)存的話,我們也闊以在Fragment的onDestroy中watch它。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容