Android 之 LeakCanary 相关知识总结

背景

学习总结一下 LeakCanary 相关的知识。

接入方式

比较简单,只需要引入下面的依赖就可以了。无需增加其他代码。

内部会通过 MainProcessAppWatcherInstaller 实现自动初始化 LeakCanary 的逻辑。

arduino 复制代码
dependencies {
  // debugImplementation because LeakCanary should only run in debug builds.
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:3.0-alpha-1'
}

代码结构

版本:3.0-SNAPSHOT

各模块功能

  • leakcanary
    • leakcanary-android:集成入口模块,提供 LeakCanary 安装,公开 API 等能力。
    • leakcanary-android-core:核心代码,包括显示内存泄漏信息的页面,通知等代码。
    • leakcanary-android-release:专门给 release 的版本用的。
  • object-watcher
    • object-watcher:观察者相关代码,核心类为 ObjectWatcher 和 KeyedWeakReference。
    • object-watcher-android、object-watcher-androidx、object-watcher-android-core:各种支持自动分析内存泄漏的对象的 watcher 对象。例如 ActivityWatcher 和 ServiceWatcher。
    • object-watcher-android-startup:启动自动初始化逻辑,AppWatcherStartupInitializer。
  • shark:各种分析模块的父 Module
    • shark-android:分析安卓设备,包括型号安卓版本等信息
    • shark-cli:命令行工具,使用命令行调用 shark 的相关功能进行内存分析。
    • shark-graph:分析堆中对象的关系图。
    • shark-hprof:解析 hprof 文件。
    • shark-log:日志模块。
  • plumber
    • plumber-android:自动修复工具,对于已知的内存泄漏问题,尝试在进行时进行自动修复(例如通过反射直接将其置空)。

整体流程

  • 主要了解一下初始化的逻辑,MainProcessAppWatcherInstaller。
  • 检测前:在什么时候开始检测。InstallableWatcher 实现类,例如 ActivityWatcher。
  • 检测中:检测的依据是什么,ObjectWatcher。
  • 检测后:主要是 dump 出内存快照文件并进行分析。

初始化

默认情况下,导入 leakCanary 库就行了,不需要开发者进行手动初始化。

MainProcessAppWatcherInstaller

通过 ContentProvider,实现自动初始化。

  • 默认情况下,AndroidManifest 中注册的 ContentProvider 创建和初始化逻辑,会在 application 启动的时候执行
  • AppWatcherInstaller 继承 ContentProvider 并实现 onCreate 方法,在 AndroidManifest 文件中注册
  • 在 onCreate 中调用 AppWatcher.manualInstall() 执行 LeakCanary 的初始化逻辑。

AppWatcher.manualInstall

  • 方法中的 watchersToInstall 参数,默认值是 appDefaultWatchers,返回了四个默认的 Watcher,分别负责监控对应类的实例。
  • 遍历调用四个 watcher 的 install 方法,执行相应的初始化逻辑。
  • ActivityWatcher、FragmentAndViewModelWatcher、RootViewWatcher、ServiceWatcher。

InternalLeakCanary

LeakCanary 启动初始化后的核心逻辑。

kotlin 复制代码
override fun invoke(application: Application) {
    _application = application

    // 检查是否运行在Debuggable Build中,用于开发时检测内存泄漏
    checkRunningInDebuggableBuild()

    // 向AppWatcher的对象监视器添加一个监听器,用于监测对象的保留情况
    AppWatcher.objectWatcher.addOnObjectRetainedListener(this)

    // 创建垃圾回收触发器
    val gcTrigger = GcTrigger.Default

    // 获取配置提供者
    val configProvider = { LeakCanary.config }

    // 创建处理线程
    val handlerThread = HandlerThread(LEAK_CANARY_THREAD_NAME)
    handlerThread.start()
    val backgroundHandler = Handler(handlerThread.looper)

    // 创建HeapDumpTrigger实例,用于触发堆转储操作
    heapDumpTrigger = HeapDumpTrigger(
        application, backgroundHandler, AppWatcher.objectWatcher, gcTrigger, configProvider
    )

    // 注册应用程序可见性监听器,用于在应用程序可见性更改时触发堆转储
    application.registerVisibilityListener { applicationVisible ->
                                            this.applicationVisible = applicationVisible
                                            heapDumpTrigger.onApplicationVisibilityChanged(applicationVisible)
                                           }

    // 注册应用程序已恢复的活动监听器
    registerResumedActivityListener(application)

    // 添加动态快捷方式到应用程序(金丝雀)
    addDynamicShortcut(application)

    // 在主线程上发布一个任务,使得日志在Application.onCreate()之后输出。  
    // We post so that the log happens after Application.onCreate()  
    mainHandler.post {
        // 在后台线程上发布一个任务,因为HeapDumpControl.iCanHasHeap()检查一个共享的偏好设置,  
        // 这会阻塞直到加载完成,从而导致StrictMode违规。  
        // https://github.com/square/leakcanary/issues/1981  
        // We post to a background handler because HeapDumpControl.iCanHasHeap() checks a shared pref  
        // which blocks until loaded and that creates a StrictMode violation.  
        SharkLog.d {
        when (val iCanHasHeap = HeapDumpControl.iCanHasHeap()) {
            is Yup -> application.getString(R.string.leak_canary_heap_dump_enabled_text)
            is Nope -> application.getString(
                R.string.leak_canary_heap_dump_disabled_text, iCanHasHeap.reason()
            )
        }
        }
    }
}

VisibilityTracker

LeakCanary 如何感知应用是否处于前台。

通过 ActivityLifecycleCallbacks 计算 start-stop 的 Activity 个数来实现来判断应用是否在前台。

kotlin 复制代码
internal class VisibilityTracker(
  private val listener: (Boolean) -> Unit
) : Application.ActivityLifecycleCallbacks by noOpDelegate(), BroadcastReceiver() {

  private var startedActivityCount = 0

  /**
   * Visible activities are any activity started but not stopped yet. An activity can be paused
   * yet visible: this will happen when another activity shows on top with a transparent background
   * and the activity behind won't get touch inputs but still need to render / animate.
   */
  private var hasVisibleActivities: Boolean = false

  /**
   * Assuming screen on by default.
   */
  private var screenOn: Boolean = true

  private var lastUpdate: Boolean = false

  override fun onActivityStarted(activity: Activity) {
    startedActivityCount++
    if (!hasVisibleActivities && startedActivityCount == 1) {
      hasVisibleActivities = true
      updateVisible()
    }
  }

  override fun onActivityStopped(activity: Activity) {
    // This could happen if the callbacks were registered after some activities were already
    // started. In that case we effectively considers those past activities as not visible.
    if (startedActivityCount > 0) {
      startedActivityCount--
    }
    if (hasVisibleActivities && startedActivityCount == 0 && !activity.isChangingConfigurations) {
      hasVisibleActivities = false
      updateVisible()
    }
  }

  override fun onReceive(
    context: Context,
    intent: Intent
  ) {
    screenOn = intent.action != ACTION_SCREEN_OFF
    updateVisible()
  }

  private fun updateVisible() {
    val visible = screenOn && hasVisibleActivities
    if (visible != lastUpdate) {
      lastUpdate = visible
      listener.invoke(visible)
    }
  }
}

