内存泄露(八)-- LeakCanary

KOOM线上APM监控最全剖析

LeakCanary源码分析: 一文让你彻底理解LeakCanary的工作原理

目录

    • 一、LeakCanary核心原理
    • 二、为什么LeakCanary不能用于线上?
    • 三、LeakCanary的使用

一、LeakCanary核心原理

内存泄露(八)-- LeakCanary_第1张图片

    1. Activity Destroy之后将它放在一个WeakReference
    1. 这个WeakReference关联到一个ReferenceQueue
    1. 查看ReferenceQueue是否存在Activity的引用
    1. 如果该Activity泄漏了,Dump出heap信息,然后再去分析泄漏路径。
private final Set retainedKeys;
private final ReferenceQueue queue;

final class KeyedWeakReference extends WeakReference {
  public final String key;
  public final String name;
  ...
}
 // 为Activity生成一个对应的key
String key = UUID.randomUUID().toString();
// 将这个Activity对应的key添加到集合retainedKeys中
retainedKeys.add(key);
 // 核心代码,创建一个弱引用,指向这个Activity并且指定一个引用队列
final KeyedWeakReference reference =
    new KeyedWeakReference(watchedReference, key, referenceName, queue);

其中KeyedWeakReference继承WeakReference,该弱引用的key跟retainedKeys集合的key相同,都是UUID.randomUUID().toString()。

 
  

如何判定是否发生泄漏,主要源码解析:
RefWatcher.java

private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
  // 支线代码: watchExecutor的实现是AndroidWatchExecutor
  watchExecutor.execute(new Retryable() {
    @Override public Retryable.Result run() {
      // 检测Activity是否被回收
      return ensureGone(reference, watchStartNanoTime);
    }
  });
}

// 核心代码
Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
  // 计算时间差提示给开发
  long gcStartNanoTime = System.nanoTime();
  long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);

  // 尝试移除已经被回收的Activity对应的key(因为代码跑到这里可能已经gc过了)
  removeWeaklyReachableReferences();

  // 检测Activity是否已经被回收(key被移除了就是被回收了)
  if (gone(reference)) {
    return DONE;
  }

  // 如果没有被回收,尝试进行一次gc(这就是我们上面说的必要的时候,后面有细讲)
  gcTrigger.runGc();

  // gc之后再进行一次移除
  removeWeaklyReachableReferences();

  // 如果Activity还没有被回收,说明发生了泄漏
  if (!gone(reference)) {
    long startDumpHeap = System.nanoTime();
    long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);

    // 抓取堆信息并生成文件
    File heapDumpFile = heapDumper.dumpHeap();
    if (heapDumpFile == RETRY_LATER) {
      return RETRY;
    }
    long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
    // 对泄漏结果进行分析并通知给相应的服务,然后就会弹出一个通知告诉我们发生了泄漏
    heapdumpListener.analyze(
        new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
            gcDurationMs, heapDumpDurationMs));
  }
  return DONE;
}

// 检测Activity是否已经被回收,只要Activity对应的key不在了,就说明已经回收了
private boolean gone(KeyedWeakReference reference) {
  return !retainedKeys.contains(reference.key);
}

// 移除所有已经被回收的对象,被回收了就移除activity对应的key
private void removeWeaklyReachableReferences() {
  KeyedWeakReference ref;
  // 遍历引用队列,同时移除该弱引用指向的Activity的key
  while ((ref = (KeyedWeakReference) queue.poll()) != null) {
    retainedKeys.remove(ref.key);
  }
}

  可以看到,首先,我们将检测的代码逻辑丢到watchExecutor来执行(watchExecutor其实是个AndroidWatchExecutor,用来切换线程),当我们的检测逻辑运行时,大概率已经发生过gc了(这是watchExecutor的功劳),所以我们尝试去清除一次activity的key队列,然后检测被destroy的activity是否已经被回收,如果没有被回收,也不一定发生了泄漏,因为可能还没有进行过gc,所以我们手动进行了一次gc,然后再次检测该activity 对应的key是否还在key队列,如果还在,那么就说明发生了泄漏,就直接dump堆空间以及相关信息,并提示给开发者。

  还记得我们前面为Activity生成的key吗,当这个Activity被回收后,指向它的弱引用就会被放入引用队列queue中,所以当我们检测到queue中有这个引用时,就说明该Activity已经被回收了,就从retainedKeys队列移除这个key。所以,当一个Activity被destroy之后,就先把它对应的key添加到retainedKeys队列中,等到gc之后,再检测retainedKeys这个队列,如果对应的key还在,就说明发生了内存泄漏。

ActivityRefWatcher如何监视Activity

    1. 首先会创建一个refwatcher,启动一个ActivityRefWatcher。
      ActivityRefWatcher用于监听Activity的回收情况
    1. 通过ActivityLifecycleCallbacks把Activity的ondestroy生命周期与ActivityRefWatcher关联
    1. 最后在线程池中去开始分析我们的泄漏

总结checkForLeak

    1. 把 .hprof 转为 Snapshot

