LeakCanary(二)記憶體洩露監測原理研究

CharliChen發表於2016-12-13

LeakCanary 記憶體洩露監測原理研究

字數2978 閱讀1120 評論2 

"Read the fucking source code" -- linus一句名言體現出了閱讀原始碼的重要性,學習別人得程式碼是提升自己的重要途徑。最近用到了LeakCanary,順便看一下其程式碼,學習一下。
LeakCanary是安卓中用來檢測記憶體洩露的小工具,它能幫助我們提早發現程式碼中隱藏的bug, 降低應用中記憶體洩露以及OOM產生的概率。

總體流程

LeakCanary中,檢測主要分為三步:1.檢測一個物件是否是可疑的洩漏物件;2.如果第一步發現可疑物件,dump記憶體快照,通過分析.hprof檔案,確定懷疑的物件是否真的洩漏。3.將分析的結果展示


廢話不多說,關於LeakCanary的使用方法,其實很簡單,如果我們只想檢測Activity的記憶體洩露,而且只想使用其預設的報告方式,我們只需要在Application中加一行程式碼,

LeakCanary.install(this);

那我們今天閱讀原始碼的切入點,就從這個靜態方法開始。

 /**
   * 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());
  }

這個函式內部直接呼叫了另外一個過載的函式

/**
   * 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程式裡
    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;
  }

因為leakcanay會開啟一個遠端service用來分析每次產生的記憶體洩露,而安卓的應用每次開啟程式都會呼叫Applicaiton的onCreate方法,因此我們有必要預先判斷此次Application啟動是不是在analyze service啟動時,

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

    //查詢當前程式名
    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程式裡面啟動,最直接的方法就是判斷當前程式名和service所屬的程式是否相同。當前程式名的獲取方式是使用ActivityManager的getRunningAppProcessInfo方法,找到程式pid與當前程式pid相同的程式,然後從中拿到processName. service所屬程式名。獲取service應處程式的方法是用PackageManager的getPackageInfo方法。

RefWatcher

ReftWatcher是leakcancay檢測記憶體洩露的發起點。使用方法為,在物件生命週期即將結束的時候,呼叫

RefWatcher.watch(Object object)

為了達到檢測記憶體洩露的目的,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: 執行記憶體洩露檢測的executor
  • debuggerControl :用於查詢是否正在除錯中,除錯中不會執行記憶體洩露檢測
  • queue : 用於判斷弱引用所持有的物件是否已被GC。
  • gcTrigger: 用於在判斷記憶體洩露之前,再給一次GC的機會
  • headDumper: 用於在產生記憶體洩露室執行dump 記憶體heap
  • heapdumpListener: 用於分析前面產生的dump檔案,找到記憶體洩露的原因
  • excludedRefs: 用於排除某些系統bug導致的記憶體洩露
  • retainedKeys: 持有那些呆檢測以及產生記憶體洩露的引用的key。

接下來,我們來看看watch函式背後是如何利用這些工具,生成記憶體洩露分析報告的。

public void watch(Object watchedReference, String referenceName) {
    checkNotNull(watchedReference, "watchedReference");
    checkNotNull(referenceName, "referenceName");
    //如果處於debug模式,則直接返回
    if (debuggerControl.isDebuggerAttached()) {
      return;
    }
    //記住開始觀測的時間
    final long watchStartNanoTime = System.nanoTime();
    //生成一個隨機的key,並加入set中
    String key = UUID.randomUUID().toString();
    retainedKeys.add(key);
    //生成一個KeyedWeakReference
    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue);
    //呼叫watchExecutor,執行記憶體洩露的檢測
    watchExecutor.execute(new Runnable() {
      @Override public void run() {
        ensureGone(reference, watchStartNanoTime);
      }
    });
  }

所以最後的核心函式是在ensureGone這個runnable裡面。要理解其工作原理,就得從keyedWeakReference說起

WeakReference與ReferenceQueue

從watch函式中,可以看到,每次檢測物件記憶體是否洩露時,我們都會生成一個KeyedReferenceQueue,這個類其實就是一個WeakReference,只不過其額外附帶了一個key和一個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");
  }
}

在構造時我們需要傳入一個ReferenceQueue,這個ReferenceQueue是直接傳入了WeakReference中,關於這個類,有興趣的可以直接看Reference的原始碼。我們這裡需要知道的是,每次WeakReference所指向的物件被GC後,這個弱引用都會被放入這個與之相關聯的ReferenceQueue佇列中。

我們這裡可以貼下其核心程式碼

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類載入的時候,java虛擬機器會建立一個最大優先順序的後臺執行緒,這個執行緒的工作原理就是不斷檢測pending是否為null,如果不為null,就將其放入ReferenceQueue中,pending不為null的情況就是,引用所指向的物件已被GC,變為不可達。

那麼只要我們在構造弱引用的時候指定了ReferenceQueue,每當弱引用所指向的物件被記憶體回收的時候,我們就可以在queue中找到這個引用。如果我們期望一個物件被回收,那如果在接下來的預期時間之後,我們發現它依然沒有出現在ReferenceQueue中,那就可以判定它的記憶體洩露了。LeakCanary檢測記憶體洩露的核心原理就在這裡。

其實Java裡面的WeakHashMap裡也用到了這種方法,來判斷hash表裡的某個鍵值是否還有效。在構造WeakReference的時候給其指定了ReferenceQueue.

監測時機

什麼時候去檢測能判定記憶體洩露呢?這個可以看AndroidWatchExecutor的實現

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

這裡又看到一個比較少的用法,IdleHandler,IdleHandler的原理就是在messageQueue因為空閒等待訊息時給使用者一個hook。那AndroidWatchExecutor會在主執行緒空閒的時候,派發一個後臺任務,這個後臺任務會在DELAY_MILLIS時間之後執行。LeakCanary設定的是5秒。

二次確認保證記憶體洩露準確性

為了避免因為gc不及時帶來的誤判,leakcanay會進行二次確認進行保證。

void ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    //計算從呼叫watch到進行檢測的時間段
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
    //根據queue移除已被GC的物件的弱引用
    removeWeaklyReachableReferences();
    //如果記憶體已被回收或者處於debug模式,直接返回
    if (gone(reference) || debuggerControl.isDebuggerAttached()) {
      return;
    }
    //如果記憶體依舊沒被釋放,則再給一次gc的機會
    gcTrigger.runGc();
    //再次移除
    removeWeaklyReachableReferences();
    if (!gone(reference)) {
      //走到這裡,認為記憶體確實洩露了
      long startDumpHeap = System.nanoTime();
      long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
      //dump出heap報告
      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

監測到記憶體洩露後,首先做的就是dump出當前的heap,預設的AndroidHeapDumper呼叫的是

Debug.dumpHprofData(filePath);

到處當前記憶體的hprof分析檔案,一般我們在DeviceMonitor中也可以dump出hprof檔案,然後將其從dalvik格式轉成標準jvm格式,然後使用MAT進行分析。

那麼LeakCanary是如何分析記憶體洩露的呢?

HaHa

LeakCanary 分析記憶體洩露用到了一個和Mat類似的工具叫做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));
    }
  }

關於HaHa的原理,感興趣的同學可以深究,這裡就不深入介紹了。

返回的ActivityResult物件中包含了物件到GC root的最短路徑。LeakCanary在dump出hprof檔案後,會啟動一個IntentService進行分析:HeapAnalyzerService在分析出結果之後會啟動DisplayLeakService用來發起Notification 以及將結果記錄下來寫在檔案裡面。以後每次啟動LeakAnalyzerActivity就從檔案裡讀取歷史結果。

ExcludedRef

由於某些系統的bug,以及某些廠商rom的bug,Activity在finish之後仍然會被某些系統元件給hold住。LeakCanary列出了一些很常見的,比如三星的手機activity會被audioManager給hold住,試了一下huawei的系統貌似也會出現,還有比如activity中如果有會獲取鍵盤焦點的view,在activity finish之後view會被InputMethodManager給hold住,因為view會持有activity 造成activity洩漏,除非有新的view獲取鍵盤焦點。

LeakCanary中有一個AndroidExcludedRefs列舉類,其中列舉了很多特定版本系統issue引起的記憶體洩漏,因為這種問題 不是開發者導致的,因此HeapAnalyzerService在分析記憶體洩露時,會將這些GC Root排除在外。而且每個ExcludedRef通常都跟特定廠商或者Android版本有關,這些列舉類都加了一個適用條件。

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

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

手動釋放資源

然後並不是leakCanary不報錯我們就不用管,activity記憶體洩露了,大部分情況下沒多大事,但是有些佔用記憶體很多的頁面,比如相簿,webview頁面,因為acitivity不能回收,它所指向的view以及view下面的bitmap都不能被回收,這是會造成很不好的後果的,很可能會導致OOM,因此我們需要手動在Activity結束時回收資源。

Under 4.0 & Fragment

LeakCanary只支援4.0以上,原因是其中在watch 每個Activity時適用了Application的registerActivityLifecycleCallback函式,這個函式只在4.0上才支援,但是在4.0以下也是可以用的,可以在Application中將返回的RefWatcher存下來,然後在基類Activity的onDestroy函式中呼叫。

同理,如果我們想檢測Fragment的記憶體的話,我們也闊以在Fragment的onDestroy中watch它。

相關文章