LeakCanary2 之 Leakcanary-android源码分析

880 阅读6分钟

前言

上篇文章分析了LeakSentry的源码,本文在此基础上来分析下LeakCanary的核心库——Leakcanary-android的源码

一、原理

上篇文章中分析了当Activity、Fragment销毁的时候会调用RefWatcher#watch来观察这些对象,如果这些对象得不到释放那么就会一直保存在retainedReferences中,当App切到后台一定时间后或者保留的引用超过了临界值,那么就会手动的触发一个GC,如果触发后条件仍然满足,那么就会去生成内存堆转储快照,然后开启服务去分析该文件,找出泄露的对象,并给出引用链

二、源码实现

首先根据上篇文章我们知道在InternalLeakSentry的init代码块中会将InternalLeakCanary实例作为LeakSentry的监听器,在InternalLeakSentry#install的最后回调了InternalLeakCanary.onLeakSentryInstalled

InternalLeakCanary#onLeakSentryInstalled

override fun onLeakSentryInstalled(application: Application) {
    this.application = application
    val heapDumper = AndroidHeapDumper(application, leakDirectoryProvider)
    // GC触发器
    val gcTrigger = GcTrigger.Default
    val configProvider = { LeakCanary.config }
    val handlerThread = HandlerThread(HeapDumpTrigger.LEAK_CANARY_THREAD_NAME)
    handlerThread.start()
    val backgroundHandler = Handler(handlerThread.looper)
    heapDumpTrigger = HeapDumpTrigger(
            application, backgroundHandler, LeakSentry.refWatcher, gcTrigger, heapDumper, configProvider
    )
    application.registerVisibilityListener { applicationVisible ->
        this.applicationVisible = applicationVisible
        heapDumpTrigger.onApplicationVisibilityChanged(applicationVisible)
    }
    addDynamicShortcut(application)
}

方法内部主要构件了一个HeapDumpTrigger实例,然后调用了registerVisibilityListener这个扩展方法

internal class VisibilityTracker(private val listener: (Boolean) -> Unit) :
        ActivityLifecycleCallbacksAdapter() {
    private var startedActivityCount = 0
    private var hasVisibleActivities: Boolean = false
    override fun onActivityStarted(activity: Activity) {
        startedActivityCount++
        if (!hasVisibleActivities && startedActivityCount == 1) {
            hasVisibleActivities = true
            listener.invoke(true)
        }
    }
    override fun onActivityStopped(activity: Activity) {
        if (startedActivityCount > 0) {
            startedActivityCount--
        }
        if (hasVisibleActivities && startedActivityCount == 0 && !activity.isChangingConfigurations) {
            hasVisibleActivities = false
            listener.invoke(false)
        }
    }
}
internal fun Application.registerVisibilityListener(listener: (Boolean) -> Unit) {
    registerActivityLifecycleCallbacks(VisibilityTracker(listener))
}

方法内部通过监听Activity声明周期,每次onStart的时候将可见Activity数量加1,如果可见数量为1,并且原来是不可见的,那么就认为App从不可见转变到了可见。每次onStop的时候将可见Activity数量减1,如果可见数量变成了0,并且原来是可见的,并且不是因为配置改变导致的(配置改变会立马又再次启动该Activity),那么就认为App从可见转变到了不可见。再看onLeakSentryInstalled,在可见状态发生变化后会调用onApplicationVisibilityChanged

heapDumpTrigger#onApplicationVisibilityChanged

fun onApplicationVisibilityChanged(applicationVisible: Boolean) {
    if (applicationVisible) {
        applicationInvisibleAt = -1L
    } else {
        applicationInvisibleAt = SystemClock.uptimeMillis()
        scheduleRetainedInstanceCheck("app became invisible", LeakSentry.config.watchDurationMillis)
    }
}

主要看看从可见变成不可见时的情况(App被切换到后台),记录下应用程序开始不可见的时间

private fun scheduleRetainedInstanceCheck(reason: String, delayMillis: Long) {
    if (checkScheduled) {
        return
    }
    checkScheduled = true
    backgroundHandler.postDelayed({
        checkScheduled = false
        checkRetainedInstances(reason)
    }, delayMillis)
}

在5秒(默认)后,会在子线程中执行checkRetainedInstances

heapDumpTrigger#checkRetainedInstances