internal fun Application.registerVisibilityListener(listener: (Boolean) -> Unit) {
  val visibilityTracker = VisibilityTracker(listener)
  registerActivityLifecycleCallbacks(visibilityTracker)
  registerReceiver(visibilityTracker, IntentFilter().apply {
    addAction(ACTION_SCREEN_ON)
    addAction(ACTION_SCREEN_OFF)
  })
}

GcTrigger

kotlin 复制代码
fun interface GcTrigger {

    object Default : GcTrigger {
        override fun runGc() {
            Runtime.getRuntime()
            .gc()
            enqueueReferences()
            System.runFinalization()
        }

        private fun enqueueReferences() {
            Thread.sleep(100)
        } catch (e: InterruptedException) {
            throw AssertionError()
        }
        }
    }
}

通过调用 Runtime.getRuntime().gc() 来触发垃圾回收,然后调用 enqueueReferences() 方法将引用加入队列,最后调用 System.runFinalization() 来运行终结器。在 enqueueReferences() 方法中,使用 Thread.sleep(100) 来模拟延迟,以确保引用队列守护进程有足够的时间将引用移动到适当的队列中。

触发检测的时机

个人理解,四个 watcher 的主要工作,就是去寻找一个合适的时机,准备执行内存泄漏的检测 。具体的内存泄漏检测工作是交给 ObjectWatcher 来。

ActivityWatcher

ActivityWatcher 通过 application.registerActivityLifecycleCallbacks 注册回调,监听所有Activity的生命周期,在每个Activity onDestory 的时候,调用 ObjectWatcher 的 expectWeaklyReachable 判断是否出现内存泄漏。

kotlin 复制代码
class ActivityWatcher(
  private val application: Application,
  private val reachabilityWatcher: ReachabilityWatcher
) : InstallableWatcher {

  private val lifecycleCallbacks =
    object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
      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)
  }
}

FragmentAndViewModelWatcher

顾名思义,就是监控了 Fragment 和 ViewModel,在某一个时机执行泄漏检测。

  • fragmentDestroyWatchers是一个watcher数组,为了兼容androidX和support以及不同版本的Fragment。
    AndroidOFragmentDestroyWatcher
    AndroidXFragmentDestroyWatcher
    AndroidSupportFragmentDestroyWatcher
  • 每个Fragment Watcher的实现都是差不多的,通过fragmentManager#registerFragmentLifecycleCallbacks注册回调,监听 Fragment 的生命周期,在onFragmentDestroyed 的时候,触发内测泄漏的检测逻辑。
  • AndroidXFragmentDestroyWatcher
    现在比较多用的是AndroidX了,所以我们简单看下具体的实现。
kotlin 复制代码
internal class AndroidXFragmentDestroyWatcher(
  private val reachabilityWatcher: ReachabilityWatcher
) : (Activity) -> Unit {

  private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {

    override fun onFragmentCreated(
      fm: FragmentManager,
      fragment: Fragment,
      savedInstanceState: Bundle?
    ) {
      ViewModelClearedWatcher.install(fragment, reachabilityWatcher)
    }

    override fun onFragmentViewDestroyed(
      fm: FragmentManager,
      fragment: Fragment
    ) {
      val view = fragment.view
      if (view != null) {
        reachabilityWatcher.expectWeaklyReachable(
          view, "${fragment::class.java.name} received Fragment#onDestroyView() callback " +
          "(references to its views should be cleared to prevent leaks)"
        )
      }
    }

    override fun onFragmentDestroyed(
      fm: FragmentManager,
      fragment: Fragment
    ) {
      reachabilityWatcher.expectWeaklyReachable(
        fragment, "${fragment::class.java.name} received Fragment#onDestroy() callback"
      )
    }
  }

  override fun invoke(activity: Activity) {
    if (activity is FragmentActivity) {
      val supportFragmentManager = activity.supportFragmentManager
      supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
      ViewModelClearedWatcher.install(activity, reachabilityWatcher)
    }
  }
}
  • 在 onFragmentViewDestroyed 的时候观察 view, 在 onFragmentDestroyed 的时候观察 fragment。因为在 fragment 中,view 和 fragment 的生命周期是不一定是同步的。
  • 在 onFragmentCreated 的时候,创建一个自定义的 ViewModel:ViewModelClearedWatcher。
  • 当 fragment destroy的时候,就会执行到 ViewModelClearedWatcher 的 onCleared 方法,在这个时候检测其他 viewModel 是否发生泄漏。

ViewModelClearedWatcher

先了解一下ViewModel。ViewModelStoreOwner(一般是activity或者fragment)内部会持有一个ViewModelStore,使用一个 mMap 存放创建的 ViewModel。当 activity onDestroy 的时候,就会遍历执行viewModel 的 onClear 方法。

实现思路:

  • ViewModelStore 中 mMap 是私有的,需要通过反射获取到,这就可以拿到当前 ViewModelStore 内的所有的 ViewModel。
  • 往监测的 activity 或 fragment,创建一个自定义的 ViewModel。那么当这个 ViewModel 被销毁执行onCleared(也就是页面被销毁)的时候,去遍历 mMap 中的其他 ViewModel,检测每个 ViewModel 的内存泄漏情况。
kotlin 复制代码
//该watcher 继承了ViewModel,生命周期被 ViewModelStoreOwner 管理。
internal class ViewModelClearedWatcher(
  storeOwner: ViewModelStoreOwner,
  private val reachabilityWatcher: ReachabilityWatcher
) : ViewModel() {

  private val viewModelMap: Map<String, ViewModel>?

  init {
    //(1.3.2.3)通过反射获取所有的 store 存储的所有viewModelMap
    viewModelMap = try {
      val mMapField = ViewModelStore::class.java.getDeclaredField("mMap")
      mMapField.isAccessible = true
      @Suppress("UNCHECKED_CAST")
      mMapField[storeOwner.viewModelStore] as Map<String, ViewModel>
    } catch (ignored: Exception) {
      null
    }
  }

  override fun onCleared() {
    ///(1.3.2.4) viewmodle 被清理释放的时候回调,检查所有viewmodle 是否会有泄漏
    viewModelMap?.values?.forEach { viewModel ->
      reachabilityWatcher.expectWeaklyReachable(
        viewModel, "${viewModel::class.java.name} received ViewModel#onCleared() callback"
      )
    }
  }

  companion object {
    fun install(
      storeOwner: ViewModelStoreOwner,
      reachabilityWatcher: ReachabilityWatcher
    ) {
      val provider = ViewModelProvider(storeOwner, object : Factory {
        @Suppress("UNCHECKED_CAST")
        override fun <T : ViewModel?> create(modelClass: Class<T>): T =
          ViewModelClearedWatcher(storeOwner, reachabilityWatcher) as T
      })
      ///(1.3.2.2) 获取ViewModelClearedWatcher实例
      provider.get(ViewModelClearedWatcher::class.java)
    }
  }
}

判断是否出现内存泄漏

