LeakCanary 内存泄露监测原理研究

Posted by JackPeng on August 9, 2019

"Read the fucking source code" -- linus一句名言体现出了阅读源码的重要性,学习别人得代码是提升自己的重要途径。最近用到了LeakCanary,顺便看一下其代码,学习一下。
LeakCanary是安卓中用来检测内存泄露的小工具,它能帮助我们提早发现代码中隐藏的bug, 降低应用中内存泄露以及OOM产生的概率。

废话不多说,关于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它。