文章目录

    • 原理概述
    • 根本运用
    • 源码剖析
      • 1. 初始化
      • 2. 引证监控
        • 2.1 引证和GC
        • 2.2 监控
        • 2.3 总结
      • 3. dump目标及剖析
        • 3.1 dump目标
        • 3.2 目标剖析
    • 总结
    • 参阅


作为android进阶常识,性能优化不管是在社招面试仍是在日常工作中都是适当实用的常识,而且也是区分中级和高档程序员的试金石。

今天咱们先学习内存优化中的一个小常识点,便是内存走漏的检测和解决。当然,怎样解决是大多数中级工程师都要去学习的东西,网上也有很多的材料,所以我这儿不会详解。而是首要着眼于内存走漏的检测。Square公司出品的大名鼎鼎的LeakCanary,便是业界知名的内存走漏检测的利器。

【Android 日常学习】LeakCanary——面试官最爱问的性能优化工具,你知道它是怎么工作的吗?(源码分析)

阅览本篇文章,预计需要20分钟,你将会学习到:

  • LeakCanary检测内存走漏的原理
  • 运用ContentProvider进行三方库初始化的办法

原理概述

关于LeakCanary的原理,官网上现已给出了详细的解说。翻译过来便是:

  1. LeakCanary运用ObjectWatcher来监控Android的生命周期。当Activity和Fragment被destroy今后,这些引证被传给ObjectWatcher以WeakReference的形式引证着。假如gc完5秒钟今后这些引证还没有被清除去,那便是内存走漏了。
  2. 当被走漏掉的目标到达一个阈值,LeakCanary就会把java的堆栈信息dump到.hprof文件中。
  3. LeakCanary用Shark库来解析.hprof文件,找到无法被整理的引证的引证栈,然后再依据对Android体系的常识来判定是哪个实例导致的走漏。
  4. 经过走漏信息,LeakCanary会将一条完好的引证链缩减到一个小的引证链,其余的由于这个小的引证链导致的走漏链都会被聚合在一起。

经过官网的介绍,咱们很容易就抓住了学习LeakCanary这个库的要点:

  • LeakCanary是怎样运用ObjectWatcher 监控生命周期的?
  • LeakCanary怎样dump和剖析.hprof文件的?

看官方原理总是感觉不过瘾,下面咱们从代码层面上来剖析。本文依据LeakCanary 2.0 beta版。

根本运用

LeakCanary的运用适当的简单。只需要在module的build.gradle添加一行依靠,代码侵入少。

dependencies {
  // debugImplementation because LeakCanary should only run in debug builds.
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0-beta-3'
}

就这样,应用非常简单就接入了LeakCanary内存检测功能。当然还有一些更高档的用法,比如更改自定义config,添加监控项等,咱们能够参阅官网

源码剖析

1. 初始化

和之前的1.x版本相比,2.0甚至都不需要再在Application里边添加install的代码。可能很多的同学都会疑惑,LeakCanary是怎样刺进自己的初始化代码的呢? 其实这儿LeakCanary是运用了ContentProvider来进行初始化。我之前在介绍Android插件化系列三:技能流派和四大组件支持的时分曾经介绍过ContentProvider的特色,即在打包的过程中来自不同module的ContentProvider终究都会merge到一个文件中,启动app的时分ContentProvider是主动安装,而且安装会比Application的onCreate还早。LeakCanary便是依据这个原理进行的设计。详细能够参阅【译】你的Android库是否还在Application中初始化?

咱们能够检查LeakCanary源码,发现它在leakcanary-object-watcher-android的AndroidManifest.xml中有一个ContentProvider。

<provider
        android:name="leakcanary.internal.AppWatcherInstaller$MainProcess"
        android:authorities="${applicationId}.leakcanary-installer"
        android:exported="false"/>

然后咱们检查AppWatcherInstaller的代码,发现内部是运用InternalAppWatcher进行的install。

  // AppWatcherInstaller
override fun onCreate(): Boolean {
    val application = context!!.applicationContext as Application
    InternalAppWatcher.install(application)
    return true
}
  // InternalAppWatcher
fun install(application: Application) {
    // 省略部分代码
    checkMainThread()
    if (this::application.isInitialized) {
      return
    }
    InternalAppWatcher.application = application
    val configProvider = { AppWatcher.config }
    ActivityDestroyWatcher.install(application, objectWatcher, configProvider)
    FragmentDestroyWatcher.install(application, objectWatcher, configProvider)
    onAppWatcherInstalled(application)
}

能够看到这儿首要把Activity和Fragment区分了开来,然后别离进行注册。Activity的生命周期监听是借助于Application.ActivityLifecycleCallbacks。