kotlin 复制代码
class ObjectWatcher constructor(
  private val clock: Clock,
  private val checkRetainedExecutor: Executor,
  /**
   * Calls to [watch] will be ignored when [isEnabled] returns false
   */
  private val isEnabled: () -> Boolean = { true }
) : ReachabilityWatcher {

  private val onObjectRetainedListeners = mutableSetOf<OnObjectRetainedListener>()

  /**
   * References passed to [watch].
   */
  private val watchedObjects = mutableMapOf<String, KeyedWeakReference>()

  private val queue = ReferenceQueue<Any>()

  /**
   * Returns true if there are watched objects that aren't weakly reachable, and
   * have been watched for long enough to be considered retained.
   */
  val hasRetainedObjects: Boolean
    @Synchronized get() {
      removeWeaklyReachableObjects()
      return watchedObjects.any { it.value.retainedUptimeMillis != -1L }
    }

  /**
   * Returns the number of retained objects, ie the number of watched objects that aren't weakly
   * reachable, and have been watched for long enough to be considered retained.
   */
  val retainedObjectCount: Int
    @Synchronized get() {
      removeWeaklyReachableObjects()
      return watchedObjects.count { it.value.retainedUptimeMillis != -1L }
    }

  /**
   * Returns true if there are watched objects that aren't weakly reachable, even
   * if they haven't been watched for long enough to be considered retained.
   */
  val hasWatchedObjects: Boolean
    @Synchronized get() {
      removeWeaklyReachableObjects()
      return watchedObjects.isNotEmpty()
    }

  /**
   * Returns the objects that are currently considered retained. Useful for logging purposes.
   * Be careful with those objects and release them ASAP as you may creating longer lived leaks
   * then the one that are already there.
   */
  val retainedObjects: List<Any>
    @Synchronized get() {
      removeWeaklyReachableObjects()
      val instances = mutableListOf<Any>()
      for (weakReference in watchedObjects.values) {
        if (weakReference.retainedUptimeMillis != -1L) {
          val instance = weakReference.get()
          if (instance != null) {
            instances.add(instance)
          }
        }
      }
      return instances
    }

  @Synchronized fun addOnObjectRetainedListener(listener: OnObjectRetainedListener) {
    onObjectRetainedListeners.add(listener)
  }

  @Synchronized fun removeOnObjectRetainedListener(listener: OnObjectRetainedListener) {
    onObjectRetainedListeners.remove(listener)
  }

  @Synchronized override fun expectWeaklyReachable(
    watchedObject: Any,
    description: String
  ) {
    if (!isEnabled()) {
      return
    }
    removeWeaklyReachableObjects()
    val key = UUID.randomUUID()
      .toString()
    val watchUptimeMillis = clock.uptimeMillis()
    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[key] = reference
    checkRetainedExecutor.execute {
      moveToRetained(key)
    }
  }

  /**
   * Clears all [KeyedWeakReference] that were created before [heapDumpUptimeMillis] (based on
   * [clock] [Clock.uptimeMillis])
   */
  @Synchronized fun clearObjectsWatchedBefore(heapDumpUptimeMillis: Long) {
    val weakRefsToRemove =
      watchedObjects.filter { it.value.watchUptimeMillis <= heapDumpUptimeMillis }
    weakRefsToRemove.values.forEach { it.clear() }
    watchedObjects.keys.removeAll(weakRefsToRemove.keys)
  }

  /**
   * Clears all [KeyedWeakReference]
   */
  @Synchronized fun clearWatchedObjects() {
    watchedObjects.values.forEach { it.clear() }
    watchedObjects.clear()
  }

  @Synchronized private fun moveToRetained(key: String) {
    removeWeaklyReachableObjects()
    val retainedRef = watchedObjects[key]
    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)
  }
}

ObjectWatcher#expectWeaklyReachable

  • queue:引用队列,用于存放被 GC 后的对象。

  • watchedObjects 集合:mutableMapOf<String, KeyedWeakReference>(),存放正在观察的所有引用对象。

  • 如果 KeyedWeakReference#retainedUptimeMillis 的时间戳不为 -1的话,说明对象仍存活着。

  • KeyedWeakReference

    1. KeyedWeakReference 继承于 WeakReference,弱引用是不会阻止 GC 回收对象的,同时我们可以在构造函数中传递一个 ReferenceQueue,用于对象被 GC 后存放的队列。
    2. 创建要观察对象的 KeyedWeakReference,传入引用队列 queue 。当 WeakReference 中的对象被 gc 回收的时候,会将弱引用包装的对象加入到引用队列中。
    3. 可以通过判断引用队列中,是否有指定的实例,来判断实例是否被回收。如果对象被回收了,删除在 watchedObjects 集合 中的 key value 数据。
  • checkRetainedExecutor:延迟 5 秒后,执行 moveToRetained 方法。

    • removeWeaklyReachableObjects:将已经被 GC 的对象从 watchedObjects 集合中删除。
    • 先调用一次 removeWeaklyReachableObjects() 删除已经 GC 的对象,那么剩下的对象就可以认为是被保留(没办法 GC)的对象。
    • 遍历 queue 引用队列,如果对象已经被回收,删除 watchedObjects 集合里面对应的元素。
    • 判断 watchedObjects 是否存在对应的引用。如果没有,说明对象已经被回收。如果有的话,说明可能发生了内存泄漏。(注意这里的差异,最终是通过 watchedObjects 进行判断的)
    • 调用 onObjectRetained 方法,通知外部的 listener,处理后续逻辑。比如调用Debug.dumpHprofData() 方法从虚拟机中 dump hprof 文件。
    • 如果 dump了内存快照文件之后,就需要执行 clearObjectsWatchedBefore,根据当前时间戳,清空掉 watchedObjects 集合里面的观察对象了,可以避免再次重复判断。

dump出内存快照

HeapDumpTrigger#dumpHeap

scss 复制代码
  private fun dumpHeap(
    retainedReferenceCount: Int,
    retry: Boolean,
    reason: String
  ) {
    val directoryProvider =
      InternalLeakCanary.createLeakDirectoryProvider(InternalLeakCanary.application)
    val heapDumpFile = directoryProvider.newHeapDumpFile()

    val durationMillis: Long
    if (currentEventUniqueId == null) {
      currentEventUniqueId = UUID.randomUUID().toString()
    }
    try {
      InternalLeakCanary.sendEvent(DumpingHeap(currentEventUniqueId!!))
      if (heapDumpFile == null) {
        throw RuntimeException("Could not create heap dump file")
      }
      saveResourceIdNamesToMemory()
      val heapDumpUptimeMillis = SystemClock.uptimeMillis()
      KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis
      durationMillis = measureDurationMillis {
        configProvider().heapDumper.dumpHeap(heapDumpFile)
      }
      if (heapDumpFile.length() == 0L) {
        throw RuntimeException("Dumped heap file is 0 byte length")
      }
      lastDisplayedRetainedObjectCount = 0
      lastHeapDumpUptimeMillis = SystemClock.uptimeMillis()
      objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)
      currentEventUniqueId = UUID.randomUUID().toString()
      InternalLeakCanary.sendEvent(HeapDump(currentEventUniqueId!!, heapDumpFile, durationMillis, reason))
    } catch (throwable: Throwable) {
      InternalLeakCanary.sendEvent(HeapDumpFailed(currentEventUniqueId!!, throwable, retry))
      if (retry) {
        scheduleRetainedObjectCheck(
          delayMillis = WAIT_AFTER_DUMP_FAILED_MILLIS
        )
      }
      showRetainedCountNotification(
        objectCount = retainedReferenceCount,
        contentText = application.getString(
          R.string.leak_canary_notification_retained_dump_failed
        )
      )
      return
    }
  }
  • HeapDumper:dump 出内存快照文件的实现类,看起来是可以自定义实现和配置的。
  • 默认实现是 AndroidDebugHeapDumper,调用 Debug.dumpHprofData() 方法从虚拟机中 dump hprof 文件。
  • dumpHeap 的条件:泄露个数超过5个,并且 60s 内只会 dump 一次。
