LeakCanary 内存泄露监测原理研究

作者: 呆萌狗和求疵喵 | 来源:发表于2015-12-27 23:25 被阅读11140次

    "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它。

    相关文章

      网友评论

      • _孑孓_:leakCanary 一般会检查哪些类型的泄漏呢
        kangaroo9997:非静态内部类持有外部类的引用。
      • happyyy2017:AwPasswordHandler.mAwContents references AwContents.mContainerView 这个webview的内存泄露怎么解决?增加webview destory,removeallview等方法调用还是会有
      • happyyy2017:leakcanary 的内存泄露日志再哪里存储?
      • 我在等你回复可你没回:pending 是什么鬼
        03b655eda1e4:Reference的一种状态,参看[话说ReferenceQueue](http://hongjiang.info/java-referencequeue/)
      • MrFu:*还有比如activity中如果有会获取键盘焦点的view,在activity finish之后view会被InputMethodManager给hold住,因为view会持有activity 造成activity泄漏,除非有新的view获取键盘焦点。*


        这个怎么处理呢碰到了
        MrFu:@MrFu 自己回复了,LeakCanary 的建议是:LEAK CAN BE IGNORED 嗯,那就 ignored 吧~哈哈

      本文标题:LeakCanary 内存泄露监测原理研究

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