private val lifecycleCallbacks =
    object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
      override fun onActivityDestroyed(activity: Activity) {
        if (configProvider().watchActivities) {
          objectWatcher.watch(activity)
        }
      }
    }
application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)

而Fragment的生命周期监听是借助了Activity的ActivityLifecycleCallbacks生命周期回调,当Activity创建的时分去调用FragmentManager.registerFragmentLifecycleCallbacks办法注册Fragment的生命周期监听。

    override fun onFragmentViewDestroyed(
      fm: FragmentManager,
      fragment: Fragment
    ) {
      val view = fragment.view
      if (view != null && configProvider().watchFragmentViews) {
        objectWatcher.watch(view)
      }
    }
    override fun onFragmentDestroyed(
      fm: FragmentManager,
      fragment: Fragment
    ) {
      if (configProvider().watchFragments) {
        objectWatcher.watch(fragment)
      }
    }
  }

终究,Activity和Fragment都将自己的引证传入了ObjectWatcher.watch()进行监控。从这儿开端进入到LeakCanary的引证监测逻辑。

题外话:LeakCanary 2.0版本和1.0版本相比,添加了Fragment的生命周期监听,每个类的责任也更加明晰。可是我个人觉得运用
(Activty)->Unit
这种lambda表达式作为类的写法不是很优雅,倒不如面向接口编程。完全能够设计成ActivityWatcher和FragmentWatcher都承继自某个接口,这样也方便后续扩展。

2. 引证监控

2.1 引证和GC

  1. 引证
    首要咱们先介绍一点预备常识。咱们都知道,java中存在四种引证:
  • 强引证:废物收回器绝不会收回它,当内存空间不足,Java虚拟机宁愿抛出OOM
  • 软引证:只要在内存不足的时分JVM才会收回仅有软引证指向的目标所占的空间
  • 弱引证:当JVM进行废物收回时,不管内存是否足够,都会收回仅被弱引证相关的目标。
  • 虚引证:和没有任何引证一样,在任何时分都可能被废物收回。

一个目标在被gc的时分,假如发现还有软引证(或弱引证,或虚引证)指向它,就会在收回目标之前,把这个引证加入到与之相关的引证行列(ReferenceQueue)中去。假如一个软引证(或弱引证,或虚引证)目标本身在引证行列中,就说明该引证目标所指向的目标被收回了。

当软引证(或弱引证,或虚引证)目标所指向的目标被收回了,那么这个引证目标本身就没有价值了,假如程序中存在很多的这类目标(注意,咱们创建的软引证、弱引证、虚引证目标本身是个强引证,不会主动被gc收回),就会糟蹋内存。因而咱们这就能够手动收回位于引证行列中的引证目标本身。

比如咱们常常看到这种用法

WeakReference<ArrayList> weakReference = new WeakReference<ArrayList>(list);

还有也有这样一种用法

WeakReference<ArrayList> weakReference = new WeakReference<ArrayList>(list, new ReferenceQueue<WeakReference<ArrayList>>());

这样就能够把目标和ReferenceQueue相关起来,进行目标是否gc的判断了。别的咱们从弱引证的特征中看到,弱引证是不会影响到这个目标是否被gc的,很合适用来监控目标的gc状况。
2. GC
java中有两种手动调用GC的方式。

System.gc();
// 或者
Runtime.getRuntime().gc();

2.2 监控

咱们在第一节中提到,Activity和Fragment都依靠于响应的LifecycleCallback来回调毁掉信息,然后调用了ObjectWatcher.watch添加了毁掉后的监控。接下来咱们看

ObjectWatcher.watch做了什么操作。
  @Synchronized fun watch(
    watchedObject: Any,
    name: String
  ) {
    removeWeaklyReachableObjects()
    val key = UUID.randomUUID().toString()
    val watchUptimeMillis = clock.uptimeMillis()
    val reference =
      KeyedWeakReference(watchedObject, key, name, watchUptimeMillis, queue)
    watchedObjects[key] = reference
    checkRetainedExecutor.execute {
      moveToRetained(key)
    }
  }
  private fun removeWeaklyReachableObjects() {
    // 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.
    var ref: KeyedWeakReference?
    do {
      ref = queue.poll() as KeyedWeakReference?
      if (ref != null) {
        watchedObjects.remove(ref.key)
      }
    } while (ref != null)
  }
  @Synchronized private fun moveToRetained(key: String) {
    removeWeaklyReachableObjects()
    val retainedRef = watchedObjects[key]
    if (retainedRef != null) {
      retainedRef.retainedUptimeMillis = clock.uptimeMillis()
      onObjectRetainedListeners.forEach { it.onObjectRetained() }
    }
  }