ini 复制代码
 private fun checkRetainedObjects() {
    val iCanHasHeap = HeapDumpControl.iCanHasHeap()

    val config = configProvider()

    if (iCanHasHeap is Nope) {
      if (iCanHasHeap is NotifyingNope) {
        // Before notifying that we can't dump heap, let's check if we still have retained object.
        var retainedReferenceCount = objectWatcher.retainedObjectCount

        if (retainedReferenceCount > 0) {
          gcTrigger.runGc()
          retainedReferenceCount = objectWatcher.retainedObjectCount
        }

        val nopeReason = iCanHasHeap.reason()
        val wouldDump = !checkRetainedCount(
          retainedReferenceCount, config.retainedVisibleThreshold, nopeReason
        )

        if (wouldDump) {
          val uppercaseReason = nopeReason[0].toUpperCase() + nopeReason.substring(1)
          onRetainInstanceListener.onEvent(DumpingDisabled(uppercaseReason))
          showRetainedCountNotification(
            objectCount = retainedReferenceCount,
            contentText = uppercaseReason
          )
        }
      } else {
        SharkLog.d {
          application.getString(
            R.string.leak_canary_heap_dump_disabled_text, iCanHasHeap.reason()
          )
        }
      }
      return
    }

    var retainedReferenceCount = objectWatcher.retainedObjectCount

    if (retainedReferenceCount > 0) {
      gcTrigger.runGc()
      retainedReferenceCount = objectWatcher.retainedObjectCount
    }

    if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return

    val now = SystemClock.uptimeMillis()
    val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis
    if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {
      onRetainInstanceListener.onEvent(DumpHappenedRecently)
      showRetainedCountNotification(
        objectCount = retainedReferenceCount,
        contentText = application.getString(R.string.leak_canary_notification_retained_dump_wait)
      )
      scheduleRetainedObjectCheck(
        delayMillis = WAIT_BETWEEN_HEAP_DUMPS_MILLIS - elapsedSinceLastDumpMillis
      )
      return
    }

    dismissRetainedCountNotification()
    val visibility = if (applicationVisible) "visible" else "not visible"
    dumpHeap(
      retainedReferenceCount = retainedReferenceCount,
      retry = true,
      reason = "$retainedReferenceCount retained objects, app is $visibility"
    )
  }
  • 先通过 objectWatcher.retainedObjectCount 方法拿到了被 objectWatcher 持有的对象的个数。如果残留的对象大于0就主动执行一次 gc,然后再次获取到残留对象的个数。
  • 通过 checkRetainedCount 方法判断是否需要马上 dump,如果需要就返回 false
  • 需要马上 dump 之后,会检查两次 dump 之间的时间间隔是否小于1分钟,如果小于一分钟就会弹出一个通知说:Last heap dump was less than a minute ago,然后过一段时间再次执行scheduleRetainedObjectCheck 方法。
  • 如果俩次 dump 时间间隔已经大于等于一分钟了,就会调用 dumpHeap 方法。

分析内存快照

BackgroundThreadHeapAnalyzer 和 AndroidDebugHeapAnalyzer

开启后台线程,通过 AndroidDebugHeapAnalyzer.runAnalysisBlocking 方法来分析堆快照的,并在分析过程中和分析完成后发送回调事件。

底层是调用 shark 库,分析对象图,根据 AndroidObjectInspectors 定义好的泄露规则,寻找出泄露对象。

kotlin 复制代码
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)
      }
    }
  }
}
kotlin 复制代码
fun runAnalysisBlocking(
    heapDumped: HeapDump,
    isCanceled: () -> Boolean = { false },
progressEventListener: (HeapAnalysisProgress) -> Unit
): HeapAnalysisDone<*> {
...

// 获取堆转储文件、持续时间和原因
val heapDumpFile = heapDumped.file

val heapDumpReason = heapDumped.reason

// 根据堆转储文件是否存在进行堆分析
val heapAnalysis = if (heapDumpFile.exists()) {
    analyzeHeap(heapDumpFile, progressListener, isCanceled)
} else {
    missingFileFailure(heapDumpFile)
}

// 根据堆分析结果进行处理
val fullHeapAnalysis = when (heapAnalysis) {
    ...
}

// 更新进度,表示正在生成报告
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.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
}

例如 Activity 的泄露,mDestroyed 为 true,但是 activity 仍存在,则可以认为是 activity 发生了泄露。

kotlin 复制代码
  ACTIVITY {
    override val leakingObjectFilter = { heapObject: HeapObject ->
      heapObject is HeapInstance &&
        heapObject instanceOf "android.app.Activity" &&
        heapObject["android.app.Activity", "mDestroyed"]?.value?.asBoolean == true
    }

    override fun inspect(
      reporter: ObjectReporter
    ) {
      reporter.whenInstanceOf("android.app.Activity") { instance ->
        // Activity.mDestroyed was introduced in 17.
        // https://android.googlesource.com/platform/frameworks/base/+
        // /6d9dcbccec126d9b87ab6587e686e28b87e5a04d
        val field = instance["android.app.Activity", "mDestroyed"]

        if (field != null) {
          if (field.value.asBoolean!!) {
            leakingReasons += field describedWithValue "true"
          } else {
            notLeakingReasons += field describedWithValue "false"
          }
        }
      }
    }
  }

了解 Hprof 文件结构

文件格式定义

hprof 是由 JVMTI Agent HPROF 生成的一种二进制文件。具体文件格式可以看官方文档:hprof文件格式定义

如上图所示,Hprof 文件是由文件头和文件内容两部分组成,文件内容是由一系列的 record 组成,record 的类型通过 TAG 进行区分。

对应的数据结构定义

头文件:HprofHeader,记录 hprof 文件的元信息。

文件内容:HprofRecord 就代表了一个 Hprof 记录,比如 StringRecord 代表字符串,ClassDumpRecord 代表内存的类和接口信息,InstanceDumpRecord 代表内存的实例信息。

hprof文件读取

整体过程:先读取文件头,再读取 record,根据 TAG 区分 record 的类型,接着按照 HPROF Agent 给出的格式依次读取各种信息。

  • 头文件解析

通过调用 HprofHeader#parseHeaderOf,就可以解析得到对应的头文件信息了。

