leakcanary核心原理源码浅析

Cloud_Huan Cloud_Huan     2022-08-12     110

关键词:

网上大牛太多,不敢说分析,也不敢装成大大,所以只能是浅析…
那么今天这篇主要解决什么问题呢?其实就一个问题,LeakCanay.install(this)这个函数到底是怎么走的,用测试的话说就是数据流是怎么走的,用探索性测试的方法说就是:快递法。

LeakCanay项目的地址是:https://github.com/square/leakcanary

那么经过半天的下载之后,成功导入到as中。

直接从入口函数分析:LeakCanary.install(this),那么打算是分四个部分分析,第一部分分析入口,第二部分分析怎么判断泄漏,第三部分是对内存快照的处理,第四部分是总结和扩展,东西不多,鉴于我自己的水平只能看一点算一点了 :)

一、函数入口和常用类分析

  public static RefWatcher install(Application application) {
    return refWatcher(application).listenerServiceClass(DisplayLeakService.class)
        .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
        .buildAndInstall();
  }

那么install静态方法里面包含了3个方法,这3个方法都是通过AndroidRefWatcherBuilder这个辅助类来配置相关信息的:

listenerServiceClass方法是和结果分析相关的服务绑定,绑定到DisplayLeakService.class这个类上面,这个类负责通知泄漏消息给你
excludedRefs方法是排除一些开发可以忽略的泄漏路径(一般是系统级别BUG),这些枚举在AndroidExcludedRefs这个类当中定义
buildAndInstall这才是重点方法,所以看下:

  public RefWatcher buildAndInstall() {
    RefWatcher refWatcher = build();
    if (refWatcher != DISABLED) {
      LeakCanary.enableDisplayLeakActivity(context);
      ActivityRefWatcher.installOnIcsPlus((Application) context, refWatcher);
    }
    return refWatcher;
  }

上面没啥了,无非就是实例化RefWatcher对象,这个对象是用来判断泄漏对象的,马上会说
所以直接看ActivityRefWatcher.installOnIcsPlus方法:

  public static void installOnIcsPlus(Application application, RefWatcher refWatcher) {
    if (SDK_INT < ICE_CREAM_SANDWICH) {
      // If you need to support Android < ICS, override onDestroy() in your base activity.
      return;
    }
    ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
    activityRefWatcher.watchActivities();
  }

嗯,已经跳转到了ActivityRefWatcher这个类,这个类提供了下面这个api:

  public void watchActivities() {
    // Make sure you don‘t get installed twice.
    stopWatchingActivities();
    application.registerActivityLifecycleCallbacks(lifecycleCallbacks);
  }

所以核心方法找到了,就是registerActivityLifecycleCallbacks这个方法,这是application提供的一个方法,用来统一管理所有activity的生命周期,LeakCanay是在onDestory()方法实现监控的:

  private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
      new Application.ActivityLifecycleCallbacks() {
        @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        }

        @Override public void onActivityStarted(Activity activity) {
        }

        @Override public void onActivityResumed(Activity activity) {
        }

        @Override public void onActivityPaused(Activity activity) {
        }

        @Override public void onActivityStopped(Activity activity) {
        }

        @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
        }

        @Override public void onActivityDestroyed(Activity activity) {
          ActivityRefWatcher.this.onActivityDestroyed(activity);
        }
      };

之后把activity直接扔给RefWatcher类处理,所以第一部分完成

总结:
LeakCanay的自动检测泄漏是在4.0之上的,用了Application的ActivityLifecycleCallbacks回调接口中的onDestory()回调实现对项目中的activity进行对象监听,然后交给RefWatcher监听类进行进一步处理。

二、怎么判断泄漏

那么我们回顾一下传统的检测内存泄漏的方法:

1.GC后dump一份内存快照

2.频繁操作GC后再dump一份内存快照

3.重复并用mat工具对比对象增量

