LeakCanary详解

LeakCanary简介

leakCanary是square公司推出的一个用于检测内存泄漏的工具,在一个activity完全ondestroy方法执行时,我们都会希望它的内存空间能够完全被回收。但是实际上并非是这样的,往往可能会出现内存泄漏,比如说这个activity被其他没有回收的类所持有引用。那么就会造成ondestory方法执行完之后,这个activity没有被回收,造成内存泄漏。所以,为了让我们在开发测试时更容易的发现内存泄漏的情况,square公司推出了这个开源库。

LeakCanary流程

LeakCanary的使用

对于一个app引入leakCanary这个库是非常的简单的,只需要在gradle文件中引入下面几个依赖,这几个依赖会针对不同的编译版本引入不同的包:

dependencies {
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
   testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
 }

然后在代码中,我们只需要在我们项目中的application类去调用install方法建立LeakCanary监测就行了。

  if (LeakCanary.isInAnalyzerProcess(this)) {
        //这个是在分析heap的进程中,监测进程不能和分析进程在同一个。
    return;
    }
    LeakCanary.install(this);

LeakCanary的简单实现流程

在我们在Application类中调用了LeakCanary的install方法之后,就开始整个activity的监测流程。

  1. 对application注册了一个关于activity生命周期的回调callBack,在这个回调里面,LeakCanary只关注onDestroy方法,毕竟我们需要监测内存泄漏,肯定是从onDestroy方法开始的。
  2. 一旦一个activity执行了ondestroy方法,也就是这个activity需要被销毁时,此时就开始了LeakCanary的工作,会在callBack里面执行watch方法,也就是开始监测了这个activity被销毁的过程。
  3. 在watch方法里面,会生成一个带有自定义key值的弱引用,LeakCanary通过这个弱引用来监测是否被完全回收,对于android里面的弱引用的回收,当弱引用指向的对象被gc回收时,此时会将这个弱引用添加到一个对应的队列里面。
  4. 假如watch监测出来可能出现内存溢出,此时就会利用一个叫做haha的库,这个库是回来解析heapDump文件的,然后通过这个文件分析是否真正的出现了内存溢出。出现溢出,生成溢出堆栈信息。

LeakCanary的代码实现:

RefWatcher的内部实现

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

在我们在application调用了install方法时,将会通过builder方法创建RefWatcher。

  • DisplayLeakService是一个服务类,主要是用来分析泄漏的结果
  • AndroidExcludedRefs排除android系统的bug 我们先看buildAndInstall的实现
 public RefWatcher buildAndInstall() {
    ...
    //创建RefWatcher实例
    RefWatcher refWatcher = build();
    if (refWatcher != DISABLED) {
      if (watchActivities) {
        //开启Activity监听
        ActivityRefWatcher.install(context, refWatcher);
      }
      if (watchFragments) {
        //开启Fragment监听
        FragmentRefWatcher.Helper.install(context, refWatcher);
      }
    }
    LeakCanaryInternals.installedRefWatcher = refWatcher;
    return refWatcher;
  }

在这个方法中,会创建出RefWatcher,同时开启Activity和Fragment的泄漏的监听。
首先,我们先看看如何开启Activity的监听。

public static void install(Context context, RefWatcher refWatcher) {
    Application application = (Application) context.getApplicationContext();
    ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);

    application.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks);
  }

其中,activityRefWatcher.lifecycleCallbacks为

  private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
      new ActivityLifecycleCallbacksAdapter() {
        @Override public void onActivityDestroyed(Activity activity) {
          refWatcher.watch(activity);
        }
      };

所以,对于Activity的监听,其实就是对application注册了LifeCycleCallback,在每一个watcheActivity的时候,都会移除上一次的注册。

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

  public void stopWatchingActivities() {
    application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks);
  }

最后在onActivityDestroyed的回调,执行watch操作,进行内存泄漏的检测。
其实fragment和activity的监听其实是非常类似的,只不过在onDestory回调到时,调用了RefWatcher的watchFragments方法

 private final Application.ActivityLifecycleCallbacks activityLifecycleCallbacks =
        new ActivityLifecycleCallbacksAdapter() {
          @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
            for (FragmentRefWatcher watcher : fragmentRefWatchers) {
              watcher.watchFragments(activity);
            }
          }
        };

在这个watchFragments中,注册了fragment的监听。

@Override public void watchFragments(Activity activity) {
    if (activity instanceof FragmentActivity) {
      FragmentManager supportFragmentManager =
          ((FragmentActivity) activity).getSupportFragmentManager();
      supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true);
    }
  }

这个fragmentLifecycleCallbacks是fragment的lifeCycleCallback

private final FragmentManager.FragmentLifecycleCallbacks fragmentLifecycleCallbacks =
      new FragmentManager.FragmentLifecycleCallbacks() {
        @Override public void onFragmentDestroyed(FragmentManager fm, Fragment fragment) {
          refWatcher.watch(fragment);
        }
      };

这样,也就是完成了对于fragment的监听,在onFragmentDestroyed调用时,也会调用watch方法进行检测内存泄漏,只不过和activity不一样的是,在这里传入的fragment实例。

