LeakCanary源码分析

394 阅读4分钟

相关文档

LeakCanary 仓库地址

1.x升级到2.x参考

关于内存、引用相关的前置知识

软引用与弱引用

  • 软引用SoftReference 对于只有软引用关联着的对象,只会在内存不足的时候JVM才会回收该对象。内存足够的时候是不会被回收的。 在系统将要发生内存溢出之前,会将这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出OOM
  • 弱引用WeakReference 比软引用要弱,当GC发生的时候,无论当前内存是否足够,都会回收掉制只被弱引用关联的对象。适用于引用偶尔被使用且不影响垃圾回收的对象

GC Roots

GC(Garbage Collector)管理的主要区域是Java堆,一般情况下只针对堆进行垃圾回收。方法区、栈和本地方法区不被GC所管理,因而选择这些区域内的对象作为GC roots,被GC roots引用的对象不被GC回收。GC会收集那些不是GC roots且没有被GC roots引用的对象。

Java中可以作为GC Roots的对象

  • 虚拟机栈(栈桢中的本地变量表)中的引用的对象
  • 方法区中的类静态属性引用的对象
  • 方法区中的常量引用的对象
  • 本地方法栈中JNI(Native方法)的引用的对象

GC可达性

引用队列

给弱引用关联一个引用队列,当弱引用持有内容被 gc 回收后,该弱引用会被添加到关联的引用队列中。 Talk is cheap ,show me your code

//下面的这个代码是对上面这句话的翻译
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
        Object object = new Object();
        WeakReference<Object> weakReference = new WeakReference<>(object, referenceQueue);
        System.out.println("object:" + object);
        System.out.println("weakReference:" + weakReference);
        System.out.println("weakReference.get():" + weakReference.get());
        System.out.println("referenceQueue.poll():" + referenceQueue.poll());
        object = null;
        System.gc();
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(" \nGC之后\n");
        System.out.println("object:" + object);
        System.out.println("weakReference:" + weakReference);
        System.out.println("weakReference.get():" + weakReference.get());
        //也就是弱引用被回收之后会放到引用队列,也就是某个对象被回收之后可以做一些自己的处理
        System.out.println("referenceQueue:" + referenceQueue.poll());
        
//执行结果:
object:java.lang.Object@2503dbd3
weakReference:java.lang.ref.WeakReference@4b67cf4d
weakReference.get():java.lang.Object@2503dbd3
referenceQueue.poll():null

GC之后

objectnull
weakReference:java.lang.ref.WeakReference@4b67cf4d
weakReference.get():null
referenceQueue:java.lang.ref.WeakReference@4b67cf4d

如何知道一个对象被回收呢?

当弱引用 WeakReference 所引用的对象A被回收后,这个 WeakReference 对象就会被添加到 ReferenceQueue 队列里,我们可以通过其 poll() 方法获取到这个被回收的对象的 WeakReference 实例,进而知道需要监控的对象A是否被回收了。

LeakCanary解析(1.5.1版本)

核心流程

  1. 创建RefWatcher,启动ActivityRefWatcher的watchActivities方法

  2. 通过ActivityLifecycleCallbacks监控Activity的onDestory方法,在Activity执行onDestory的时候,执行refWatcher的watch方法。该方法将这个Activity和随机的UUID以放到弱引用,这个弱引用关联一个引用队列。

  3. 开启线程池分析内存泄露。

  • 遍历引用队列 queue,判断队列中是否存在当前 Activity 的弱引用(有弱引用,说明Activity被回收了),存在则删除 retainedKeys(set集合) 中对应的引用的 key值。(也就是说弱引用不为空,说明队列中已经有泄露的Activity的弱引用了,那么就移除key,所以经过这一步,被回收的Actvity的对应的key是不存在的了)

  • 判断 retainedKeys 中是否包含当前 Activity 引用的 key 值。如果不包含,说明没有内存泄露

  • 调用GC,再遍历引用队列中是否有当前Activity的弱引用,有的话,则从retainedKeys移除该Activity对应的key

  • 再判断retainedKeys中是否还存在key,还存在这个key,说明key对应的Activity的弱引用还不在引用队列 queue中,也就反推这个key对应的Activity就泄露了。

  1. 有泄露,则dump内存,dump出来的路径为:/data/user/0/applicationId/files/leakcanary/2211a4cc-b8ca-4bad-b373-447d6383e354_pending.hprof,数字是随机数,(是内部存储路径)。开启HeapAnalyzerService走checkForLeak方法,这里会解析dump出来的hprof文件封装成Snapshot,再根据前面封装好的弱引用和key值确定泄露的对象。分析方法来自haha库
  2. 找出泄露对象的最短路径,作为结果反馈到LeakCanary中的DisplayLeakActivity上
  3. 分析完成后,删除dump出来的文件