所以旧方法对劳动力和精确度都是一个考验啊,LeakCanay同样是用了内存快照,但是在精确对比之前还加入了弱引用算法,可以理解成是粗定位到精定位的一个过程。

接着上一部分,跳到RefWatcher.watch这个函数:

public void watch(Object watchedReference, String referenceName) {
    if (this == DISABLED) {
      return;
    }
    checkNotNull(watchedReference, "watchedReference");
    checkNotNull(referenceName, "referenceName");
    final long watchStartNanoTime = System.nanoTime();
    String key = UUID.randomUUID().toString();
    retainedKeys.add(key);
    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue);

    ensureGoneAsync(watchStartNanoTime, reference);
  }

上面做的事情就是把activity对象封装成带key值和带引用队列(ReferenceQueue)的KeyedWeakReference对象,key值是用来最终定位泄漏对象用的,第三部分会用到,引用队列是用来监控弱引用回收的,这个类是继承WeakReference类,所以封装完你会看到这个类的一些特点:

1.带key,通过UUID.randomUUID().toString(),是唯一key序列

2.包装成WeakReference并添加到ReferenceQueue

之后呢,封装完成就要开始分析了,核心方法是ensureGone,同时也是LeakCanay的核心所在,所以我注释了几个关键步骤:

  Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);

    removeWeaklyReachableReferences();   //看下检测的弱引用回收了没

    if (debuggerControl.isDebuggerAttached()) {
      // The debugger can create false leaks.
      return RETRY;
    }
    if (gone(reference)) {              //好,回收了,那么这个activity没有泄漏
      return DONE;
    }
    gcTrigger.runGc();                  //还是没有回收,手动GC一下
    removeWeaklyReachableReferences();   //在看看对象回收没有
    if (!gone(reference)) {                    //竟然还没回收,那么怀疑是内存泄漏了,所以下一步dump内存快照.hprof下来进一步精确分析
      long startDumpHeap = System.nanoTime();
      long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);

      File heapDumpFile = heapDumper.dumpHeap();
      if (heapDumpFile == RETRY_LATER) {
        // Could not dump the heap.
        return RETRY;
      }
      long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
      heapdumpListener.analyze(
          new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
              gcDurationMs, heapDumpDurationMs));
    }
    return DONE;
  }

一个个看,removeWeaklyReachableReferences这个方法:

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);
    }
  }

如果这个对象作为弱引用,被回收了,那么添加到引用队列(ReferenceQueue)当中去,所以这个函数.poll是出栈的意思,如果成功出栈了,那么说明你加入了引用队列,然后可以认为是已经被回收了的,然后retainedKeys这个是一个Set容器,在之前会加入生成的唯一key作为标识,这里如果这个对象回收了,那么就移除这个key值。

然后是gone函数,就是看retainedKeys容器有没有key,如果回收了,就不存在key了,那么就没有泄漏,否则就怀疑有泄漏。

然后后面的手动GC和检查都是一个类似二次确认的道理,还是没有回收,那么才会进入精确阶段,.hropf分析大法,这是第三部分内容。

总结:怀疑该对象有没有泄漏,需要经过两部走,RefWatcher封装了非常核心的方法,把对象封装成WackReference,并执行GC回收,如果存在强引用,那么会无法回收,就会认为有内存泄漏的可能了,然后才会快照分析发。

三、.hropf内存快照分析

那么在第二步根据弱引用有没有回收这个上已经是基本确定了这个对象有没有泄漏,那么下一部就是获取dumpheap以及对这个dumpheap进行analyze。

调用就是下面这几行,然而这几行的背后其实引用的又是另外一个项目haha,具体的开源地址为:https://github.com/square/haha

File heapDumpFile = heapDumper.dumpHeap();
      if (heapDumpFile == RETRY_LATER) {
        // Could not dump the heap.
        return RETRY;
      }
      long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
      heapdumpListener.analyze(
          new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
              gcDurationMs, heapDumpDurationMs));

