LeakCanary 解析

151 阅读10分钟

一 ,前言

本文主要分三个部分,先简单讲述内存泄漏的原理和部分JVM知识,再讨论LeakCanary利用弱引用特性检测对象是否存活,最后分析源码流程

二, 内存泄漏原理

LeakCanary是个使用起来非常简单的检测内存泄漏的工具,在解读LeakCanary是如何工作的之前必须对内存泄漏有一定的认识。

内存泄漏是指程序向系统申请一块内存,使用完毕后没有及时清理,导致这块内存一致被占据,直到程序结束。

简单的说内存泄漏就是 使用完毕的对象应该被回收,但是由于某些原因 无法被回收

内存泄漏不会直接对程序产生影响,当程序长时间运行,无法回收的对象越来越多,内存中可用空间越来越少,任意位置都可能会引发OutOfMemoryError 异常。

(1)JVM垃圾回收算法

Android应用也是运行在Java虚拟机上,所以有必要了解Java虚拟机的内存管理知识。

区别于C,C++程序员需要创建对象时需要手动开辟,回收内存。Java虚拟机提供内存管理机制,帮助程序员管理内存。

回收一块内存的前提是,如何判断这个内存 或 对象已经不使用了,可以回收了。必须有相应的机制,不可能这个对象正在使用呢,好端端的就被回收了。

Java虚拟机使用的垃圾回收算法叫做:可达性算法,当对象与GC Roots没有任何引用链相连,正则此对象无用可以回收

在可达性算法中,引入了一个概念叫做GC Roots,包括以下几种:

(1)在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。

(2)在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。

(3)在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。 (4)在本地方法栈中JNI(即通常所说的Native方法)引用的对象。

(5)Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。

(6)所有被同步锁(synchronized关键字)持有的对象。 ·反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

(虚拟机栈,本地方法栈,方法区等概念需要了解Java虚拟机内存模型)

偷一张图

Untitled.png

(2)举例分析

Java虚拟机或 Android中内存泄漏的场景本质上说是生命周期长的对象持有生命周期短的对象,从而导致生命周期短的对象不能被释放

内存泄漏最容易出现情况就是 使用静态变量引用对象,静态变量与应用的生命周期一致。

比如:

(1)在Android中把Activity的引用赋值给某个静态变量,正常情况下Activity被关闭,调用过onDestroy()后,Activity占用的内存应该被释放,但是由于被静态变量持有引用。

根据可达性算法,静态变量是GC Roots,当虚拟机进行GC操作是Activity不会被回收

(2)使用静态变量持有业务数据时也要注意,如果数据量过大则会一直占用内存,要根据业务及时清理

(3)这也是单例模式容易引起内存泄漏的原因,静态变量比Activity生命周期长

Handler作为内部类引起的Activity的内存泄漏也是一种常见情况,要结合源码分析

(1)Java内部类的特性会自动持有外部类引用

(2)当Handler发送message时,message内部持有Handler的引用,目的是当消息被处理完成后,通知Handler

(3)这就构成一个引用链,message-handler-Activity

(4)message执行是有点延迟的,线程消息队列可能有其他消息先执行,或者message本身就是一个延时消息。message执行时Activity可能早被关闭,但是内部对象引用链仍然存在,造成内存泄漏。如果在handler回调中进行UI操作,那么直接就奔溃了。

(5)Handler比Activity的生命周期长

(3)小结

内存泄漏就是 使用完毕的对象应该被回收,但是由于某些原因 无法被回收,一直在内存中直到程序结束

Java虚拟机或 Android中内存泄漏的场景本质上说是生命周期长的对象持有生命周期短的对象,从而导致生命周期短的对象不能被释放

LeakCanary工作原理

LeakCanary自动检测Activity,Fragment,View,ViewModel,Service对象的泄漏。

以Activity为例:

Activity调用onDestory()后,Activity可以被回收,如果Activity被其他GC Roots持有引用,就无法被虚拟机回收。

