美文网首页
LeakCanary : 内存泄露检测

LeakCanary : 内存泄露检测

作者: 皇马船长 | 来源:发表于2017-01-05 15:26 被阅读257次

    什么是内存泄露

    一些对象有着有限的生命周期。当这些对象所要做的事情完成了,我们希望他们会被回收掉。但是如果有一系列对这个对象的引用,那么在我们期待这个对象生命周期结束的时候被收回的时候,它是不会被回收的。它还会占用内存,这就造成了内存泄露。持续累加,内存很快被耗尽。

    比如,当 Activity.onDestroy被调用之后,activity 以及它涉及到的 view 和相关的 bitmap 都应该被回收。但是,如果有一个后台线程持有这个 activity 的引用,那么 activity 对应的内存就不能被回收。这最终将会导致内存耗尽,然后因为 OOM 而 crash。

    LeakCanary

    LeakCanarySquare公司开发的一个用于检测OOM(out of memory的缩写)问题的开源库。你可以在 debug 包种轻松检测内存泄露。
    Github地址:https://github.com/square/leakcanary

    如何使用

    引入LeakCanary库 ,在项目的build.gradle文件添加:

       debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3'
       releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'
    

    gradle 强大的可配置性,可以确保只在编译 debug 版本时才会检查内存泄露,而编译 release 等版本的时候则会自动跳过检查,避免影响性能。
    在自定义的Application中初始化 ,LeakCanary.install()会返回一个预定义的 RefWatcher,同时也会启用一个 ActivityRefWatcher,用于自动监控调用 Activity.onDestroy()之后泄的activity。

    如果只想检测Activity的内存泄露,只需要添加这一行代码 。

     private static RefWatcher refWatcher;
     @Override
        public void onCreate() {
            super.onCreate();
            //LeakCanary 就会自动侦测 activity 的内存泄露
            // 会返回一个预定义的 RefWatcher ,同时也会启用一个 ActivityRefWatcher,用于自动监控调用 Activity.onDestroy() 之后泄露的 activity。
            refWatcher = LeakCanary.install(this);
        }
    

    如果还想检测fragment

    public class BaseFragment extends Fragment {
        //使用 RefWatcher 监控 Fragment:
        @Override
        public void onDestroy() {
            super.onDestroy();
            RefWatcher refWatcher = MyApplication.getRefWatcher();
            refWatcher.watch(this);
        }
    }
    

    当你在测试debug版本过程中出现内存泄露时,LeakCanary将会自动展示一个通知栏

    screenshot.png

    通过通知里的信息,我们可以解决很多内存泄露问题 。

    备注:LeakCanary只支持4.0以上,原因是其中在watch 每个Activity时调用了Application的registerActivityLifecycleCallback函数,这个函数只在4.0上才支持,但是在4.0以下也是可以用的,可以在Application中将返回的RefWatcher存下来,然后在基类Activity的onDestroy函数中调用。

    工作原理

    简单概述一下, 源码还没有分析明白 。

    1. RefWatcher.watch()创建一个 KeyedWeakReference 到要被监控的对象 (也就是弱引用)。
    2. 然后在后台线程检查引用是否被清除,如果没有,调用GC。
    3. 如果引用还是未被清除,把 heap 内存 dump 到 APP 对应的文件系统中的一个 .hprof文件中。
    4. 在另外一个进程中的 HeapAnalyzerService有一个 HeapAnalyzer使用HAHA 解析这个文件。得益于唯一的 reference key, HeapAnalyzer找到 KeyedWeakReference,定位内存泄露。
    5. HeapAnalyzer计算 到 GC roots 的最短强引用路径,并确定是否是泄露。如果是的话,建立导致泄露的引用链。
    6. 引用链传递到 APP 进程中的 DisplayLeakService, 并以通知的形式展示出来。

    源码层面简单分析

    RefWatch

    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;
    
    • watchExecutor: 执行内存泄露检测的executor
    • debuggerControl :用于查询是否正在调试中,调试中不会执行内存泄露检测
    • gcTrigger: 用于在判断内存泄露之前,再给一次GC的机会
    • headDumper: 用于在产生内存泄露室执行dump 内存heap
    • retainedKeys: 持有那些呆检测以及产生内存泄露的引用的key
    • queue : 用于判断弱引用所持有的对象是否已被GC。
    • heapdumpListener: 用于分析前面产生的dump文件,找到内存泄露的原因
      接下来,我们来看看watch函数背后是如何利用这些工具,生成内存泄露分析报告的。
     /**
       * Watches the provided references and checks if it can be GCed. This method is non blocking,
       * the check is done on the {@link Executor} this {@link RefWatcher} has been constructed with.
       *
       * @param referenceName An logical identifier for the watched object.
       */
      public void watch(Object watchedReference, String referenceName) {
        checkNotNull(watchedReference, "watchedReference");
        checkNotNull(referenceName, "referenceName");
        // 如果处于debug模式,直接return
        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这个方法里面。要理解其工作原理,就得从keyedWeakReference说起

    WeakReference与ReferenceQueue

    从watch函数中,可以看到,每次检测对象内存是否泄露时,我们都会生成一个KeyedReferenceQueue,这个类其实就是一个WeakReference,只不过其额外附带了一个key和一个name

    
    /** @see {@link HeapDump#referenceKey}. */
    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队列中。

    在reference类加载的时候,java虚拟机会创建一个最大优先级的后台线程,这个线程的工作原理就是不断检测pending是否为null,如果不为null,就将其放入ReferenceQueue中,pending不为null的情况就是,引用所指向的对象已被GC,变为不可达。

    那么只要我们在构造弱引用的时候指定了ReferenceQueue,每当弱引用所指向的对象被内存回收的时候,我们就可以在queue中找到这个引用。如果我们期望一个对象被回收,那如果在接下来的预期时间之后,我们发现它依然没有出现在ReferenceQueue中,那就可以判定它的内存泄露了。LeakCanary检测内存泄露的核心原理就在这里。

    监测时机

    什么时候去检测能判定内存泄露呢?这个可以看AndroidWatchExecutor的实现

    public final class AndroidWatchExecutor implements Executor {
      private final Handler backgroundHandler;
    
      public AndroidWatchExecutor() {
        mainHandler = new Handler(Looper.getMainLooper());
        HandlerThread handlerThread = new HandlerThread(LEAK_CANARY_THREAD_NAME);
        handlerThread.start();
        backgroundHandler = new Handler(handlerThread.getLooper());
      }
      ....
      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);
    
          File heapDumpFile = heapDumper.dumpHeap();
    
          if (heapDumpFile == null) {
            // Could not dump the heap, abort.
            return;
          }
          long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
          heapdumpListener.analyze(
              new HeapDump(heapDumpFile, reference.key, reference.name, watchDurationMs, gcDurationMs,
                  heapDumpDurationMs));
        }
      }
    

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

    返回的ActivityResult对象中包含了对象到GC root的最短路径。LeakCanary在dump出hprof文件后,会启动一个IntentService进行分析:HeapAnalyzerService在分析出结果之后会启动DisplayLeakService用来发起Notification 以及将结果记录下来写在文件里面。以后每次启动LeakAnalyzerActivity就从文件里读取历史结果。

    参考文档

    LeakCanary 中文使用说明
    LeakCanary 内存泄露监测原理研究 : 结合源码分析leakcanary检查内存泄露的过程。

    相关文章

      网友评论

          本文标题:LeakCanary : 内存泄露检测

          本文链接:https://www.haomeiwen.com/subject/jvxzvttx.html