博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
LeakCanary(二)内存泄露监测原理研究
阅读量:4227 次
发布时间:2019-05-26

本文共 12988 字,大约阅读时间需要 43 分钟。

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
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
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
retainedKeys; private final ReferenceQueue
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 {
public final String key; public final String name; KeyedWeakReference(Object referent, String key, String name, ReferenceQueue 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 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 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的方法如下:

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

转载地址:http://npdqi.baihongyu.com/

你可能感兴趣的文章
【iOS游戏开发】icon那点事 之 图标设计(三)
查看>>
【一天一道LeetCode】#45. Jump Game II
查看>>
终端驱动程序:几个简单例子
查看>>
HTML条件注释
查看>>
内核态与用户态
查看>>
趣链 BitXHub跨链平台 (4)跨链网关“初介绍”
查看>>
POJ 1101 解题报告
查看>>
DAG以及任务调度
查看>>
3126 Prime Path
查看>>
app自动化测试---ADBInterface驱动安装失败问题:
查看>>
九度OJ 1091:棋盘游戏 (DP、BFS、DFS、剪枝)
查看>>
AtomicInteger源码解析
查看>>
CopyOnWriteArraySet源码学习
查看>>
Openfiler 配置 NFS 示例
查看>>
Oracle 11.2.0.1 RAC GRID 无法启动 : Oracle High Availability Services startup failed
查看>>
Oracle 18c 单实例安装手册 详细截图版
查看>>
Oracle Linux 6.1 + Oracle 11.2.0.1 RAC + RAW 安装文档
查看>>
Oracle 11g 新特性 -- Online Patching (Hot Patching 热补丁)说明
查看>>
Oracle 11g 新特性 -- ASM 增强 说明
查看>>
Oracle 11g 新特性 -- Database Replay (重演) 说明
查看>>