LeakCanary利用弱引用WeakReference 的特性,检测Activity在onDestory()之后的GC是否被正常回收。

WeakReference 弱引用,当一个对象没有被强引用指向,只有一个弱引用指向,当GC运行时不管内存是否充足都会被回收。

被回收后对象被添加到队列当中,通过判断GC后队列是否有值来确定对象是否被正常回收。

伪代码如下:

val queue = ReferenceQueue<Any>()
val weak = WeakReference<Any>("xxx", queue)
System.gc()
if (queue.remove() != null) {
   //对象已被回收
} else {
   //没有被回收
}

套用在Activity,在Activity.onDestory()会调用,LeakCanary将Activity引用添加到WeakReference 主动调用GC回收,遍历队列,如果队列中没有对象 或 数量不对则证明有对象没有被正常回收,表明可能存在内存泄漏的情况。

三,LeakCanary源码

debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1’

(1)2.0以后leakCanary不需要在Application中手动初始化

leakcanary源码是多模块项目,在 leakcanary-object-watcher-android 模块中定义ContentProvider 的子类 MainProcessAppWatcherInstaller 中自动初始化。

ContextProvider的初始化由系统调用,在Application的attachBaseContext之后,在onCreate之前。利用系统特性完成自动初始化的效果。

leakCanary核心初始化代码 AppWatcher.manualInstall(application)

(2)AppWatcher.manualInstall() 方法

初始化方法,首先要理解是 watchersToInstall 参数 ,类型是InstallableWatcher接口集合,接口声明install(),uninstall() 方法

默认调用appDefaultWatchers() 方法为参数添加默认值:ActivityWatcherFragmentAndViewModelWatcherRootViewWatcherServiceWatcher

见名知意,他们负责观察Activity,fragment,viewmodel,view,service是否内存溢出

在初始化时调用install() 开始工作,接下来看看ActivityWatcher 是如何实现的

fun manualInstall(
    application: Application,
    retainedDelayMillis: Long = TimeUnit.SECONDS.toMillis(5),
    watchersToInstall: List<InstallableWatcher> = appDefaultWatchers(application)
  ) {
		//省略代码
    LeakCanaryDelegate.loadLeakCanary(application)

    watchersToInstall.forEach {
      it.install()
    }
		//省略代码
  }

fun appDefaultWatchers(
    application: Application,
    reachabilityWatcher: ReachabilityWatcher = objectWatcher
  ): List<InstallableWatcher> {
    return listOf(
      ActivityWatcher(application, reachabilityWatcher),
      FragmentAndViewModelWatcher(application, reachabilityWatcher),
      RootViewWatcher(reachabilityWatcher),
      ServiceWatcher(reachabilityWatcher)
    )
  }

interface InstallableWatcher {

  fun install()

  fun uninstall()
}

(3)ActivityWatcher

用于监听Activity生命周期,添加到观察队列中,实现还是比较简单的,核心利用Application.ActivityLifecycleCallbacks 监听应用内所有Activity的生命周期,在onActivityDestroyed 回调中 调用ReachabilityWatcher.expectWeaklyReachable 对Activity进行观测。

install() 和 uninstall() 方法则是对 回调的注册与解绑

class ActivityWatcher(
  private val application: Application,
  private val reachabilityWatcher: ReachabilityWatcher
) : InstallableWatcher {

  private val lifecycleCallbacks =
    object : Application.ActivityLifecycleCallbacks bynoOpDelegate() {
      override fun onActivityDestroyed(activity: Activity) {
        reachabilityWatcher.expectWeaklyReachable(
          activity, "${activity::class.java.name} received Activity#onDestroy() callback"
        )
      }
    }

  override fun install() {
    application.registerActivityLifecycleCallbacks(lifecycleCallbacks)
  }

  override fun uninstall() {
    application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks)
  }
}

(4)ReachabilityWatcher

接口类,唯一实现类是,ObjectWatcher ,检测对象是否被回收的核心逻辑在ObjectWatcher 中。