对于RefWatcher,这个类是LeakCanary的核心。我们可以先看看其中的一些成员变量

  //执行内存泄漏的检测
  private final WatchExecutor watchExecutor;
  //用于查询是否在查询中
  private final DebuggerControl debuggerControl;
  //处理Gc,执行gc操作
  private final GcTrigger gcTrigger;
  //Dump内存泄漏的堆文件
  private final HeapDumper heapDumper;
  private final HeapDump.Listener heapdumpListener;
  private final HeapDump.Builder heapDumpBuilder;
  //产生内存泄漏的key
  private final Set<String> retainedKeys;
  //引用队列
  private final ReferenceQueue<Object> queue;

在catch方法执行检测

//这个方法会在任意一个activity的ondestroy方法调用时调用
public void watch(Object watchedReference, String referenceName) {
    //通过这个Disable参数可以仅在debug包中开启。
    if (this == DISABLED) {
      return;
    }
    //引用和引用名称不能为空
    checkNotNull(watchedReference, "watchedReference");
    checkNotNull(referenceName, "referenceName");
    //记录监测的开始时间
    final long watchStartNanoTime = System.nanoTime();
    //生成一个随机的key,保证每次生成的弱引用的key都是不同的
    String key = UUID.randomUUID().toString();
    //添加到维护的key数组里面去,这是一个写时复制的数组,这个数组是用来记录LeakCanary生成的弱引用所对应的key,也就是记录监测的activity的key
    retainedKeys.add(key);
    //在这里生成弱引用,这个是LeakCanary自定义的,添加了key和引用名称,这个queue便是弱引用指向的对象被正确回收时会被添加到的队列。
    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue);
    //开始监测这个引用的回收
    ensureGoneAsync(watchStartNanoTime, reference);
  }

上面的方法,除了最后一句话,其他的都是在做一些初始化操作而已,ensureGoneAsync这个方法才是执行的监测的入口,这个是一个同步方法,它会利用watchExecutor去顺序的执行每一个弱引用的回收确认:

private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
    //excutor监测池,持续监测这个弱引用是否被回收
    watchExecutor.execute(new Retryable() {
      @Override public Retryable.Result run() {
        return ensureGone(reference, watchStartNanoTime);
      }
    });
  }

通过了run方法的执行,开始通过ensureGone方法去确认每一个弱引用的回收,在阅读这个关键的方法时,我们需要先来了解两个方法removeWeaklyReachableReferences和isGone:

private void removeWeaklyReachableReferences() {
    //这个会发生在finalization和垃圾收集器之前
    KeyedWeakReference ref;
    while ((ref = (KeyedWeakReference) queue.poll()) != null) {
      retainedKeys.remove(ref.key);
    }
  }
=====>
public Reference<? extends T> poll() {
        synchronized (lock) {
            //queue的头为空,表示没有弱引用所指向的对象被回收
            if (head == null)
                return null;

            return reallyPollLocked();
        }
    }
=====>
private Reference<? extends T> reallyPollLocked() {
        if (head != null) {
            //获取当前head头
            Reference<? extends T> r = head;
            //头等于尾,清空
            if (head == tail) {
                tail = null;
                head = null;
            } else {
                获取下一个元素
                head = head.queueNext;
            }
            //设置queueNext属性,标记一个引用有进队或者出队操作
            r.queueNext = sQueueNextUnenqueued;
            return r;
        }

        return null;
    }

这个removeWeaklyReachableReferences方法会从前面指定的队列去获取是否存在被回收的弱引用,假如存在,则把我们保存的key数组中移除这个被成功回收的key值,剩下的就是可能存在泄漏的弱引用的key值,知道了这个,下面看看isGone的判断就比较简单了

private boolean gone(KeyedWeakReference reference) {
    return !retainedKeys.contains(reference.key);
  }

只需要判断retainedKeys数组中是否包含该引用的key就能知道有没有被回收了。下面来看看这这个ensureGone方法,看看是如何确认有没有造成内存泄漏的

Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
    //通过回收队列的引用去移除我们的key数组
    removeWeaklyReachableReferences();
    
    if (debuggerControl.isDebuggerAttached()) {
      return RETRY;
    }
    //如果被标记的弱引用均已被回收,那么返回。
    if (gone(reference)) {
      return DONE;
    }
    //在这里进行二次确认,手动执行一次gc
    gcTrigger.runGc();
    //在gc完成之后,再此通过queue移除我们的key数组。
    removeWeaklyReachableReferences();
    if (!gone(reference)) {
      //仍然存在没被回收的弱引用,此时可能造成内存泄漏,我们需要通过heapDump文件来分析
      long startDumpHeap = System.nanoTime();
      long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
      //根据现在内存,生成dump文件
      File heapDumpFile = heapDumper.dumpHeap();
      if (heapDumpFile == RETRY_LATER) {
        //目前不能读取,待重试状态
        return RETRY;
      }
      long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
      //heapdumpListener分析这个heapDump文件,判断是否泄漏。
      heapdumpListener.analyze(
          new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
              gcDurationMs, heapDumpDurationMs));
    }
    return DONE;
  }

当有可能造成内存泄漏的时候,会通过heap内存文件分析是否真正的出现了内存泄漏,会将这个文件的分析转交给分析类去处理,而Watcher在此时就完成了自己的工作。
所以,watcher类主要做的事情就是在每一个ondestory调用时定义一个相对应的弱引用,并在gc后分析queue和key数组,判断是否可能造成泄漏,在有可能造成泄漏的时候,将dump文件的分析转交给其他类去处理。

Dump

我们看看dump文件分析的主要代码,首先watcher会通过listener将dump文件转交给监听,下面是这个listener的调用链:

heapdumpListener.analyze(
          new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
              gcDurationMs, heapDumpDurationMs));
public void analyze(HeapDump heapDump) {
        Preconditions.checkNotNull(heapDump, "heapDump");
        HeapAnalyzerService.runAnalysis(this.context, heapDump, this.listenerServiceClass);
    }

HeapAnalyzeService是一个IntentService,它有自己的工作线程,在这个工作线程执行完毕之后,会关闭自身service。

public static void runAnalysis(Context context, HeapDump heapDump, Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
        Intent intent = new Intent(context, HeapAnalyzerService.class);
        intent.putExtra("listener_class_extra", listenerServiceClass.getName());
        intent.putExtra("heapdump_extra", heapDump);
        context.startService(intent);
    }

工作线程的执行,

protected void onHandleIntent(Intent intent) {
        if(intent == null) {
            CanaryLog.d("HeapAnalyzerService received a null intent, ignoring.", new Object[0]);
        } else {
            String listenerClassName = intent.getStringExtra("listener_class_extra");
            HeapDump heapDump = (HeapDump)intent.getSerializableExtra("heapdump_extra");
            //将dump文件的分析交给了HeapAnalyzer类
            HeapAnalyzer heapAnalyzer = new HeapAnalyzer(heapDump.excludedRefs);
            //获取泄漏的结果。
            AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey);
            AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
        }
    }

通过着listener会开启一个IntentService,intentService与普通的service最主要的区别就是拥有自己的工作线程,而且待工作线程执行完毕之后,会自己关闭service,不用我们去手动进行控制。在HeapAnalyzerService中,又通过HeapAnalyzer去分析弱引用。我们来看看checkForLeak这个方法:

public AnalysisResult checkForLeak(File heapDumpFile, String referenceKey) {
      ...
      //heap文件是.hprof后缀格式的
      HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
      //获取解析器,这个解析器库haha所提供的
      HprofParser parser = new HprofParser(buffer);
      //生成此时的内存快照
      Snapshot snapshot = parser.parse();
      
      //复制gc的根对象,这个root是用来判断可达性的,从root到我们的弱引用所指向的对象,
      deduplicateGcRoots(snapshot);

      //寻找泄漏的引用
      Instance leakingRef = findLeakingReference(referenceKey, snapshot);

      if (leakingRef == null) {
        return noLeak(since(analysisStartNanoTime));
      }

      return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef);
    }

首先会生成内存快照,然后通过此时的快照来获取所有的根节点,通过这个根节点来判断弱引用指向对象的可达性。我们看看寻找泄漏引用的逻辑:

private Instance findLeakingReference(String key, Snapshot snapshot) {
    //通过自定义弱引用对象的name获取该对象
    ClassObj refClass = snapshot.findClass(KeyedWeakReference.class.getName());
    List<String> keysFound = new ArrayList<>();
    //获取当前的实例列表
    for (Instance instance : refClass.getInstancesList()) {
      //获取实例的参数域
      List<ClassInstance.FieldValue> values = classInstanceValues(instance);
      //获取key值
      String keyCandidate = asString(fieldValue(values, "key"));
      if (keyCandidate.equals(key)) {
        //如果需要寻找的key和获取出来的对象的key值一致,表示寻找到了这个弱引用,即有泄漏。
        return fieldValue(values, "referent");
      }
      keysFound.add(keyCandidate);
    }
    throw new IllegalStateException(
        "Could not find weak reference with key " + key + " in " + keysFound);
  }

如果返回的不为空,那么返回寻找到的泄漏的弱引用对象。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,928评论 6 509
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,748评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,282评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,065评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,101评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,855评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,521评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,414评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,931评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,053评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,191评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,873评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,529评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,074评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,188评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,491评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,173评论 2 357

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,213评论 25 707
  • 面试必背 会舍弃、总结概括——根据我这些年面试和看面试题搜集过来的知识点汇总而来 建议根据我的写的面试应对思路中的...
    luoyangzk阅读 6,756评论 6 173
  • Android 内存管理的目的 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。简单粗...
    晨光光阅读 1,294评论 1 4
  • 天使班3.0第三阶段践行目标 1:早起早睡:6:00-6:30起冥想或大拜,晚10:30之前入睡,睡前关机,读书半...
    渔歌子_3c34阅读 170评论 0 0
  • 清晨与傍晚 老狗与提岸 坐在故乡的三月 冬天的灭绝 之后,是乍暖还寒 异乡的城市 春天和冬天一样寒冷 没有雨和雪 ...
    鹿原先生和蓬蒿阅读 158评论 0 4