那么重点看heapdumpListener.analyze这个方法,还记得第一部分的入口处有一个listenerServiceClass,打开来看看:

  public AndroidRefWatcherBuilder listenerServiceClass(
      Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
    return heapDumpListener(new ServiceHeapDumpListener(context, listenerServiceClass));
  }

所以追着下去可以看到在ServiceHeapDumpListener这个类封装了analyze方法的具体实现,如下:

 @Override public void analyze(HeapDump heapDump) {
    checkNotNull(heapDump, "heapDump");
    HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass);
  }

之后便是开启一个服务专门用来处理heapdump,找出最短路径并展示通知,所以直接跳到:

AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey);

这个最关键的方法里面。

  /**
   * Searches the heap dump for a {@link KeyedWeakReference} instance with the corresponding key,
   * and then computes the shortest strong reference path from that instance to the GC roots.
   */
  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();  //1.把.hprof转为Snapshot,这个Snapshot对象就包含了对象引用的所有路径
      deduplicateGcRoots(snapshot);   //2.精简gcroots??没懂?

      Instance leakingRef = findLeakingReference(referenceKey, snapshot);  //3.找出泄漏的对象

      // 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);   //4.找出泄漏对象的最短路径
    } catch (Throwable e) {
      return failure(e, since(analysisStartNanoTime));
    }
  }

上面的checkForLeak方法就是输入.hprof,输出分析结果,主要有以下几个步骤:

1.把.hprof转为Snapshot,这个Snapshot对象就包含了对象引用的所有路径

2.精简gcroots,把重复的路径删除,重新封装成不重复的路径的容器

3.找出泄漏的对象

4.找出泄漏对象的最短路径

重点分析放在第3、4步:

  private Instance findLeakingReference(String key, Snapshot snapshot) {
    ClassObj refClass = snapshot.findClass(KeyedWeakReference.class.getName());
    List<String> keysFound = new ArrayList<>();
    for (Instance instance : refClass.getInstancesList()) {
      List<ClassInstance.FieldValue> values = classInstanceValues(instance);
      String keyCandidate = asString(fieldValue(values, "key"));
      if (keyCandidate.equals(key)) {
        return fieldValue(values, "referent");
      }
      keysFound.add(keyCandidate);
    }
    throw new IllegalStateException(
        "Could not find weak reference with key " + key + " in " + keysFound);
  }

那么上面这个方法是在snapshot快照中找到第一个弱引用(因为就是这个对象没有回收,泄漏了嘛),然后根据遍历这个对象的所有实例,如果key值和最开始定义封装的key值相同,那么返回这个泄漏对象,就是已近在快照中定位到了泄漏对象了。

 private AnalysisResult findLeakTrace(long analysisStartNanoTime, Snapshot snapshot,
      Instance leakingRef) {

    ShortestPathFinder pathFinder = new ShortestPathFinder(excludedRefs);
    ShortestPathFinder.Result result = pathFinder.findPath(snapshot, leakingRef);

    // False alarm, no strong reference path to GC Roots.
    if (result.leakingNode == null) {
      return noLeak(since(analysisStartNanoTime));
    }

    LeakTrace leakTrace = buildLeakTrace(result.leakingNode);

    String className = leakingRef.getClassObj().getClassName();

    // Side effect: computes retained size.
    snapshot.computeDominators();

    Instance leakingInstance = result.leakingNode.instance;

    long retainedSize = leakingInstance.getTotalRetainedSize();

    retainedSize += computeIgnoredBitmapRetainedSize(snapshot, leakingInstance);

    return leakDetected(result.excludingKnownLeaks, className, leakTrace, retainedSize,
        since(analysisStartNanoTime));
  }

最后一个步骤是根据上一部得到的泄漏对象找到最短路径,封装在ShortestPathFinder.Result result = pathFinder.findPath(snapshot, leakingRef);这句中,具体算法就不看了,因为看不懂,汗一个…

