瞎侃内存泄露库leakcanary源码

假设你已经体验过leakcanary了,没有体验过的话花五分钟去体验一下。我的上篇文章
《五分钟体验内存泄露检测LeakCanary》
http://www.jianshu.com/p/51395d8e512f
简单来说,leakcanary就是一个库,app可以利用这个库来检测内存泄露,内存泄露了会在状态栏上通知你。这篇文章就简单说下源码吧,因为源码太简单了,我也不打算多讲。

总体流程

整个过程就是在应用启动时,构建一个观察者,再注册Activity的回调,在Activity销毁的时候,通过这个观察者来观察是否有内存泄露。流程图就是下面这个,简单的不行。

流程图.png

代码流程也说下啦,不然不像程序员了
在LeakCanary中调用install

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

其中这里的refWatcher(application)是个函数,不是类,你见过类是小写开头的吗?这个函数也在LeakCanary中

  public static AndroidRefWatcherBuilder refWatcher(Context context) {
    return new AndroidRefWatcherBuilder(context);
  }

所以这个refWatcher(application)返回的是一个构造者AndroidRefWatcherBuilder,他要构造谁?还用说吗,当然是构造观察者啦,利用buildAndInstall()函数来构造观察者

  public RefWatcher buildAndInstall() {
    RefWatcher refWatcher = build();
    ......
    .......
    return refWatcher;
  }

利用了build()函数来构造,看看罗,先屏住呼吸,这个函数稍微长那么一点点,不过也很简单,偷懒直接看他返回值就好,哈哈哈哈,返回RefWatcher对象,RefWatcher是何方神圣?这个RefWatcher对象厉害了,他就是传说中的观察者,拥有火眼金睛,专门观察内存泄露的,历史会记住他的。

  public final RefWatcher build() {
    if (isDisabled()) {
      return RefWatcher.DISABLED;
    }

    ExcludedRefs excludedRefs = this.excludedRefs;
    if (excludedRefs == null) {
      excludedRefs = defaultExcludedRefs();
    }

    HeapDump.Listener heapDumpListener = this.heapDumpListener;
    if (heapDumpListener == null) {
      heapDumpListener = defaultHeapDumpListener();
    }

    DebuggerControl debuggerControl = this.debuggerControl;
    if (debuggerControl == null) {
      debuggerControl = defaultDebuggerControl();
    }

    HeapDumper heapDumper = this.heapDumper;
    if (heapDumper == null) {
      heapDumper = defaultHeapDumper();
    }

    WatchExecutor watchExecutor = this.watchExecutor;
    if (watchExecutor == null) {
      watchExecutor = defaultWatchExecutor();
    }

    GcTrigger gcTrigger = this.gcTrigger;
    if (gcTrigger == null) {
      gcTrigger = defaultGcTrigger();
    }

    return new RefWatcher(watchExecutor, debuggerControl, gcTrigger, heapDumper, heapDumpListener,
        excludedRefs);
  }

那看看观察者RefWatcher是怎么构造的,既然它这么鬼重要

    return new RefWatcher(watchExecutor, debuggerControl, gcTrigger, heapDumper, heapDumpListener,
        excludedRefs);

可以看到他用了好几个东西构造呢

watchExecutor 这玩意是个线程控制器,控制activity销毁后5s再去观察泄露情况
debuggerControl 这玩意是控制debugger的,没多大作用,不想鸟他
gcTrigger 这玩意用来触发垃圾回收的,上面的线程控制器5s后观察有泄露,不算泄露,必须垃圾回收后,在去观察一次。所以最多会观察两次。第一次是5s后观察,第二次是5s后再垃圾回收后观察。
heapDumpListener hprof文件解释完后,会告诉这个监听者。这个监听者就会更新状态栏
excludedRefs 这玩意是做额外处理的,这里面定义了一些类,如果是这些类泄露了,不会提示的。例如我就是想我的Activity泄露,你别管我,那你可以把类名加到excludedRefs 中。

上面这些类差不多就包含了库里面的所有java文件了,我截个图你看,他妈其实这个库没多少文件。所以说代码不再多,没bug就行。

java文件.png

再看看这个源码项目目录,让自己有个大概概念,基本上就是由两个库组成嘛,一个是java基础库,一个是Android定制库啦。Android定制库的很多类都是java基础库的子类,例如基础库的构造者叫RefWatcherBuilder,Android定制库的构造者叫AndroidRefWatcherBuilder ,这都是很顺其自然的东西啦。

目录结构
基础库的java文件.png

妈的,好像有点跑题了。

上面说到构造了观察者RefWatcher,然后为了做到RefWatcher在activity销毁的时候去观察泄露,要先注册Activity的回调,还是在buildAndInstall函数。

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

build()函数构造了观察者了,然后下面一句ActivityRefWatcher.installOnIcsPlus((Application) context, refWatcher);就是注册回调。

  public static void installOnIcsPlus(Application application, RefWatcher refWatcher) {
    ....
    activityRefWatcher.watchActivities();
    ....
  }

进入 activityRefWatcher.watchActivities();

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

终于看到回调函数注册了耶,看看回调是怎么处理的拉,天,最后就是在activity的ondestory里面利用观察者去观察泄露啦refWatcher.watch(activity);

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

  void onActivityDestroyed(Activity activity) {
    refWatcher.watch(activity); //观察泄露
  }

