6

LeakCanary 内存泄露监测原理研究

 3 years ago
source link: http://yuanfentiank789.github.io/2019/08/09/LeakCanary-%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2%E7%9B%91%E6%B5%8B%E5%8E%9F%E7%90%86%E7%A0%94%E7%A9%B6/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
LeakCanary 内存泄露监测原理研究

"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是如何分析内存泄露的呢?

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK