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)
}
}