总结:在第三个分析步骤,解析hprof文件中,是先把这个文件封装成snapshot,然后根据弱引用和前面定义的key值,确定泄漏的对象,最后找到最短泄漏路径,作为结果反馈出来,那么如果在快照中找不到这个怀疑泄漏的对象,那么就认为这个对象其实并没有泄漏,因为已经回收了,如下的代码:

// False alarm, weak reference was cleared in between key check and heap dump.
      if (leakingRef == null) {
        return noLeak(since(analysisStartNanoTime));
      }

四、总结和扩展

那么LeakCanay的源码浅析就分析完了,从一开始觉得这工具高大上到分析完觉得就是这样(当前我是写不出来的)。最后总结一下全文,也方便作为分享的忽悠 :)

   LeakCanay的入口是在application的onCreate()方法中声明的,其实用的就是Application的ActivityLifecycleCallbacks回调接口监听所有activity的onDestory()的,在这个方法进行RefWatcher.watch对这个对象进行监控。

   具体是这样做的,封装成带key的弱引用对象,然后GC看弱引用对象有没有回收,没有回收的话就怀疑是泄漏了,需要二次确认。然后生成HPROF文件,分析这个快照文件有没有存在带这个key值的泄漏对象,如果没有,那么没有泄漏,否则找出最短路径,打印给我们,我们就能够找到这个泄漏对象了。

好了,一堆代码就总结成上面那句话了,那么扩展是啥呢?其实无非就是结果输出了,还记得一开始的初始化吗?把DisplayLeakService.class类作为参数传入,把DisplayLeakService是负责生成结果的,那么继承这玩意的父类不就可以自定义输出结果了吗,比如可以持续集成,然后出现泄漏后就发个邮件神马的,自动提单之类的,感谢开源~~~

leakcanary原理浅析(代码片段)

LeakCanary是Android内存泄漏的框架,作为一个“面试常见问题”它一定有值得学习的地方,今天就好好学习一下它。作为一名开发,我觉得给人讲框架或者库的原理,最好先把大概思路给读者讲一下,这样读者后面理解会按照这个... 查看详情

全新leakcanary2!完全基于kotlin重构升级!(代码片段)

大概一年以前,写过一篇LeakCanary源码解析,当时是基于1.5.4版本进行分析的。Square公司在今年四月份发布了全新的2.0版本,完全使用Kotlin进行重构,核心原理并没有太大变化,但是做了一定的性能优化。在本... 查看详情

leakcanary原理解析(代码片段)

转载注明出处:http://blog.csdn.net/xiaohanluo/article/details/78196755使用LeakCanary是Square为Android应用提供的一个监测内存泄露的工具,源码地址:https://github.com/square/leakcanary。在gradle文件中引入依赖:dependenciesdebugCompile'c... 查看详情

leakcanary原理解析(代码片段)

转载注明出处:http://blog.csdn.net/xiaohanluo/article/details/78196755使用LeakCanary是Square为Android应用提供的一个监测内存泄露的工具,源码地址:https://github.com/square/leakcanary。在gradle文件中引入依赖:dependenciesdebugCompile'c... 查看详情

全新leakcanary2!完全基于kotlin重构升级!(代码片段)

大概一年以前,写过一篇LeakCanary源码解析,当时是基于1.5.4版本进行分析的。Square公司在今年四月份发布了全新的2.0版本,完全使用Kotlin进行重构,核心原理并没有太大变化,但是做了一定的性能优化。在本... 查看详情

jmeter5.0核心源码浅析(代码片段)

【转自:https://blog.csdn.net/zuozewei/article/details/85042829】源码下载地址:https://github.com/apache/jmeter废话不多说,下面进入正题~一、源码结构1.工程目录 2.源码目录  3.源码分析运行机制  HashTree是JMeter执行测试依赖的... 查看详情

leakcanary原理分析(代码片段)