部分类介绍

  • RefWatcher 监视Activity的泄露情况
  • ExcludedRefs(AndroidExcludedRefs) 过滤掉Android SDK导致的内存泄露问题 用于排除某些系统bug导致的内存泄露,可以自己定制
  • HeapAnalyzer 单独进程,分析堆转储文件,验证是否真的内存泄露
  • HeapAnalyzerService 分析泄露的服务
  • DisplayLeakService 这个服务主要用来分析内存泄漏结果并发送通知。你可以继承并重写这个类来进行一些自定义操作,比如上传分析结果
  • DisplayLeakActivity 点击通知栏跳转到泄露描述的页面

核心代码分析

初始化
LeakCanary.install(this)
Builder模式构建相关配置
 public static RefWatcher install(Application application) {
    return refWatcher(application).listenerServiceClass(DisplayLeakService.class)
        .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
        .buildAndInstall();
  }
  

  private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
      new Application.ActivityLifecycleCallbacks() {
        ...
        @Override public void onActivityDestroyed(Activity activity) {
        // 把每一个走了onDestory生命周期的Activity添加到Watcher进行内存泄露排查
         ActivityRefWatcher.this.onActivityDestroyed(activity);
        }
      };


  @SuppressWarnings("ReferenceEquality") // Explicitly checking for named null.
  Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
    //删除引用队列中的activity的key,以保证key集合里面的key都是未被回收的activity所关联的key
    removeWeaklyReachableReferences();
    // debug 调试状态下
    if (debuggerControl.isDebuggerAttached()) {
      // The debugger can create false leaks.
      return RETRY;
    }
    // 
    if (gone(reference)) {        
      return DONE;
    }
    // 此时还不能简单地判断是否泄露,可能存在某个对象已经不可达,但是尚未进入引用队列 queue 
    
    // GC
    gcTrigger.runGc();
    // 再把已经回收的activity的key给移除
    removeWeaklyReachableReferences();
    // 如果目前key的集合里面还存在某个Activity弱引用关联的key的话,那就判定这个Activitygone(reference)返回false,表示发生泄露
    if (!gone(reference)) {
      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;
  }
  
    // true表示retainedKeys.contains(reference.key)为false,即key不包含在里面
   ,即已经在之前的removeWeaklyReachableReferences方法中给移除了,从而说明这个弱引用没有泄露,那么false就表示泄露了
    private boolean gone(KeyedWeakReference reference) {
    return !retainedKeys.contains(reference.key);
  }

// 被回收的activiy的key会被移除,那么剩下来的key对应的activity就是可能泄露的
 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);
    }
  }
  
查找泄露
1、把.hpfor转为Snapshot
2、优化gcroots
3、找出泄露的对象/找出泄露对象的最短路径
  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);
      // 这里用的是haha库的分析方法
      Snapshot snapshot = parser.parse();
      // 这里是去重,减小内存压力,里面才用了[THashMap](https://github.com/palantir/trove-3.0.3/blob/master/src/gnu/trove/map/hash/THashMap.java)
      deduplicateGcRoots(snapshot);

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