private fun checkRetainedInstances(reason: String) {
    val config = configProvider()
    // 如果不需要分析堆转储文件就直接返回
    if (!config.dumpHeap) {
        return
    }
    // 获取到还没被释放的被观察者对象数量
    var retainedKeys = refWatcher.retainedKeys
    // 如果保留的对象数量等于0,或者保留的对象数量小于临界值并且App可见或者不可见但是不可见时间少于5秒就返回true,
    // 否则(保留对象数量大于临界值,或者不可见时间超过了5秒)就返回false
    if (checkRetainedCount(retainedKeys, config.retainedVisibleThreshold)) return
    // 如果配置了在debug时不转储堆文件,但是现在正在debug,那么显示一个通知,然后等20秒再试
    if (!config.dumpHeapWhenDebugging && DebuggerControl.isDebuggerAttached) {
        showRetainedCountWithDebuggerAttached(retainedKeys.size)
        scheduleRetainedInstanceCheck("debugger was attached", WAIT_FOR_DEBUG_MILLIS)
        return
    }
    // 运行一下GC,将那些只有弱引用引用着的对象回收
    gcTrigger.runGc()
    // 运行GC后,还保留的对象数量(这些对象极有可能发生泄漏)
    retainedKeys = refWatcher.retainedKeys
    // 如果不可见超过5秒还是会继续执行,否则运行了GC后发现保留对象数量已经少于临界值了那么就直接返回
    if (checkRetainedCount(retainedKeys, config.retainedVisibleThreshold)) return
    // 保存下retainedKeys,便于与后续进行比对
    HeapDumpMemoryStore.setRetainedKeysForHeapDump(retainedKeys)
    HeapDumpMemoryStore.heapDumpUptimeMillis = SystemClock.uptimeMillis()
    dismissNotification()
    // 生成堆转储快照
    val heapDumpFile = heapDumper.dumpHeap()
    if (heapDumpFile == null) {
        scheduleRetainedInstanceCheck("failed to dump heap", WAIT_AFTER_DUMP_FAILED_MILLIS)
        showRetainedCountWithHeapDumpFailed(retainedKeys.size)
        return
    }
    refWatcher.removeRetainedKeys(retainedKeys)
    // 开启服务分析堆转储文件
    HeapAnalyzerService.runAnalysis(application, heapDumpFile)
}

主要来看下heapDumper#dumpHeap和HeapAnalyzerService#runAnalysis,heapDumper是一个AndroidHeapDumper实例

override fun dumpHeap(): File? {
    // 内部就是创建了一个File并且保证.hprof文件不超过7个
    val heapDumpFile = leakDirectoryProvider.newHeapDumpFile() ?: return null
    val waitingForToast = FutureResult<Toast?>()
    // 展示一个toast表示正在生成堆转储文件
    showToast(waitingForToast)
    if (!waitingForToast.wait(5, SECONDS)) {
        return null
    }
    val notificationManager =
            context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    if (Notifications.canShowNotification) {
        val dumpingHeap = context.getString(R.string.leak_canary_notification_dumping)
        val builder = Notification.Builder(context)
                .setContentTitle(dumpingHeap)
        val notification = Notifications.buildNotification(context, builder, LEAKCANARY_LOW)
        notificationManager.notify(R.id.leak_canary_notification_dumping_heap, notification)
    }
    val toast = waitingForToast.get()
    return try {
        // 获取堆转储快照写入heapDumpFile
        Debug.dumpHprofData(heapDumpFile.absolutePath)
        if (heapDumpFile.length() == 0L) {
            CanaryLog.d("Dumped heap file is 0 byte length")
            null
        } else {
            heapDumpFile
        }
    } catch (e: Exception) {
        null
    } finally {
        cancelToast(toast)
        notificationManager.cancel(R.id.leak_canary_notification_dumping_heap)
    }
}

成功生成堆转储文件后会调用HeapAnalyzerService#runAnalysis进行分析

internal class HeapAnalyzerService : ForegroundService(...), AnalyzerProgressListener {
    
    override fun onHandleIntentInForeground(intent: Intent?) {
        if (intent == null) {
            CanaryLog.d("HeapAnalyzerService received a null intent, ignoring.")
            return
        }
        // 因为与应用进程运行在一起所以为了不影响App需要降低该子线程优先级
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)
        val heapDumpFile = intent.getSerializableExtra(HEAPDUMP_FILE_EXTRA) as File
        val heapAnalyzer = HeapAnalyzer(this)
        val config = LeakCanary.config
        val heapAnalysis =
                heapAnalyzer.checkForLeaks(
                        heapDumpFile, config.exclusionsFactory, config.computeRetainedHeapSize,
                        config.leakInspectors, config.labelers)
        // 分析完后,回调下,并且将堆转储文件删除
        try {
            config.analysisResultListener(application, heapAnalysis)
        } finally {
            heapAnalysis.heapDumpFile.delete()
        }
    }
    // 整个分析过程有15步之多,每走一步都会计算百分比更新下通知
    override fun onProgressUpdate(step: AnalyzerProgressListener.Step) {
        val percent = (100f * step.ordinal / AnalyzerProgressListener.Step.values().size).toInt()
        val lowercase = step.name.replace("_", " ")
                .toLowerCase()
        val message = lowercase.substring(0, 1).toUpperCase() + lowercase.substring(1)
        showForegroundNotification(100, percent, false, message)
    }
     companion object {
        private const val HEAPDUMP_FILE_EXTRA = "HEAPDUMP_FILE_EXTRA"
        fun runAnalysis(
                context: Context,
                heapDumpFile: File) {
            val intent = Intent(context, HeapAnalyzerService::class.java)
            intent.putExtra(HEAPDUMP_FILE_EXTRA, heapDumpFile)
            ContextCompat.startForegroundService(context, intent)
        }
    }
}

可以看到主要的逻辑都在heapAnalyzer.checkForLeaks中了

fun checkForLeaks(*): HeapAnalysis {
    val analysisStartNanoTime = System.nanoTime()
    listener.onProgressUpdate(READING_HEAP_DUMP_FILE)
    try {
        HprofParser.open(heapDumpFile)
                .use { parser ->
                    listener.onProgressUpdate(SCANNING_HEAP_DUMP)
                    // 找出所有的keyedWeakReference引用
                    val (gcRootIds, keyedWeakReferenceInstances, cleaners) = scan(
                            parser, computeRetainedHeapSize
                    )
                    val analysisResults = mutableMapOf<String, RetainedInstance>()
                    listener.onProgressUpdate(FINDING_WATCHED_REFERENCES)
                    // 获取在堆转储文件生成前保存在store里面的内容,为什么不直接调用?
                    // 可能考虑现在时刻数据已经发生变化
                    val (retainedKeys, heapDumpUptimeMillis) = readHeapDumpMemoryStore(parser)
                    // 找出发生泄露的引用
                    val leakingWeakRefs =
                            findLeakingReferences(
                                    parser, retainedKeys, analysisResults, keyedWeakReferenceInstances,
                                    heapDumpUptimeMillis
                            )
                     // 找出泄露引用的最短引用链
                    val (pathResults, dominatedInstances) =
                            findShortestPaths(
                                    parser, exclusionsFactory, leakingWeakRefs, gcRootIds,
                                    computeRetainedHeapSize
                            )
                    // 计算该泄露引用的大小 
                    val retainedSizes = if (computeRetainedHeapSize) {
                        computeRetainedSizes(parser, pathResults, dominatedInstances, cleaners)
                    } else {
                        null
                    }
                    buildLeakTraces(
                            reachabilityInspectors, labelers, pathResults, parser,
                            leakingWeakRefs, analysisResults, retainedSizes
                    )
                    addRemainingInstancesWithNoPath(parser, leakingWeakRefs, analysisResults)
                    return HeapAnalysisSuccess(
                            heapDumpFile, System.currentTimeMillis(), since(analysisStartNanoTime),
                            analysisResults.values.toList()
                    )
                }
    } catch (exception: Throwable) {
        return HeapAnalysisFailure(
                heapDumpFile, System.currentTimeMillis(), since(analysisStartNanoTime),
                HeapAnalysisException(exception)
        )
    }
}

当分析堆转储文件完毕后会回到onHandleIntentInForeground继续执行analysisResultListener

LeakCanary.config#analysisResultListener

object DefaultAnalysisResultListener : AnalysisResultListener {
    override fun invoke(
            application: Application,
            heapAnalysis: HeapAnalysis) {
    val movedHeapDump = renameHeapdump(heapAnalysis.heapDumpFile)
    val updatedHeapAnalysis = when (heapAnalysis) {
        is HeapAnalysisFailure -> heapAnalysis.copy(heapDumpFile = movedHeapDump)
        is HeapAnalysisSuccess -> heapAnalysis.copy(heapDumpFile = movedHeapDump)
    }
    val (id, groupProjections) = LeaksDbHelper(application)
            .writableDatabase.use { db ->
        val id = HeapAnalysisTable.insert(db, updatedHeapAnalysis)
        id to LeakingInstanceTable.retrieveAllByHeapAnalysisId(db, id)
    }
    val (contentTitle, screenToShow) = ...
    val pendingIntent = LeakActivity.createPendingIntent(
            application, arrayListOf(GroupListScreen(), HeapAnalysisListScreen(), screenToShow)
    )
    val contentText = application.getString(R.string.leak_canary_notification_message)
    Notifications.showNotification(
            application, contentTitle, contentText, pendingIntent,
            R.id.leak_canary_notification_analysis_result,
            LEAKCANARY_RESULT
    )
}

方法内部主要就是保存数据到数据库,显示一个Notification,点击后跳转到LeakActivity显示Screens的内容。接着上篇文章最后还说到了当RefWatcher内部往retainedReferences内部添加一个对象时都会回调onReferenceRetained,这做的事情跟可见度从可见变成不可见的事情一样

总结

  • LeakSentry默认使用RefWatcher观察将要被销毁的Activity、Fragment(用户也可自定义观察对象),在被观察5秒后如果对象还没被回收,并且保留的对象数量大于临界值或者应用不可见超过5秒那么就会手动运行一下GC,运行后如果还满足那么会生成内存堆转储快照,然后开启一个前台服务(IntentService)去解析该快照,然后将解析结果与原先被观察但还没回收的对象进行对比,找出真正泄露的对象,比给出最短引用链,当App从可见到不可见时也同样会触发该逻辑