核心逻辑在 expectWeaklyReachable 方法

参数watchedObject 需要观察是否被GC回收的对象,

使用UUID.randomUUID()watchedObject 分配唯一key

使用watchedObject 创建弱引对象KeyedWeakReference ,继承自WeakReference

添加到UUID 和 弱引用 作为key-value 添加到map,记录当前正在被观察的对象

根据弱引用的特性,被垃圾回收的对象会添加到引用队列中,遍历引用队列根据key移除map中的值

一轮操作后,发现map中仍然有对象,说明可能发生内存泄漏,通过接口OnObjectRetainedListener 通知 InternalLeakCanary 对象,最终调用到HeapDumpTrigger.scheduleRetainedObjectCheck() 方法,

fun interface ReachabilityWatcher {

  /**
   * Expects the provided [watchedObject] to become weakly reachable soon. If not,
   * [watchedObject] will be considered retained.
   */
  fun expectWeaklyReachable(
    watchedObject: Any,
    description: String
  )
}

class ObjectWatcher{

@Synchronized override fun expectWeaklyReachable(
    watchedObject: Any,
    description: String
  ) {
    if (!isEnabled()) {
      return
    }
    removeWeaklyReachableObjects()
		// UUID随机数 定义 key 作为watchedObject的唯一标志
    val key = UUID.randomUUID()
      .toString()
    val watchUptimeMillis = clock.uptimeMillis()
		//创建弱引用对象 KeyedWeakReference 继承WeakReference,
		//利用弱引用特性设置 引用队列queue,对象被回收会添加到queue中
    val reference =
      KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
    SharkLog.d {
      "Watching " +
        (if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.javaClass.name}") +
        (if (description.isNotEmpty()) " ($description)" else "") +
        " with key $key"
    }
		//watchedObjects是 map,通过键值对保存 KeyedWeakReference实例
    watchedObjects[key] = reference
		//开启异步任务,处理对象是否被回收的逻辑
    checkRetainedExecutor.execute {
      moveToRetained(key)
    }
  }

@Synchronized private fun moveToRetained(key: String) {
		//根据方法内注释,在垃圾回收实际开始之前就会把可以回收的对象加入队列当中
		//遍历队列的同时,移除map中的元素
    removeWeaklyReachableObjects()
		//根据key 获取map保存的 软引用
    val retainedRef = watchedObjects[key]
		//如果不等于null 有值 说明没有被垃圾回收
    if (retainedRef != null) {
      retainedRef.retainedUptimeMillis = clock.uptimeMillis()
			//通过监听 通知其他组件 有对象没有被释放 仍然在内存中
      onObjectRetainedListeners.forEach { it.onObjectRetained() }
    }
  }
	
	
  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)
  }

}

(5)HeapDumpTrigger

接上一步,当发现应用内存在未被回收的对象,进行二次检查,经过检查后仍然存在未被回收的对象,则生成.hprof 文件,逻辑如下:

(a)调用objectWatcher.retainedObjectCount属性,retainedObjectCount属性重写get方法,内部调用再次调用removeWeaklyReachableObjects() ,检查引用队列

(b)如果队列数量大于零 手动触发GC,进行垃圾回收后再次检查引用队列,获取对象存活数量

(c)调用checkRetainedCount() 方法,判断是否还有剩余的监听对象存活,如果app在前台展示,存活的个数是否超过阈值(默认5),如果超过则进行下一步逻辑开始生成.hprof 文件。如果app在后台则,一段间隔时间后就开始生成.hprof 文件

(d)存活对象数量判断后,进入dumpHeap() 逻辑并不复杂。收集数据,.hprof 文件信息,文件UUID ,时间戳 打包成HeapDump 事件,通过接口发送事件到子线程。