这儿咱们看到,有一个存储着KeyedWeakReference的ReferenceQueue目标。在每次添加watch object的时分,都会去把现已处于ReferenceQueue中的目标给从监控目标的map即watchObjects中整理掉,由于这些目标都现已被收回了。然后再去生成一个KeyedWeakReference,这个目标便是一个持有了key和监测开端时间的WeakReference目标。终究再去调用moveToRetained,适当于记载和回调给监控方这个目标正式开端监测的时间。

那么咱们现在现已拿到了需要监控的目标了,可是又是怎样去判断这个目标现已内存走漏的呢?这就要持续往下面看。咱们首要到前面在解说InternalAppWatcher的install办法的时分,除了install了Activity和Fragment的检测器,还调用了onAppWatcherInstalled(application)办法,看代码发现这个办法便是InternalLeakCanary的invoke办法。

  override fun invoke(application: Application) {
    this.application = application
    AppWatcher.objectWatcher.addOnObjectRetainedListener(this)
    val heapDumper = AndroidHeapDumper(application, leakDirectoryProvider)
    val gcTrigger = GcTrigger.Default
    val configProvider = { LeakCanary.config }
    val handlerThread = HandlerThread(LEAK_CANARY_THREAD_NAME)
    handlerThread.start()
    val backgroundHandler = Handler(handlerThread.looper)
    heapDumpTrigger = HeapDumpTrigger(
        application, backgroundHandler, AppWatcher.objectWatcher, gcTrigger, heapDumper,
        configProvider
    )
  }
  override fun onObjectRetained() {
    if (this::heapDumpTrigger.isInitialized) {
      heapDumpTrigger.onObjectRetained()
    }
  }

咱们看到首要是初始化了heapDumper,gcTrigger,heapDumpTrigger等目标用于gc和heapDump,一起还完成了OnObjectRetainedListener,并把自己添加到了上面的onObjectRetainedListeners中,以便每个目标moveToRetained的时分,InternalLeakCanary都能获取到onObjectRetained()的回调,回调里就只是回调了heapDumpTrigger.onObjectRetained()办法。看来都是依靠于HeapDumpTrigger这个类。
HeapDumpTrigger首要的处理逻辑都在checkRetainedObjects办法中。

  private fun checkRetainedObjects(reason: String) {
    val config = configProvider()
    var retainedReferenceCount = objectWatcher.retainedObjectCount
    if (retainedReferenceCount > 0) {
      gcTrigger.runGc()  // 触发一次GC操作,只保留不能被收回的目标
      retainedReferenceCount = objectWatcher.retainedObjectCount
    }
    if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return
    if (!config.dumpHeapWhenDebugging && DebuggerControl.isDebuggerAttached) {
      showRetainedCountWithDebuggerAttached(retainedReferenceCount)
      scheduleRetainedObjectCheck("debugger was attached", WAIT_FOR_DEBUG_MILLIS)
      return
    }
    val heapDumpUptimeMillis = SystemClock.uptimeMillis()
    KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis
    dismissRetainedCountNotification()
    val heapDumpFile = heapDumper.dumpHeap()
    if (heapDumpFile == null) {
      scheduleRetainedObjectCheck("failed to dump heap", WAIT_AFTER_DUMP_FAILED_MILLIS)
      showRetainedCountWithHeapDumpFailed(retainedReferenceCount)
      return
    }
    lastDisplayedRetainedObjectCount = 0
    objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)
    HeapAnalyzerService.runAnalysis(application, heapDumpFile)
  }

那么HeapDumpTrigger详细做了些啥呢?我理了一下首要是下面几个功能:

  • 后台线程轮询当前还存活着的目标
  • 假如存活的目标大于0,那就触发一次GC操作,收回掉没有走漏的目标
  • GC完后,仍然存活着的目标数和预定的目标数相比较,假如多了就调用heapDumper.dumpHeap()办法把目标dump成文件,并交给HeapAnalyzerService去剖析
  • 依据存活状况展现告诉

2.3 总结

看到了这儿,咱们应该脑海中有概念了。Activity和Fragment经过注册体系的监听在onDestroy的时分把自己的引证放入ObjectWatcher进行监测,监测首要是经过HeapDumpTrigger类轮询进行,首要是调用AndroidHeapDumper来dump出文件来,然后依靠于HeapAnalyzerService来进行剖析。后面一末节,咱们将会聚焦于目标dump操作和HeapAnalyzerService的剖析过程。

3. dump目标及剖析

3.1 dump目标

hprof是JDK供给的一种JVM TI Agent native东西。JVM TI,全拼是JVM Tool interface,是JVM供给的一套标准的C/C++编程接口,是完成Debugger、Profiler、Monitor、Thread Analyser等东西的一致基础,在主流Java虚拟机中都有完成。hprof东西事实上也是完成了这套接口,能够认为是一套简单的profiler agent东西。

