LeakCanary(二)内存泄露监测原理研究
来源:互联网 发布:api接口文档 源码 编辑:程序博客网 时间:2024/06/09 23:35
LeakCanary 内存泄露监测原理研究
"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它。
- LeakCanary(二)内存泄露监测原理研究
- Android内存泄露监测工具之leakcanary
- Android内存泄露监测之leakcanary
- Github项目解析(三)-->Android内存泄露监测之leakcanary
- Android内存泄露实时监测——LeakCanary(Android Studio+Eclipse)
- Android内存泄露实时监测——LeakCanary/MAT等(Android Studio+Eclipse),测试
- 内存泄露检测工具LeakCanary
- LeakCanary: 查找内存泄露
- 内存泄露检测工具LeakCanary
- [leakcanary]内存泄露检测
- LeakCanary检查内存泄露
- LeakCanary检查内存泄露
- LeakCanary 检测内存泄露
- Android 内存泄露检测工具 LeakCanary 的监控原理
- 接入LeakCanary检测内存泄露
- Android 内存泄露检测工具 LeakCanary
- Android内存泄露检测(LeakCanary)
- LeakCanary-Android内存泄露检测
- 如何用httpie更高效的调试接口
- Unity常见的解析数据方式XML,JSON,ProtocolBuf篇(一)Protobuf
- java 代码覆盖率
- html中js表单验证
- 如何修改“设置” -> “显示” ->“休眠”的默认休眠时间
- LeakCanary(二)内存泄露监测原理研究
- JavaScript异步
- hystrix入门
- java内存分配和String类型的深度解析
- 用wow.js实现滚动页面时触发CSS 动画效果的插件
- 堆、栈、程序区、静态数据区、字符常量区的介绍与区别
- MAVEN pom.xml文件添加依赖报错
- CI框架在Apache服务器下去掉URL中的index.php
- Inorder Successor in Binary Search Tree