(e)发送事件的调用链 InternalLeakCanary.sendEvent(event: Event)LeakCanary.Config.eventListenerseventListeners 是个集合,默认保存多个实现类。处理HeapDump 事件的类有三个:RemoteWorkManagerHeapAnalyzerWorkManagerHeapAnalyzerBackgroundThreadHeapAnalyzer 。实现不同,但最终都是调用AndroidDebugHeapAnalyzer.runAnalysisBlocking()方法

class HeapDumpTrigger {

	private fun checkRetainedObjects() {
		//省略代码
		//调用objectWatcher.retainedObjectCount属性,retainedObjectCount属性重写get方法,
		//内部调用再次调用removeWeaklyReachableObjects() ,检查引用队列
	  var retainedReferenceCount = objectWatcher.retainedObjectCount
	
	  if (retainedReferenceCount > 0) {
	    gcTrigger.runGc()
	    retainedReferenceCount = objectWatcher.retainedObjectCount
	  }
	
	  if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return
	
	
	  val visibility = if (applicationVisible) "visible" else "not visible"
	  dumpHeap(
	    retainedReferenceCount = retainedReferenceCount,
	    retry = true,
	    reason = "$retainedReferenceCount retained objects, app is $visibility"
	  )
	}

}

private fun dumpHeap(
    retainedReferenceCount: Int,
    retry: Boolean,
    reason: String
  ) {
    val directoryProvider =
      InternalLeakCanary.createLeakDirectoryProvider(InternalLeakCanary.application)
    //创建文件
		val heapDumpFile = directoryProvider.newHeapDumpFile()
		//获取UUID
    val durationMillis: Long
    if (currentEventUniqueId == null) {
      currentEventUniqueId = UUID.randomUUID().toString()
    }
		//发送事件
    try {
      InternalLeakCanary.sendEvent(HeapDump(currentEventUniqueId!!, heapDumpFile, durationMillis, reason))
    } catch (throwable: Throwable) {
      InternalLeakCanary.sendEvent(HeapDumpFailed(currentEventUniqueId!!, throwable, retry))

      return
    }
  }
//继承Event
class HeapDump(
      uniqueId: String,
      val file: File,
      val durationMillis: Long,
      val reason: String
    ) : Event(uniqueId)

class ObjectWatch{
	
	val retainedObjectCount: Int
    @Synchronized get() {
      removeWeaklyReachableObjects()
      return watchedObjects.count { it.value.retainedUptimeMillis != -1L }
    }
}

(6)AndroidDebugHeapAnalyzer

核心方法runAnalysisBlocking 在当前线程进行堆分析,分析结束后发送事件EventListener.Event.HeapAnalysisDone 通知其他组件。

方法调用链如下:

(a) AndroidDebugHeapAnalyzer.runAnalysisBlocking()

(b)AndroidDebugHeapAnalyzer.analyzeHeap()

(c)HeapAnalyzer.analyze() 在此方法进入 shark组件分析hprof文件,在heap dump中搜索泄漏实例,计算这些实例到GC Roots的最短引用路径。分析hprof文件还有更复杂的调用,能力有限就不继续讲解了。

(d)分析结束后,返回事件对象HeapAnalysisDone ,根据对象中包含的信息,类型调用不同的回调逻辑,通知调用链InternalLeakCanary.sendEvent(event: Event)LeakCanary.Config.eventListeners 。 在eventListeners 中没有很多对HeapAnalysisDone 处理的逻辑,开发者可以调用LeakCanary.Config.eventListeners 设置自己的监听。