我嚓,废了好大劲终于观察者refWatcher可以去观察了。下面看看他妹的怎么观察的吧。

refWatcher观察泄露原理

原理上节说过了,就是利用弱引用WeakReference和引用序列ReferenceQueue的原理实现的。
在activity对象ondestory的时候,新建一个WeakReference对象指向activity对象,如果activity对象被垃圾回收的话,WeakReference对象就会进入引用序列ReferenceQueue。所以,我们只需要在activity对象ondestory后去查看ReferenceQueue序列是否有该WeakReference对象即可。
第一次观察是activity destory 5秒后,观察,如果发现ReferenceQueue序列还没有WeakReference对象,就进入第二次观察,如果有了,就说明没有泄露,结束观察。
第二次观察,跟第一次相比,区别在于会先进行垃圾回收,再去观察ReferenceQueue序列情况。

好了,又要开始看源码了,god
在RefWatcher中的watch函数切入

  public void watch(Object watchedReference, String referenceName) {
    String key = UUID.randomUUID().toString();
    retainedKeys.add(key);
    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue);
    ensureGoneAsync(watchStartNanoTime, reference);
  }

首先用一个key来标记这个指向activity的WeakReference对象,key就相当于这弱引用的名字嘛,例如这个弱引用的名字叫“林黛玉”,林黛玉弱弱的。然后把这个名字key放在一个容器retainedKeys中。
final KeyedWeakReference reference =
new KeyedWeakReference(watchedReference, key, referenceName, queue);
这一句就是把弱引用跟引用序列关联在一起啦。然后过5s,读一下引用序列,看一下还有没有这个弱引用

  private void removeWeaklyReachableReferences() {
    KeyedWeakReference ref;
    while ((ref = (KeyedWeakReference) queue.poll()) != null) {
      retainedKeys.remove(ref.key);
    }
  }

queue.poll()就是从引用序列去拿弱引用啦,拿到弱引用后,就读弱引用名字ref.key,如果这个名字在容器retainedKeys中,就把名字从容器去掉。例如读到“林黛玉”,就把林黛玉的名字移除出retainedKeys。最后看retainedKeys有没有“林黛玉”,就知道林黛玉有没有泄露了。gone()函数返回true就是没有泄露。

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

整的实现就在一个函数ensureGone中

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

    removeWeaklyReachableReferences(); //查看弱引用序列,清理容器retainedKeys

    if (debuggerControl.isDebuggerAttached()) {
      // The debugger can create false leaks.
      return RETRY;
    }
    if (gone(reference)) {   //容器没有“林黛玉”,说明没有内存泄漏,结束观察
      return DONE;
    }

    gcTrigger.runGc();   //能走到这里,说明容器还有林黛玉,先垃圾回收一些
    removeWeaklyReachableReferences();
    if (!gone(reference)) {        //说明林黛玉泄露了
      long startDumpHeap = System.nanoTime();
      long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);

      File heapDumpFile = heapDumper.dumpHeap();  //导出hprof文件
      if (heapDumpFile == RETRY_LATER) {
        // Could not dump the heap.
        Log.i("wenfeng","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));  //解释hprof文件
    }
    return DONE;
  }

最后林黛玉泄露后,会开启一个服务去解释内存堆文件,解释结果会返回给heapdumpListener。

堆文件.png

堆文件也有保存在本地,pend代表正在解释的文件,其他是解释完的了。
这个文件的保存路径是由DefaultLeakDirectoryProvider决定的,他就是首先看sdcard有没有权限,有权限就把hprof文件保存到sdcard,没有就保存到内存卡。逻辑太简单了,自己看去。
至于怎么解释hprof文件,哎,又是个大课题,先不说了,我都消化不良了。原理反正就是利用HAHA库去解释,传入弱引用到HAHA库,然后库就去查找这个弱引用对象的最短gcroot路径,然后打印出来,大概这样。有空再聊。

上面说5s后开始观察泄露,是怎么实现的?

哎,其实是在ensureGoneAsync中实现的拉,光看名字就知道是异步的拉

  private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
    watchExecutor.execute(new Retryable() {
      @Override public Retryable.Result run() {
        Log.i("wenfeng","ensureGone");
        return ensureGone(reference, watchStartNanoTime);

      }
    });
  }

就是有个执行者执行任务,这个执行者就是AndroidWatchExecutor,最后就会在postDelayed中利用子线程去观察泄露啦。可以看到这个还有失败重试机制呢。失败越多,延时就越长。具体你们自己看源码,不细说了。

  private void postToBackgroundWithDelay(final Retryable retryable, final int failedAttempts) {
    long exponentialBackoffFactor = (long) Math.min(Math.pow(2, failedAttempts), maxBackoffFactor);
    long delayMillis = initialDelayMillis * exponentialBackoffFactor;
    backgroundHandler.postDelayed(new Runnable() {
      @Override public void run() {
        Retryable.Result result = retryable.run();
        if (result == RETRY) {
          postWaitForIdle(retryable, failedAttempts + 1);
        }
      }
    }, delayMillis);
  }

我天,写了这么多字,吓到我了。一般我都是写几百字的,特别在这个快餐时代。其实没必要写这么多,你看看人家google的android视频,都是5分钟之内的,比你一小时来的强。

IT文库 » 瞎侃内存泄露库leakcanary源码
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址