kotlin 复制代码
    fun parseHeaderOf(source: BufferedSource): HprofHeader {
      require(!source.exhausted()) {
        throw IllegalArgumentException("Source has no available bytes")
      }
      //版本号
      val endOfVersionString = source.indexOf(0)
      val versionName = source.readUtf8(endOfVersionString)

      val version = supportedVersions[versionName]
      checkNotNull(version) {
        "Unsupported Hprof version [$versionName] not in supported list ${supportedVersions.keys}"
      }
      // Skip the 0 at the end of the version string.
      source.skip(1)
      //标识符大小
      val identifierByteSize = source.readInt()
      //时间戳
      val heapDumpTimestamp = source.readLong()
      return HprofHeader(heapDumpTimestamp, version, identifierByteSize)
    }
  • Record内容解析

从头文件结束的位置,开始进行读取。

开启 while 循环,不断的读取出每个 record 的内容。

根据对应的 TAG 和 length 信息,读取到 Body 内容,创建对应的 Record 对象进行保存起来,方便后面查询。

scss 复制代码
      while (!source.exhausted()) {
        // type of the record
        val tag = reader.readUnsignedByte()

        // number of microseconds since the time stamp in the header
        reader.skip(intByteSize)

        // number of bytes that follow and belong to this record
        val length = reader.readUnsignedInt()

        when (tag) {
          //String类型  
          STRING_IN_UTF8.tag -> {
            if (STRING_IN_UTF8 in recordTags) {
              listener.onHprofRecord(STRING_IN_UTF8, length, reader)
            } else {
              reader.skip(length)
            }
        }
      }

HprofHeapGraph 查找

上面读取后的 Record 信息,都会保存到 HprofHeapGraph 里面,通过 HprofHeapGraph 可以根据条件查找到对应的内存数据。

比如找到内存中所有的 Bitmap 对象。

  1. 找到 Bitmap 对应的 Class。
  2. 找出内存中的 instance,过滤出 class 是 bitmapClassName 的 instance。

自定义 Dumper 实现

  • 默认使用 leakCanary 的 AndroidHeapDumper,会卡主进程。
  • 可以考虑接入 koom 的 koom-fast-dump,在子进程dump hprof文件。

子进程 dump 内存快照

KOOM - 利用子进程 dump hprof

  • 由于原生的 dump 操作会挂起对应进程中所有的线程,然后进行快照文件抓取,最后恢复 JVM 所有线程。需要持续一段时间,可能导致卡顿,甚至 ANR。也就是说,就算开启后台线程也会卡顿,这也是 Leakcanary 不能线上部署的原因。
  • 而快手利用了 linux 的 COW技术,fork 子进程进行 dump 操作。并且为了解决在子进程中卡住的问题,先在主进程中进程 JVM 的线程暂停。

KOOM 提出了一个在不冻结 APP 的情况下 dump hprof 的思路:fork 出子进程,总体流程是这样的:

  1. 父进程 suspend JVM
  2. 父进程 fork 出子进程
  3. 父进程 resume JVM 后,线程等待子进程结束从而拿到 hprof
  4. 子进程调用 Debug.dumpHprofData 生成 hprof 后退出
  5. 父进程启动 Service 在另一个进程里解析 hprof 并构造 GC ROOT PATH

整个过程 APP 只在 fork 前后冻结了一小会,这么短的时间是可以接受的,由于 fork 采用 Copy-On-Write 机制,子进程能够继承父进程的内存。

源码实现:ForkJvmHeapDumper 是 Java 层接口,hprof_dump 实现底层的逻辑。

typescript 复制代码
public class ForkJvmHeapDumper {
  @Override
  public boolean dump(String path) {
    // ...
    boolean dumpRes = false;
    try {
      int pid = trySuspendVMThenFork();
      if (pid == 0) {                       // 子进程
        Debug.dumpHprofData(path);
        KLog.i(TAG, "notifyDumped:" + dumpRes);
        //System.exit(0);
        exitProcess();
      } else {                              // 父进程
        resumeVM();
        dumpRes = waitDumping(pid);
        KLog.i(TAG, "hprof pid:" + pid + " dumped: " + path);
      }

    } catch (IOException e) {
      e.printStackTrace();
      KLog.e(TAG, "dump failed caused by IOException!");
    }
    return dumpRes;
  }    
}
scss 复制代码
HprofDump::HprofDump() : init_done_(false), android_api_(0) {
  android_api_ = android_get_device_api_level();
}

void HprofDump::Initialize() {
  if (init_done_ || android_api_ < __ANDROID_API_L__) {
    return;
  }

  void *handle = kwai::linker::DlFcn::dlopen("libart.so", RTLD_NOW);
  KCHECKV(handle)

  if (android_api_ < __ANDROID_API_R__) {
    suspend_vm_fnc_ =
        (void (*)())DlFcn::dlsym(handle, "_ZN3art3Dbg9SuspendVMEv");
    KFINISHV_FNC(suspend_vm_fnc_, DlFcn::dlclose, handle)

    resume_vm_fnc_ = (void (*)())kwai::linker::DlFcn::dlsym(
        handle, "_ZN3art3Dbg8ResumeVMEv");
    KFINISHV_FNC(resume_vm_fnc_, DlFcn::dlclose, handle)
  } else if (android_api_ <= __ANDROID_API_S__) {
    // Over size for device compatibility
    ssa_instance_ = std::make_unique<char[]>(64);
    sgc_instance_ = std::make_unique<char[]>(64);

    ssa_constructor_fnc_ = (void (*)(void *, const char *, bool))DlFcn::dlsym(
        handle, "_ZN3art16ScopedSuspendAllC1EPKcb");
    KFINISHV_FNC(ssa_constructor_fnc_, DlFcn::dlclose, handle)

    ssa_destructor_fnc_ =
        (void (*)(void *))DlFcn::dlsym(handle, "_ZN3art16ScopedSuspendAllD1Ev");
    KFINISHV_FNC(ssa_destructor_fnc_, DlFcn::dlclose, handle)

    sgc_constructor_fnc_ =
        (void (*)(void *, void *, GcCause, CollectorType))DlFcn::dlsym(
            handle,
            "_ZN3art2gc23ScopedGCCriticalSectionC1EPNS_6ThreadENS0_"
            "7GcCauseENS0_13CollectorTypeE");
    KFINISHV_FNC(sgc_constructor_fnc_, DlFcn::dlclose, handle)

    sgc_destructor_fnc_ = (void (*)(void *))DlFcn::dlsym(
        handle, "_ZN3art2gc23ScopedGCCriticalSectionD1Ev");
    KFINISHV_FNC(sgc_destructor_fnc_, DlFcn::dlclose, handle)

    mutator_lock_ptr_ =
        (void **)DlFcn::dlsym(handle, "_ZN3art5Locks13mutator_lock_E");
    KFINISHV_FNC(mutator_lock_ptr_, DlFcn::dlclose, handle)

    exclusive_lock_fnc_ = (void (*)(void *, void *))DlFcn::dlsym(
        handle, "_ZN3art17ReaderWriterMutex13ExclusiveLockEPNS_6ThreadE");
    KFINISHV_FNC(exclusive_lock_fnc_, DlFcn::dlclose, handle)

    exclusive_unlock_fnc_ = (void (*)(void *, void *))DlFcn::dlsym(
        handle, "_ZN3art17ReaderWriterMutex15ExclusiveUnlockEPNS_6ThreadE");
    KFINISHV_FNC(exclusive_unlock_fnc_, DlFcn::dlclose, handle)
  }
  DlFcn::dlclose(handle);
  init_done_ = true;
}

pid_t HprofDump::SuspendAndFork() {
  KCHECKI(init_done_)

  if (android_api_ < __ANDROID_API_R__) {
    suspend_vm_fnc_();
  } else if (android_api_ <= __ANDROID_API_S__) {
    void *self = __get_tls()[TLS_SLOT_ART_THREAD_SELF];
    sgc_constructor_fnc_((void *)sgc_instance_.get(), self, kGcCauseHprof,
                         kCollectorTypeHprof);
    ssa_constructor_fnc_((void *)ssa_instance_.get(), LOG_TAG, true);
    // avoid deadlock with child process
    exclusive_unlock_fnc_(*mutator_lock_ptr_, self);
    sgc_destructor_fnc_((void *)sgc_instance_.get());
  }

  pid_t pid = fork();
  if (pid == 0) {
    // Set timeout for child process
    alarm(60);
    prctl(PR_SET_NAME, "forked-dump-process");
  }
  return pid;
}

bool HprofDump::ResumeAndWait(pid_t pid) {
  KCHECKB(init_done_)

  if (android_api_ < __ANDROID_API_R__) {
    resume_vm_fnc_();
  } else if (android_api_ <= __ANDROID_API_S__) {
    void *self = __get_tls()[TLS_SLOT_ART_THREAD_SELF];
    exclusive_lock_fnc_(*mutator_lock_ptr_, self);
    ssa_destructor_fnc_((void *)ssa_instance_.get());
  }
  int status;
  for (;;) {
    if (waitpid(pid, &status, 0) != -1) {
      if (!WIFEXITED(status)) {
        ALOGE("Child process %d exited with status %d, terminated by signal %d",
              pid, WEXITSTATUS(status), WTERMSIG(status));
        return false;
      }
      return true;
    }
    // if waitpid is interrupted by the signal,just call it again
    if (errno == EINTR){
      continue;
    }
    return false;
  }
}

}  // namespace leak_monitor
}
  • HprofDump::Initialize,获取设备的 Android Api 版本号
  • 根据不同的版本号,通过 dlopen 和 dlsym,查找相应的 native 函数调用。
  • HprofDump::SuspendAndFork,父进程 suspend JVM,父进程 fork 出子进程,返回子进程 pid。
  • 子进程用 Debug.dumpHprofData 生成 hprof 文件后结束,父进程拿到 hprof 文件后进行分析。