SnapShot对象包含了所有对象引用的所有路径,就可以查找到所需要的内存泄漏路径

    1. 优化gcroots
      通过deduplicationGcRoots()方法会对分析的结果进行去重,把重复的内存泄漏以及gcroot进行删除
    1. 找出泄漏的对象/找出泄漏对象的最短路径
      findLeakingReference():找出泄漏的对象
      总结findLeakingReference
      (1)在snapshot快照中找到第一个弱引用
      (2)遍历这个对象的所有实例
      (3)如果key值和最开始定义封装的key值相同,那么返回这个泄漏对象

      findLeakTrace(): 找出泄漏对象的最短路径
      LeakCanary里面的GC Roots只关注两种类型:
      ① 静态的
      ② 这个对象被其他线程使用,并且其他线程正在运行没有结束

      findLeakingReference()和findLeakTrace()这两个方法的主要功能总结:
      (1)解析hprof文件,把这个文件封装成snapshot
      (2)根据弱引用和前面定义的key值,确定泄漏的对象
      (3)找到最短泄漏路径,作为结果反馈出来

  当一个Activity被销毁之后,Leakcanary会在onDestory方法中进行2次GC(为啥要多次GC,其实是因为一次GC并不能保证对象被回收),如果熟悉JVM的伙伴应该知道,只要涉及到GC,极大的概率会触发STW,那么这个时候就会卡顿。
  LeakCanary监控泄漏利用了弱引用的特性,为Activity创建弱引用,当Activity对象变成弱可达时(没有强引用),弱引用会被加入到引用队列中,通过在Activity.OnDestroy()后连续触发两次GC,并检查引用队列,可以判定Activity是否发生了泄漏。但频繁的GC会造成用户可感知的卡顿,所以Koom采用了无性能损耗的内存阈值监控来触发镜像采集。
Koom
内存泄露(八)-- LeakCanary_第2张图片

二、为什么LeakCanary不能用于线上?

    1. LeakCanary通过弱引用关联对象并主动触发GC随后休眠100ms判定是否泄漏,可能造成频繁GC易导致程序卡顿;
    1. 每次检测到泄漏都会dump快照文件,同一个泄漏多次触发也会dump多份快照;
    1. dump hprof文件(内存镜像)耗时,易造成程序长时间无响应;
    1. hprof文件太大,解析与回捞耗时。

三、LeakCanary的使用

添加LeakCanary依赖包

https://github.com/square/leakcanary

在主模块app下的build.gradle下添加如下依赖:

debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3.1'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1'

内存泄露(八)-- LeakCanary_第3张图片
开启LeakCanary

  1. 添加Application子类
      首先创建一个ExampleApplication,该类继承于Application,在该类的onCreate方法中添加如下代码开启LeakCanary监控:
LeakCanary.install(this);

内存泄露(八)-- LeakCanary_第4张图片
2. 在配置文件中注册ExampleApplication
  在AndroidManifest.xml中的application标签中添加如下信息:

android:name=".ExampleApplication"

内存泄露(八)-- LeakCanary_第5张图片
  这个时候安装应用到手机,会自动安装一个Leaks应用,如下图:
内存泄露(八)-- LeakCanary_第6张图片

制造一个内存泄漏的点
  建立一个ActivityManager类,单例模式,里面有一个数组用来保存Activity:

package com.example.android.sunshine.app;
import android.app.Activity;
import android.util.SparseArray;
import android.view.animation.AccelerateInterpolator;
import java.util.List;
public class ActivityManager {
    private SparseArray container = new SparseArray();
    private int key = 0;
    private static ActivityManager mInstance;
    private ActivityManager(){}
    public static ActivityManager getInstance(){
        if(mInstance == null){
            mInstance = new ActivityManager();
        }
        return mInstance;
    }

    public void addActivity(Activity activity){
        container.put(key++,activity);
    }
}

  然后在DetailActivity中的onCreate方法中将当前activity添加到ActivityManager的数组中:

 	@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_detail);
        ActivityManager.getInstance().addActivity(this);
        if (savedInstanceState == null) {
            // Create the detail fragment and add it to the activity
            // using a fragment transaction.

            Bundle arguments = new Bundle();
            arguments.putParcelable(DetailFragment.DETAIL_URI, getIntent().getData());

            DetailFragment fragment = new DetailFragment();
            fragment.setArguments(arguments);

            getSupportFragmentManager().beginTransaction()
                    .add(R.id.weather_detail_container, fragment)
                    .commit();
        }
    }

  我们从首页跳转到详情页的时候会进入DetailActivity的onCreate的方法,然后就将当前activity添加到了数组中,当返回时,我们没把他从数组中删除。再次进入的时候,会创建新的activity 并添加到数组中,但是之前的activity仍然被引用,无法释放,但是这个activity不会再被使用,这个时候就造成了内存泄漏。我们来看看LeakCanary是如何报出这个问题的。

演示:
内存泄露(八)-- LeakCanary_第7张图片
  解析的过程有点耗时,所以需要等待一会才会在Leaks应用中,当我们点开某一个信息时,会看到详细的泄漏信息:
内存泄露(八)-- LeakCanary_第8张图片

你可能感兴趣的:(内存泄漏,Android,性能优化,内存泄漏,Android,性能优化)