跳到内容
🤔 文档有问题?报告编辑

LeakCanary 发布版

LeakCanary 发布版

修复在 debug 构建版本中发现的泄漏有助于减少 Application Not Responding 冻结和 OutfOfMemoryError 错误崩溃,但这只触及了所有可能发生的泄漏的表面。对于在 debug 构建版本中发现的泄漏,很难确定首先修复哪些泄漏。

这种情况与 debug 崩溃非常相似,我们通常无法准确评估它们在生产环境中的未来影响,也无法找到生产环境中将发生的所有崩溃。对于崩溃,应用通常通过发布崩溃报告流程来监控崩溃率,并根据计数来优先修复问题。

LeakCanary 发布版提供了一些 API,可以在发布构建版本中在生产环境中运行堆分析。

危险

关于这一切,它都是实验性的。在生产环境中运行堆分析并不是一件非常常见的事情,我们仍在学习和试验。此外,artifact 名称和 API 都可能发生变化。

入门

LeakCanary 提供了一个专门用于检测发布构建版本中泄漏的 artifact

dependencies {
  // LeakCanary for releases
  releaseImplementation 'com.squareup.leakcanary:leakcanary-android-release:2.14'
  // Optional: detect retained objects. This helps but is not required.
  releaseImplementation 'com.squareup.leakcanary:leakcanary-object-watcher-android:2.14'
}

这是一个代码示例,当屏幕关闭或应用进入后台时运行堆分析,首先检查 Firebase Remote Config 标志是否开启,然后将结果上传到 Bugsnag

import android.os.Process.THREAD_PRIORITY_BACKGROUND
import java.util.concurrent.Executors
import kotlin.concurrent.thread
import leakcanary.BackgroundTrigger
import leakcanary.HeapAnalysisClient
import leakcanary.HeapAnalysisConfig
import leakcanary.HeapAnalysisInterceptor
import leakcanary.HeapAnalysisInterceptor.Chain
import leakcanary.HeapAnalysisJob
import leakcanary.HeapAnalysisJob.Result.Done
import leakcanary.ScreenOffTrigger

class ReleaseExampleApplication : ExampleApplication() {

  override fun onCreate() {
    super.onCreate()

    // Delete any remaining heap dump (if we crashed)
    analysisExecutor.execute {
      analysisClient.deleteHeapDumpFiles()
    }

    // Starts heap analysis on background importance
    BackgroundTrigger(
      application = this,
      analysisClient = analysisClient,
      analysisExecutor = analysisExecutor,
      analysisCallback = analysisCallback
    ).start()

    // Starts heap analysis when screen off
    ScreenOffTrigger(
      application = this,
      analysisClient = analysisClient,
      analysisExecutor = analysisExecutor,
      analysisCallback = analysisCallback
    ).start()
  }

  /**
   * Call this to trigger heap analysis manually, e.g. from
   * a help button.
   *
   * This method returns a `HeapAnalysisJob` on which you can
   * call `HeapAnalysisJob.cancel()` at any time.
   */
  fun triggerHeapAnalysisNow(): HeapAnalysisJob {
    val job = analysisClient.newJob()
    analysisExecutor.execute {
      val result = job.execute()
      analysisCallback(result)
    }
    return job
  }

  private val analysisClient by lazy {
    HeapAnalysisClient(
      // Use private app storage. cacheDir is never backed up which is important.
      heapDumpDirectoryProvider = { cacheDir },
      // stripHeapDump: remove all user data from hprof before analysis.
      config = HeapAnalysisConfig(stripHeapDump = true),
      // Default interceptors may cancel analysis for several other reasons.
      interceptors = listOf(flagInterceptor) + HeapAnalysisClient.defaultInterceptors(this)
    )
  }

  // Cancels heap analysis if "heap_analysis_flag" is false.
  private val flagInterceptor = object : HeapAnalysisInterceptor {
    val remoteConfig by lazy { FirebaseRemoteConfig.getInstance() }

    override fun intercept(chain: Chain): HeapAnalysisJob.Result {
      if (remoteConfig.getBoolean("heap_analysis_flag")) {
        chain.job.cancel("heap_analysis_flag false")
      }
      return chain.proceed()
    }
  }