为什么fork进程前要挂起子线程?

JVM 虚拟机在 dump 的时候,需要提前挂起所有的线程,才能进行内存的 dump。

我们就提前把进程中的所有子线程挂起,这样 fork 之后再去 dump 内存时,因为线程本身已经挂起了,自然就不需要再次执行挂起操作,从而可以顺利的进行内存dump操作了。

如何调用native挂起线程的方法

dlopen、dlsys 去查找相应的 native 函数进行调用。

裁剪 Hprof 文件

剖析hprof文件的两种主要裁剪流派 - Yorek's Blog

裁剪分为两大流派:

  • dump 之后,对文件进行读取并裁剪:比如Shark、微信的Matrix等。
  • dump 时直接对数据进行实时裁剪,需要 hook 数据的写入过程:比如美团的Probe、快手的KOOM等。

Matrix 裁剪

kotlin 复制代码
object MatrixDump {
    fun doShrinkHprofAndReport(heapDump: HeapDump) {
        val hprofDir = heapDump.hprofFile.parentFile
        val shrinkedHProfFile = File(hprofDir, getShrinkHprofName(heapDump.hprofFile))
        val zipResFile = File(hprofDir, getResultZipName("dump_result_" + Process.myPid()))
        val hprofFile = heapDump.hprofFile
        var zos: ZipOutputStream? = null
        try {
            val startTime = System.currentTimeMillis()
            HprofBufferShrinker().shrink(hprofFile, shrinkedHProfFile)
            MatrixLog.i(
                TAG,
                "shrink hprof file %s, size: %dk to %s, size: %dk, use time:%d",
                hprofFile.path,
                hprofFile.length() / 1024,
                shrinkedHProfFile.path,
                shrinkedHProfFile.length() / 1024,
                System.currentTimeMillis() - startTime
            )
            zos = ZipOutputStream(BufferedOutputStream(FileOutputStream(zipResFile)))
            val resultInfoEntry = ZipEntry("result.info")
            val shrinkedHProfEntry = ZipEntry(shrinkedHProfFile.name)
            zos.putNextEntry(resultInfoEntry)
            val pw = PrintWriter(OutputStreamWriter(zos, Charset.forName("UTF-8")))
            pw.println("# Resource Canary Result Infomation. THIS FILE IS IMPORTANT FOR THE ANALYZER !!")
            pw.println("sdkVersion=" + Build.VERSION.SDK_INT)
//            pw.println(
//                "manufacturer=" + Matrix.with().getPluginByClass<ResourcePlugin>(
//                    ResourcePlugin::class.java
//                ).config.getManufacture()
//            )
            pw.println("hprofEntry=" + shrinkedHProfEntry.name)
            pw.println("leakedActivityKey=" + heapDump.referenceKey)
            pw.flush()
            zos.closeEntry()
            zos.putNextEntry(shrinkedHProfEntry)
            StreamUtil.copyFileToStream(shrinkedHProfFile, zos)
            zos.closeEntry()
//            shrinkedHProfFile.delete()
//            hprofFile.delete()
            MatrixLog.i(
                TAG, "process hprof file use total time:%d",
                System.currentTimeMillis() - startTime
            )
//            CanaryResultService.reportHprofResult(
//                app,
//                zipResFile.absolutePath,
//                heapDump.activityName
//            )
        } catch (e: IOException) {
            MatrixLog.printErrStackTrace(TAG, e, "")
        } finally {
            StreamUtil.closeQuietly(zos)
        }
    }

    private fun getShrinkHprofName(origHprof: File): String? {
        val origHprofName = origHprof.name
        val extPos = origHprofName.indexOf(".hprof")
        val namePrefix = origHprofName.substring(0, extPos)
        return namePrefix + "_shrink.hprof"
    }

    private fun getResultZipName(prefix: String): String? {
        val sb = StringBuilder()
        sb.append(prefix).append('_')
            .append(SimpleDateFormat("yyyyMMddHHmmss", Locale.ENGLISH).format(Date()))
            .append(".zip")
        return sb.toString()
    }
}

Matrix dump出来的文件,裁剪后的 hprof 文件,进行 zip 压缩后的大小。

裁剪后的内存快照文件:

实现思路:先读后写。

  • 首先利用 HprofReader 来解析 hprof 文件,然后分别调用 HprofInfoCollectVisitor、HprofKeptBufferCollectVisitor、HprofBufferShrinkVisitor 这三个 Visitor 来完成 hprof 的裁剪流程,最后通过HprofWriter 重写 hprof。
  • Matrix 方案裁剪 hprof 文件时,裁剪的是 HEAP_DUMP、HEAP_DUMP_SEGMENT 里面的PRIMITIVE_ARRAY_DUMP 段。该方案仅仅会保存字符串的数据以及重复的那一份 Bitmap 的 buffer 数据,其他基本类型数组会被剔除。