findLeakingReference
1、在snapshot快照中找到第一个弱引用
2、遍历这个对象的所有实例
3、如果key值和最开始定义封装的key相同,那么返回这个泄露对象
 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);
  }


找到内存泄露最短引用路径
 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));
  }



流程图

2.x与1.x的区别

  • 2.x全部使用使用kotlin语言,1.x是java语言
  • 2.x不需要写代码进行初始化,是在ContentProvider中执行的
  • 支持 fragment,支持 androidx
  • 当泄露引用到达 5 个时才会发起 heap dump
  • 全新的 heap parser 1.x用的haha库分析leak trace;2.x用shark组件

值得学习的参考点

  • UUID.randomUUID().toString();来保证key的唯一性
  • CopyOnWriteArraySet来存储key,来实现线程安全,解决并发读写问题
  • THashMap减少内存
  • Looper.myQueue().addIdleHandler,监听线程空闲,只有当消息队列没有消息时或者是队列中的消息还没有到执行时间时才会执行的 IdleHandler
  • android:enabled="false"属性 Service和Activity默认都是android:enabled="false",这也就是为什么在未发生内存泄露前,桌面是无LeakCanary图标的原因
   <service android:name="com.squareup.leakcanary.internal.HeapAnalyzerService" android:enabled="false" android:process=":leakcanary" />
    <service android:name="com.squareup.leakcanary.DisplayLeakService" android:enabled="false" />
    <activity android:theme="resourceId:0x7f0c015b" android:label="Leaks" android:icon="res/drawable-xxxhdpi-v4/leak_canary_icon.png" android:name="com.squareup.leakcanary.internal.DisplayLeakActivity" android:enabled="false" android:taskAffinity="com.squareup.leakcanary.com.keding.leakcanarydemo">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
    <activity android:theme="resourceId:0x7f0c015c" android:label="Storage permission" android:icon="res/drawable-xxxhdpi-v4/leak_canary_icon.png" android:name="com.squareup.leakcanary.internal.RequestStoragePermissionActivity" android:enabled="false" android:taskAffinity="com.squareup.leakcanary.com.keding.leakcanarydemo" />

内存优化的一些小点

  • 使用合适的数据结构,比如key,value形式的数据,假如key是整形,那么用SparseArray替换HashMap,它避免了自动装箱的过程
  • 避免使用枚举,枚举占用内存较大,具体原因。可以用@StringDef、@IntDef来代替枚举
  • 处理Bitmap,根据控件尺寸,看需求是否需要透明通道,不需要的话,Bitmap配置用RGB_565格式;也可以用.9图,过大的图片分段加载
  • BitmapFactory Option中有个inSampleSize可以来压缩宽高;inJustDecodeBounds只用来计算图片宽高,不会把bitmap加载到内存,图片缓存池
  • 及时关闭IO资源,cursor,属性动画cancle,rxjava取消订阅
  • 谨慎的使用多进程,每个进程会预加载一些资源,哪怕没什么代码也会占用几M内存
  • 使用NDK来减小内存
  • 检查单例的写法(Context需要传application的context),Handler的写法, 内部类都写成静态内部类,防止持有外部类的引用
  • 懒加载
  • 对于一些有显示隐藏判断的UI,尝试用ViewStub和默认Gone的方式,不可见的View是不会被加载到内存中的
  • 谨慎使用第三方框架,最好看看源码,防止有坑

内存优化5R法则(来自腾讯工程师胡凯的分享)

  • Reduce缩减:降低图片分辨率、重采样、抽稀策略
  • Reuse 复用:池化策略/避免频繁重复创建对象,减小GC压力
  • Recycle 回收:主动销毁、结束,避免内存泄露;生命周期闭环
  • Refactory 重构:更合适的数据结构、更合理的程序架构
  • Revalue 重审:谨慎使用Large Heap、多进程、三方库