fun runAnalysisBlocking(
    heapDumped: HeapDump,
    isCanceled: () -> Boolean = { false },
    progressEventListener: (HeapAnalysisProgress) -> Unit
  ): HeapAnalysisDone<*> {
    val progressListener = OnAnalysisProgressListener { step ->
      val percent = (step.ordinal * 1.0) / OnAnalysisProgressListener.Step.values().size
      progressEventListener(HeapAnalysisProgress(heapDumped.uniqueId, step, percent))
    }

    val heapDumpFile = heapDumped.file
    val heapDumpDurationMillis = heapDumped.durationMillis
    val heapDumpReason = heapDumped.reason

    val heapAnalysis = if (heapDumpFile.exists()) {
      analyzeHeap(heapDumpFile, progressListener, isCanceled)
    } else {
      missingFileFailure(heapDumpFile)
    }

    val fullHeapAnalysis = when (heapAnalysis) {
      is HeapAnalysisSuccess -> heapAnalysis.copy(
        dumpDurationMillis = heapDumpDurationMillis,
        metadata = heapAnalysis.metadata + ("Heap dump reason" to heapDumpReason)
      )
      is HeapAnalysisFailure -> {
        val failureCause = heapAnalysis.exception.cause!!
        if (failureCause is OutOfMemoryError) {
          heapAnalysis.copy(
            dumpDurationMillis = heapDumpDurationMillis,
            exception = HeapAnalysisException(
              RuntimeException(
                """
              Not enough memory to analyze heap. You can:
              - Kill the app then restart the analysis from the LeakCanary activity.
              - Increase the memory available to your debug app with largeHeap=true: https://developer.android.com/guide/topics/manifest/application-element#largeHeap
              - Set up LeakCanary to run in a separate process: https://square.github.io/leakcanary/recipes/#running-the-leakcanary-analysis-in-a-separate-process
              - Download the heap dump from the LeakCanary activity then run the analysis from your computer with shark-cli: https://square.github.io/leakcanary/shark/#shark-cli
            """.trimIndent(), failureCause
              )
            )
          )
        } else {
          heapAnalysis.copy(dumpDurationMillis = heapDumpDurationMillis)
        }
      }
    }
    progressListener.onAnalysisProgress(REPORTING_HEAP_ANALYSIS)

    val analysisDoneEvent = ScopedLeaksDb.writableDatabase(application) { db ->
      val id = HeapAnalysisTable.insert(db, heapAnalysis)
      when (fullHeapAnalysis) {
        is HeapAnalysisSuccess -> {
          val showIntent = LeakActivity.createSuccessIntent(application, id)
          val leakSignatures = fullHeapAnalysis.allLeaks.map { it.signature }.toSet()
          val leakSignatureStatuses = LeakTable.retrieveLeakReadStatuses(db, leakSignatures)
          val unreadLeakSignatures = leakSignatureStatuses.filter { (_, read) ->
            !read
          }.keys
            // keys returns LinkedHashMap$LinkedKeySet which isn't Serializable
            .toSet()
          HeapAnalysisSucceeded(
            heapDumped.uniqueId,
            fullHeapAnalysis,
            unreadLeakSignatures,
            showIntent
          )
        }
        is HeapAnalysisFailure -> {
          val showIntent = LeakActivity.createFailureIntent(application, id)
          HeapAnalysisFailed(heapDumped.uniqueId, fullHeapAnalysis, showIntent)
        }
      }
    }
    LeakCanary.config.onHeapAnalyzedListener.onHeapAnalyzed(fullHeapAnalysis)
    return analysisDoneEvent
  }

object BackgroundThreadHeapAnalyzer : EventListener {

  internal val heapAnalyzerThreadHandler by lazy {
    val handlerThread = HandlerThread("HeapAnalyzer")
    handlerThread.start()
    Handler(handlerThread.looper)
  }

  override fun onEvent(event: Event) {
    if (event is HeapDump) {
      heapAnalyzerThreadHandler.post {
        val doneEvent = AndroidDebugHeapAnalyzer.runAnalysisBlocking(event) { event ->
          InternalLeakCanary.sendEvent(event)
        }
        InternalLeakCanary.sendEvent(doneEvent)
      }
    }
  }
}

四,参考

全解系列:内存泄漏定位工具LeakCanary

全新 LeakCanary 2 ! 完全基于 Kotlin 重构升级 !

为什么使用LeakCanary检测内存泄漏?

Android内存泄漏检测之LeakCanary2.0(Kotlin版)的实现原理