Koom 裁剪

ForkStripHeapDumper:边 dump 边裁剪。

Koom dump出来的文件,需要使用项目下的 jar 包进行还原。

实现思路:hook 了数据的 io 过程,在写入时对数据流进行裁剪。

java 复制代码
public class ForkStripHeapDumper implements HeapDumper {
  private static final String TAG = "OOMMonitor_ForkStripHeapDumper";
  private boolean mLoadSuccess;

  private static class Holder {
    private static final ForkStripHeapDumper INSTANCE = new ForkStripHeapDumper();
  }

  public static ForkStripHeapDumper getInstance() {
    return ForkStripHeapDumper.Holder.INSTANCE;
  }

  private ForkStripHeapDumper() {}

  private void init() {
    if (mLoadSuccess) {
      return;
    }
    if (loadSoQuietly("koom-strip-dump")) {
      mLoadSuccess = true;
      initStripDump();
    }
  }

  @Override
  public synchronized boolean dump(String path) {
    MonitorLog.i(TAG, "dump " + path);
    if (!sdkVersionMatch()) {
      throw new UnsupportedOperationException("dump failed caused by sdk version not supported!");
    }
    init();
    if (!mLoadSuccess) {
      MonitorLog.e(TAG, "dump failed caused by so not loaded!");
      return false;
    }
    boolean dumpRes = false;
    try {
      hprofName(path);
      dumpRes = ForkJvmHeapDumper.getInstance().dump(path);
      MonitorLog.i(TAG, "dump result " + dumpRes);
    } catch (Exception e) {
      MonitorLog.e(TAG, "dump failed caused by " + e);
      e.printStackTrace();
    }
    return dumpRes;
  }

  public native void initStripDump();

  public native void hprofName(String name);
}
  • 在 dump 的时候会传入文件路径,
  • 在文件 open 的 hook 回调中根据文件路径进行匹配,匹配成功之后记录下文件的 fd。
  • 在文件 write 的 hook 回调中,内容写入时匹配fd,这样就可以精准拿到hprof写入时的内容了。
  • 裁剪逻辑,都在 ProcessHeap 里面进行处理。使用一个一位数组,记录需要裁剪的区间,偶数位记录的是要裁剪区间的起始位置,奇数位是结束区间。
arduino 复制代码
//设置内存快照文件名称,hprof_name_
void HprofStrip::SetHprofName(const char *hprof_name) {
  hprof_name_ = hprof_name;
}

//hook open 方法,记录内存快照文件的fd,hprof_fd_
int HprofStrip::HookOpenInternal(const char *path_name, int flags, ...) {
  va_list ap;
  va_start(ap, flags);
  int fd = open(path_name, flags, ap);
  va_end(ap);

  if (hprof_name_.empty()) {
    return fd;
  }

  if (path_name != nullptr && strstr(path_name, hprof_name_.c_str())) {
    hprof_fd_ = fd;
    is_hook_success_ = true;
  }
  return fd;
}

//hook write 方法,匹配 hprof_fd_
ssize_t HprofStrip::HookWriteInternal(int fd, const void *buf, ssize_t count) {
  if (fd != hprof_fd_) {
    return write(fd, buf, count);
  }

  // 每次hook_write,初始化重置
  reset();
  
  //对heap进行处理
    const unsigned char tag = ((unsigned char *)buf)[0];
  // 删除掉无关record tag类型匹配,只匹配heap相关提高性能
  switch (tag) {
    case HPROF_TAG_HEAP_DUMP:
    case HPROF_TAG_HEAP_DUMP_SEGMENT: {
      ProcessHeap(
          buf,
          HEAP_TAG_BYTE_SIZE + RECORD_TIME_BYTE_SIZE + RECORD_LENGTH_BYTE_SIZE,
          count, heap_serial_num_, 0);
      heap_serial_num_++;
    } break;
    default:
      break;
  }
  //......
}
  • KOOM 裁剪掉了 system(Zygote、Image) 空间的记录,只保留了 app heap。

KOOM裁剪掉了 system(Zygote、Image) 空间的所有数据,对 app heap 进行部分裁剪。

  • 针对system space(Zygote Space、Image Space):会裁剪PRIMITIVE_ARRAY_DUMP、HEAP_DUMP_INFO、INSTANCE_DUMP和OBJECT_ARRAY_DUMP这4个子TAG,会删除这四个子TAG的全部内容(包函子TAG全都会删除)。
  • 针对 app space:会处理 PRIMITIVE_ARRAY_DUMP 这一块数据,裁剪掉基本类型数组。
ini 复制代码
// Android.
case HPROF_HEAP_DUMP_INFO: {
const unsigned char heap_type =
    ((unsigned char *)buf)[first_index + HEAP_TAG_BYTE_SIZE + 3];
is_current_system_heap_ =
    (heap_type == HPROF_HEAP_ZYGOTE || heap_type == HPROF_HEAP_IMAGE);

if (is_current_system_heap_) {
    strip_index_list_pair_[strip_index_ * 2] = first_index;
    strip_index_list_pair_[strip_index_ * 2 + 1] =
        first_index + HEAP_TAG_BYTE_SIZE /*TAG*/
        + HEAP_TYPE_BYTE_SIZE            /*heap type*/
        + STRING_ID_BYTE_SIZE /*string id*/;
    strip_index_++;
    strip_bytes_sum_ += HEAP_TAG_BYTE_SIZE    /*TAG*/
                        + HEAP_TYPE_BYTE_SIZE /*heap type*/
                        + STRING_ID_BYTE_SIZE /*string id*/;
}

array_serial_no = ProcessHeap(buf,
                                first_index + HEAP_TAG_BYTE_SIZE /*TAG*/
                                    + HEAP_TYPE_BYTE_SIZE /*heap type*/
                                    + STRING_ID_BYTE_SIZE /*string id*/,
                                max_len, heap_serial_no, array_serial_no);
} break;
ini 复制代码
    case HPROF_INSTANCE_DUMP: {
      int instance_dump_index =
          first_index + HEAP_TAG_BYTE_SIZE + OBJECT_ID_BYTE_SIZE +
          STACK_TRACE_SERIAL_NUMBER_BYTE_SIZE + CLASS_ID_BYTE_SIZE;
      int instance_size =
          GetIntFromBytes((unsigned char *)buf, instance_dump_index);

      // 裁剪掉system space
      if (is_current_system_heap_) {
        strip_index_list_pair_[strip_index_ * 2] = first_index;
        strip_index_list_pair_[strip_index_ * 2 + 1] =
            instance_dump_index + U4 /*占位*/ + instance_size;
        strip_index_++;

        strip_bytes_sum_ +=
            instance_dump_index + U4 /*占位*/ + instance_size - first_index;
      }

      array_serial_no =
          ProcessHeap(buf, instance_dump_index + U4 /*占位*/ + instance_size,
                      max_len, heap_serial_no, array_serial_no);
    }
