LeakCanary(二)内存泄露监测原理研究

来源:互联网 发布:api接口文档 源码 编辑:程序博客网 时间:2024/06/09 23:35

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

1 0
原创粉丝点击