  private val analysisExecutor = Executors.newSingleThreadExecutor {
    thread(start = false, name = "Heap analysis executor") {
      android.os.Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND)
      it.run()
    }
  }

  private val analysisCallback: (HeapAnalysisJob.Result) -> Unit = { result ->
    if (result is Done) {
      uploader.upload(result.analysis)
    }
  }

  private val uploader by lazy {
    BugsnagLeakUploader(this@ReleaseExampleApplication)
  }
}

这是 BugsnagLeakUploader

import android.app.Application
import com.bugsnag.android.Bugsnag
import com.bugsnag.android.Configuration
import com.bugsnag.android.ErrorTypes
import com.bugsnag.android.Event
import com.bugsnag.android.ThreadSendPolicy
import shark.HeapAnalysis
import shark.HeapAnalysisFailure
import shark.HeapAnalysisSuccess
import shark.Leak
import shark.LeakTrace
import shark.LeakTraceReference
import shark.LibraryLeak

class BugsnagLeakUploader(applicationContext: Application) {

  private val bugsnagClient = Bugsnag.start(
    applicationContext,
    Configuration("YOUR_BUGSNAG_API_KEY").apply {
      enabledErrorTypes = ErrorTypes(
        anrs = false,
        ndkCrashes = false,
        unhandledExceptions = false,
        unhandledRejections = false
      )
      sendThreads = ThreadSendPolicy.NEVER
    }
  )

  fun upload(heapAnalysis: HeapAnalysis) {
    when (heapAnalysis) {
      is HeapAnalysisSuccess -> {
        val allLeakTraces = heapAnalysis
          .allLeaks
          .toList()
          .flatMap { leak ->
            leak.leakTraces.map { leakTrace -> leak to leakTrace }
          }
        if (allLeakTraces.isEmpty()) {
          // Track how often we perform a heap analysis that yields no result.
          bugsnagClient.notify(NoLeakException()) { event ->
            event.addHeapAnalysis(heapAnalysis)
            true
          }
        } else {
          allLeakTraces.forEach { (leak, leakTrace) ->
            val message = "Memory leak: ${leak.shortDescription}. See LEAK tab."
            val exception = leakTrace.asFakeException(message)
            bugsnagClient.notify(exception) { event ->
              event.addHeapAnalysis(heapAnalysis)
              event.addLeak(leak)
              event.addLeakTrace(leakTrace)
              event.groupingHash = leak.signature
              true
            }
          }
        }
      }
      is HeapAnalysisFailure -> {
        // Please file any reported failure to
        // https://github.com/square/leakcanary/issues
        bugsnagClient.notify(heapAnalysis.exception)
      }
    }
  }

  class NoLeakException : RuntimeException()

  private fun Event.addHeapAnalysis(heapAnalysis: HeapAnalysisSuccess) {
    addMetadata("Leak", "heapDumpPath", heapAnalysis.heapDumpFile.absolutePath)
    heapAnalysis.metadata.forEach { (key, value) ->
      addMetadata("Leak", key, value)
    }
    addMetadata("Leak", "analysisDurationMs", heapAnalysis.analysisDurationMillis)
  }

  private fun Event.addLeak(leak: Leak) {
    addMetadata("Leak", "libraryLeak", leak is LibraryLeak)
    if (leak is LibraryLeak) {
      addMetadata("Leak", "libraryLeakPattern", leak.pattern.toString())
      addMetadata("Leak", "libraryLeakDescription", leak.description)
    }
  }

  private fun Event.addLeakTrace(leakTrace: LeakTrace) {
    addMetadata("Leak", "retainedHeapByteSize", leakTrace.retainedHeapByteSize)
    addMetadata("Leak", "signature", leakTrace.signature)
    addMetadata("Leak", "leakTrace", leakTrace.toString())
  }

  private fun LeakTrace.asFakeException(message: String): RuntimeException {
    val exception = RuntimeException(message)
    val stackTrace = mutableListOf<StackTraceElement>()
    stackTrace.add(StackTraceElement("GcRoot", gcRootType.name, "GcRoot.kt", 42))
    for (cause in referencePath) {
      stackTrace.add(buildStackTraceElement(cause))
    }
    exception.stackTrace = stackTrace.toTypedArray()
    return exception
  }

  private fun buildStackTraceElement(reference: LeakTraceReference): StackTraceElement {
    val file = reference.owningClassName.substringAfterLast(".") + ".kt"
    return StackTraceElement(reference.owningClassName, reference.referenceDisplayName, file, 42)
  }
}