1、无代码集成原理目前最新版本是2.8.1,看文档从2.0版本集成时就不需要修改任何代码了,只需要在build.gradle里面添加一行引用:debugImplementation'com.squareup.leakcanary:leakcanary-android:2.xxxx'debugImplementation保证了只在de... 查看详情

gradle庖丁解牛(构建生命周期核心委托对象创建源码浅析)

...分析了Gradle框架自身初始化(非构建生命周期初始化)的核心流程,这一篇我们续着前面的分析继续(如果没看过前一篇的建议先去看前一篇,因为这一系列存在非常高的关联性)。上一篇说到当我们执行gradletaskName命令后 查看详情

resilience4j源码解析:浅析框架设计原理

前言弹性模式(Resiliencypatterns)SentinelVsHystrixVsResilience4j事件驱动小结相关文章:Resilience4j源码解析(1):简介及调试环境搭建Resilience4j源码解析(2):浅析框架设计原理Resilience4j 查看详情

android进阶——framework核心之dumpsys命令浅析(代码片段)

文章大纲引言一、Dumpsys命令概述二、Dumpsys命令语法详解1、Package信息查询2、Activity信息查询3、网络信息查询4、其他常用服务信息查询三、Dumpsys命令实现原理1、安卓binder服务管理2、Dumpsys源码分析引言Dumpsys是安卓系统提供用来... 查看详情

android进阶——framework核心之dumpsys命令浅析(代码片段)

文章大纲引言一、Dumpsys命令概述二、Dumpsys命令语法详解1、Package信息查询2、Activity信息查询3、网络信息查询4、其他常用服务信息查询三、Dumpsys命令实现原理1、安卓binder服务管理2、Dumpsys源码分析引言Dumpsys是安卓系统提供用来... 查看详情

leakcanary原理分析(代码片段)

LeakCanary原理分析LeakCanary的初始化LeakCanary2.3的引入:debugImplementation'com.squareup.leakcanary:leakcanary-android:2.3'2.3版本无需在Application中做额外操作。深入了解一下具体是如何初始化的,我的是2.3版本的,2.0以下版本应该和... 查看详情

leakcanary源码分析

基本使用LeakCanary是用来检测Android内存泄漏的工具。在gradlew文件中引入:dependenciesdebugCompile'com.squareup.leakcanary:leakcanary-android:1.5.4'releaseCompile'com.squareup.leakcanary:leakcanary-android-n 查看详情

androidclassloader浅析(代码片段)

前言最近在看Tinker的原理,发现核心是通过ClassLoader做的,由于之前也从未接触过ClassLoader趁着上周末看了安卓ClassLoader相关源码,这里分享一发安卓的ClassLoader和热更新的实现原理。ClassLoader首先我们要知道,程序... 查看详情

leakcanary原理分析(代码片段)

LeakCanary原理分析LeakCanary的初始化LeakCanary2.3的引入:debugImplementation'com.squareup.leakcanary:leakcanary-android:2.3'2.3版本无需在Application中做额外操作。深入了解一下具体是如何初始化的,我的是2.3版本的,2.0以下版本应该和... 查看详情

hashmap源码阅读笔记——hashmap的实现原理浅析

  在java8发布以前,HashMap的实现简单来说就是一个Node数组,通过hash算法尽可能的分散了元素的位置,当一个位置有超过一个元素时,用链表的形式将元素进行连接。在java8中HashMap的实现形式有了一些改动,其中比较重要的一... 查看详情

springboot自动配置原理浅析(代码片段)

springboot自动配置原理浅析springboot版本2.5.5注解@SpringBootApplication的源码:@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@SpringBootConfiguration@E 查看详情

threadpoolexecutor任务提交过程源码浅析

...过程,大体分为4个步骤:1)判断当前线程数量是否小于核心线程数量,如果小于则创建一个新的线程去执行该任务;2)如果线程数已经超过了核心线程数,那么就提交到等待工作队列(等待队列 查看详情