objectivec 复制代码
    case HPROF_OBJECT_ARRAY_DUMP: {
      int length = GetIntFromBytes((unsigned char *)buf,
                                   first_index + HEAP_TAG_BYTE_SIZE +
                                       OBJECT_ID_BYTE_SIZE +
                                       STACK_TRACE_SERIAL_NUMBER_BYTE_SIZE);

      // 裁剪掉system space
      if (is_current_system_heap_) {
        strip_index_list_pair_[strip_index_ * 2] = first_index;
        strip_index_list_pair_[strip_index_ * 2 + 1] =
            first_index + HEAP_TAG_BYTE_SIZE + OBJECT_ID_BYTE_SIZE +
            STACK_TRACE_SERIAL_NUMBER_BYTE_SIZE + U4 /*Length*/
            + CLASS_ID_BYTE_SIZE + U4 /*Id*/ * length;
        strip_index_++;

        strip_bytes_sum_ += HEAP_TAG_BYTE_SIZE + OBJECT_ID_BYTE_SIZE +
                            STACK_TRACE_SERIAL_NUMBER_BYTE_SIZE + U4 /*Length*/
                            + CLASS_ID_BYTE_SIZE + U4 /*Id*/ * length;
      }

      array_serial_no =
          ProcessHeap(buf,
                      first_index + HEAP_TAG_BYTE_SIZE + OBJECT_ID_BYTE_SIZE +
                          STACK_TRACE_SERIAL_NUMBER_BYTE_SIZE + U4 /*Length*/
                          + CLASS_ID_BYTE_SIZE + U4 /*Id*/ * length,
                      max_len, heap_serial_no, array_serial_no);
    }
ini 复制代码
 case HPROF_PRIMITIVE_ARRAY_DUMP: {
      int primitive_array_dump_index = first_index + HEAP_TAG_BYTE_SIZE /*tag*/
                                       + OBJECT_ID_BYTE_SIZE +
                                       STACK_TRACE_SERIAL_NUMBER_BYTE_SIZE;
      int length =
          GetIntFromBytes((unsigned char *)buf, primitive_array_dump_index);
      primitive_array_dump_index += U4 /*Length*/;

      // 裁剪掉基本类型数组,无论是否在system space都进行裁剪
      // 区别是数组左坐标,app space时带数组元信息(类型、长度)方便回填
      if (is_current_system_heap_) {
        strip_index_list_pair_[strip_index_ * 2] = first_index;
      } else {
        strip_index_list_pair_[strip_index_ * 2] =
            primitive_array_dump_index + BASIC_TYPE_BYTE_SIZE /*value type*/;
      }
      array_serial_no++;

      int value_size = GetByteSizeFromType(
          ((unsigned char *)buf)[primitive_array_dump_index]);
      primitive_array_dump_index +=
          BASIC_TYPE_BYTE_SIZE /*value type*/ + value_size * length;

      // 数组右坐标
      strip_index_list_pair_[strip_index_ * 2 + 1] = primitive_array_dump_index;

      // app space时,不修改长度因为回填数组时会补齐
      if (is_current_system_heap_) {
        strip_bytes_sum_ += primitive_array_dump_index - first_index;
      }
      strip_index_++;

      array_serial_no = ProcessHeap(buf, primitive_array_dump_index, max_len,
                                    heap_serial_no, array_serial_no);
    } 

自定义 Hprof 分析脚本实现

例如 LeakCanary、Matrix,都有相关的命令行脚本实现,调用命令去分析 hprof 文件。

我们也可以自定义一些分析脚本,可以使用 Clikt 去实现自定义的命令行功能。

这里使用 LeakCanary 项目里面的 shark-cli 代码进行分析。

LeakCanary 底层是基于 shark库,对 hprof 文件进行解析和分析。

主入口:SharkCliCommand,定义命令行所需要的参数。

一系列的子命令,实现相应的功能。

  • DumpProcessCommand:通过 adb dump 出 hprof 文件,并 pull 到电脑上。
  • AnalyzeCommand:对 hprof 文件进行分析。

例如 AnalyzeCommand,就是调用 shark 库的相关方法,创建 HeapAnalyzer 进行分析 hprof 文件。

ini 复制代码
class AnalyzeCommand : CliktCommand(
  name = "analyze",
  help = "Analyze a heap dump."
) {

  override fun run() {
    val params = context.sharkCliParams
    analyze(retrieveHeapDumpFile(params), params.obfuscationMappingPath)
  }

  companion object {
    fun CliktCommand.analyze(
      heapDumpFile: File,
      proguardMappingFile: File?
    ) {
      val proguardMapping = proguardMappingFile?.let {
        ProguardMappingReader(it.inputStream()).readProguardMapping()
      }
      val objectInspectors = AndroidObjectInspectors.appDefaults.toMutableList()

      val listener = OnAnalysisProgressListener { step ->
        SharkLog.d { "Analysis in progress, working on: ${step.name}" }
      }

      val heapAnalyzer = HeapAnalyzer(listener)
      SharkLog.d { "Analyzing heap dump $heapDumpFile" }

      val heapAnalysis = heapAnalyzer.analyze(
        heapDumpFile = heapDumpFile,
        leakingObjectFinder = FilteringLeakingObjectFinder(
          AndroidObjectInspectors.appLeakingObjectFilters
        ),
        referenceMatchers = AndroidReferenceMatchers.appDefaults,
        computeRetainedHeapSize = true,
        objectInspectors = objectInspectors,
        proguardMapping = proguardMapping,
        metadataExtractor = AndroidMetadataExtractor
      )
      echo(heapAnalysis)
    }
  }
}

文章参考

【Andorid进阶】LeakCanary源码分析,从头到尾搞个明白 - 掘金

JVM 系列(5)吊打面试官:说一下 Java 的四种引用类型 - 掘金

为什么各大厂自研的内存泄漏检测框架都要参考 LeakCanary?因为它是真强啊!

剖析hprof文件的两种主要裁剪流派 - Yorek's Blog

KOOM - 利用子进程 dump hprof

LeakCanary万字源码解析,干货满满-腾讯云开发者社区-腾讯云

KOOM原理讲解(上)-JAVA内存分析-CSDN博客

相关推荐
一点媛艺2 小时前
Kotlin函数由易到难
开发语言·python·kotlin
姑苏风2 小时前
《Kotlin实战》-附录
android·开发语言·kotlin
数据猎手小k5 小时前
AndroidLab:一个系统化的Android代理框架,包含操作环境和可复现的基准测试,支持大型语言模型和多模态模型。
android·人工智能·机器学习·语言模型
你的小106 小时前
JavaWeb项目-----博客系统
android
风和先行6 小时前
adb 命令查看设备存储占用情况
android·adb
AaVictory.7 小时前
Android 开发 Java中 list实现 按照时间格式 yyyy-MM-dd HH:mm 顺序
android·java·list
似霰8 小时前
安卓智能指针sp、wp、RefBase浅析
android·c++·binder
大风起兮云飞扬丶8 小时前
Android——网络请求
android
干一行,爱一行8 小时前
android camera data -> surface 显示
android
断墨先生8 小时前
uniapp—android原生插件开发(3Android真机调试)
android·uni-app