用过Android Studio Profiler东西的同学对hprof文件都不会生疏,当咱们运用Memory Profiler东西的Dump Java heap图标的时分,profiler东西就会去捕获你的内存分配状况。可是捕获今后,只要在Memory Profiler正在运行的时分咱们才能检查,那么咱们要怎样样去保存当时的内存运用状况呢,又或者我想用别的东西来剖析堆分配状况呢,这时分hprof文件就派上用场了。Android Studio能够把这些目标给export到hprof文件中去。

LeakCanary也是运用的hprof文件进行目标存储。hprof文件比较简单,全体按照 前置信息 + 记载表的格局来组织的。可是记载的品种适当之多。详细品种能够检查HPROF Agent。

一起,android中也供给了一个简便的办法Debug.dumpHprofData(filePath)能够把目标dump到指定途径下的hprof文件中。LeakCanary运用运用Shark库来解析Hprof文件中的各种record,比较高效,运用Shark中的HprofReader和HprofWriter来进行读写解析,获取咱们需要的信息。咱们能够关注一些比较重要的,比如:

  • Class Dump
  • Instance Dump
  • Object Array Dump
  • Primitive Array Dump
    dump详细的代码在AndroidHeapDumper类中。HprofReader和HprofWriter过于杂乱,有爱好的直接检查源码吧
  override fun dumpHeap(): File? {
    val heapDumpFile = leakDirectoryProvider.newHeapDumpFile() ?: return null
    return try {
      Debug.dumpHprofData(heapDumpFile.absolutePath)
      if (heapDumpFile.length() == 0L) {
        null
      } else {
        heapDumpFile
      }
    } catch (e: Exception) {
      null
    } finally {
    }
  }

3.2 目标剖析

前面咱们现已剖析到了,HeapDumpTrigger首要是依靠于HeapAnalyzerService进行剖析。那么这个HeapAnalyzerService究竟有什么玄机?让咱们持续往下面看。能够看到HeapAnalyzerService其实是一个ForegroundService。在接收到剖析的Intent后就会调用HeapAnalyzer的analyze办法。所以终究进行剖析的当地便是HeapAnalyzer的analyze办法。
核心代码如下

    try {
      listener.onAnalysisProgress(PARSING_HEAP_DUMP)
      Hprof.open(heapDumpFile)
          .use { hprof ->
           // 1.生成graph
            val graph = HprofHeapGraph.indexHprof(hprof, proguardMapping)
            // 2.寻找Leak
            val findLeakInput = FindLeakInput(
                graph, leakFinders, referenceMatchers, computeRetainedHeapSize, objectInspectors
            )
            val (applicationLeaks, libraryLeaks) = findLeakInput.findLeaks()
            listener.onAnalysisProgress(REPORTING_HEAP_ANALYSIS)
            return HeapAnalysisSuccess(
                heapDumpFile, System.currentTimeMillis(), since(analysisStartNanoTime),
                applicationLeaks, libraryLeaks
            )
          }
    } catch (exception: Throwable) {
      listener.onAnalysisProgress(REPORTING_HEAP_ANALYSIS)
      return HeapAnalysisFailure(
          heapDumpFile, System.currentTimeMillis(), since(analysisStartNanoTime),
          HeapAnalysisException(exception)
      )
    }
  }

这段代码中涉及到了专为LeakCanary设计的Shark库的用法,在这儿就不多解说了。大约介绍一下每一步的作用:

  • 首要调用HprofHeapGraph.indexHprof办法,这个办法会把dump出来的各种实例instance,Class类目标和Array目标等都建立起查询的索引,以record的id作为key,把需要的信息都存储在Map中便于后续取用
  • 调用findLeakInput.findLeak办法,这个办法会从GC Root开端查询,找到最短的一条导致走漏的引证链,然后再依据这条引证链构建出LeakTrace。
  • 把查询出来的LeakTrace对外展现

总结

本篇文章剖析了LeakCanary检测内存走漏的思路和一些代码的设计思维,可是限于篇幅不能面面俱到。接下来咱们答复一下文章最初提出的问题。

  1. LeakCanary是怎样运用ObjectWatcher 监控生命周期的?
    LeakCanary运用了Application的ActivityLifecycleCallbacks和FragmentManager的FragmentLifecycleCallbacks办法进行Activity和Fragment的生命周期检测,当Activity和Fragment被回调onDestroy今后就会被ObjectWatcher生成KeyedReference来检测,然后借助HeapDumpTrigger的轮询和触发gc的操作找到弹出提示的机遇。
  2. LeakCanary怎样dump和剖析.hprof文件的?
    运用Android渠道自带的Debug.dumpHprofData办法获取到hprof文件,运用自建的Shark库进行解析,获取到LeakTrace

参阅

  • LeakCanary官网
  • HPROF Agent
  • 【译】你的Android库是否还在Application中初始化?