diff --git a/leakcanary-analyzer-core/src/main/java/leakcanary/LeakTraceElement.kt b/leakcanary-analyzer-core/src/main/java/leakcanary/LeakTraceElement.kt index d4059a59fc..0a5eb3b412 100644 --- a/leakcanary-analyzer-core/src/main/java/leakcanary/LeakTraceElement.kt +++ b/leakcanary-analyzer-core/src/main/java/leakcanary/LeakTraceElement.kt @@ -18,14 +18,18 @@ data class LeakTraceElement( */ val classHierarchy: List, - /** Additional information, may be null. */ - val extra: String?, - /** If not null, there was no path that could exclude this element. */ val exclusion: Exclusion?, /** List of all fields (member and static) for that object. */ - val fieldReferences: List + @Deprecated("This field will be replaced with the parser itself") + val fieldReferences: List, + + /** + * Ordered labels that were computed during analysis. A label provides + * extra information that helps understand the leak trace element. + */ + val labels: List ) : Serializable { val className: String diff --git a/leakcanary-analyzer-core/src/main/java/leakcanary/internal/LeakTraceRenderer.kt b/leakcanary-analyzer-core/src/main/java/leakcanary/internal/LeakTraceRenderer.kt index 565e8f1c62..83d0b96579 100644 --- a/leakcanary-analyzer-core/src/main/java/leakcanary/internal/LeakTraceRenderer.kt +++ b/leakcanary-analyzer-core/src/main/java/leakcanary/internal/LeakTraceRenderer.kt @@ -20,7 +20,9 @@ fun LeakTrace.renderToString(): String { val currentReachability = expectedReachability[index] leakInfo += """ #├─ ${leakTraceElement.className} - #│ Leaking: ${currentReachability.renderToString()} + #│ Leaking: ${currentReachability.renderToString()}${if (leakTraceElement.labels.isNotEmpty()) leakTraceElement.labels.joinToString( + "\n│ ", prefix = "\n│ " + ) else ""} #│ ↓ ${getNextElementString(this, leakTraceElement, index)} #""".trimMargin("#") } @@ -54,7 +56,6 @@ private fun getNextElementString( } else "" val simpleClassName = element.getSimpleClassName() val referenceName = if (element.reference != null) ".${element.reference.displayName}" else "" - val extraString = if (element.extra != null) " ${element.extra}" else "" val exclusionString = if (element.exclusion != null) " , matching exclusion ${element.exclusion.matching}" else "" val requiredSpaces = @@ -67,7 +68,7 @@ private fun getNextElementString( "" } - return staticString + holderString + simpleClassName + referenceName + extraString + exclusionString + leakString + return staticString + holderString + simpleClassName + referenceName + exclusionString + leakString } private const val ZERO_WIDTH_SPACE = '\u200b' diff --git a/leakcanary-analyzer-perflib/src/main/java/leakcanary/internal/perflib/PerflibHeapAnalyzer.kt b/leakcanary-analyzer-perflib/src/main/java/leakcanary/internal/perflib/PerflibHeapAnalyzer.kt index 2072ab8a8b..f837144eb6 100644 --- a/leakcanary-analyzer-perflib/src/main/java/leakcanary/internal/perflib/PerflibHeapAnalyzer.kt +++ b/leakcanary-analyzer-perflib/src/main/java/leakcanary/internal/perflib/PerflibHeapAnalyzer.kt @@ -585,9 +585,9 @@ class PerflibHeapAnalyzer @TestOnly internal constructor( holderType = OBJECT } } + val labels = if (extra == null) emptyList() else mutableListOf(extra) return LeakTraceElement( - node.leakReference, holderType, classHierarchy, extra, - node.exclusion, leakReferences + node.leakReference, holderType, classHierarchy, node.exclusion, leakReferences, labels ) } diff --git a/leakcanary-analyzer-perflib/src/test/java/leakcanary/internal/AsyncTaskLeakTest.kt b/leakcanary-analyzer-perflib/src/test/java/leakcanary/internal/AsyncTaskLeakTest.kt index 7e5ad1fe3c..649ba5142e 100644 --- a/leakcanary-analyzer-perflib/src/test/java/leakcanary/internal/AsyncTaskLeakTest.kt +++ b/leakcanary-analyzer-perflib/src/test/java/leakcanary/internal/AsyncTaskLeakTest.kt @@ -2,11 +2,11 @@ package leakcanary.internal import leakcanary.ExcludedRefs import leakcanary.ExcludedRefs.BuilderWithParams +import leakcanary.LeakTraceElement.Holder.THREAD +import leakcanary.LeakTraceElement.Type.STATIC_FIELD import leakcanary.internal.HeapDumpFile.ASYNC_TASK_M import leakcanary.internal.HeapDumpFile.ASYNC_TASK_O import leakcanary.internal.HeapDumpFile.ASYNC_TASK_PRE_M -import leakcanary.LeakTraceElement.Holder.THREAD -import leakcanary.LeakTraceElement.Type.STATIC_FIELD import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test @@ -40,9 +40,6 @@ internal class AsyncTaskLeakTest(private val heapDumpFile: HeapDumpFile) { val gcRoot = result.leakTrace!!.elements[0] assertThat(Thread::class.java.name).isEqualTo(gcRoot.className) assertThat(THREAD).isEqualTo(gcRoot.holder) - assertThat(gcRoot.extra).contains( - ASYNC_TASK_THREAD - ) } @Test diff --git a/leakcanary-analyzer/src/main/java/leakcanary/HeapAnalyzer.kt b/leakcanary-analyzer/src/main/java/leakcanary/HeapAnalyzer.kt index 872e98aca4..bc49a64146 100644 --- a/leakcanary-analyzer/src/main/java/leakcanary/HeapAnalyzer.kt +++ b/leakcanary-analyzer/src/main/java/leakcanary/HeapAnalyzer.kt @@ -41,7 +41,7 @@ import leakcanary.HeapValue.LongValue import leakcanary.HeapValue.ObjectReference import leakcanary.HeapValue.ShortValue import leakcanary.HprofParser.RecordCallbacks -import leakcanary.LeakTraceElement.Holder +import leakcanary.LeakNode.ChildNode import leakcanary.LeakTraceElement.Holder.ARRAY import leakcanary.LeakTraceElement.Holder.CLASS import leakcanary.LeakTraceElement.Holder.OBJECT @@ -60,8 +60,6 @@ import leakcanary.Record.HeapDumpRecord.ObjectRecord.ObjectArrayDumpRecord import leakcanary.Record.LoadClassRecord import leakcanary.Record.StringRecord import leakcanary.internal.KeyedWeakReferenceMirror -import leakcanary.internal.LeakNode -import leakcanary.internal.LeakNode.ChildNode import leakcanary.internal.ShortestPathFinder import leakcanary.internal.ShortestPathFinder.Result import java.util.ArrayList @@ -79,7 +77,8 @@ class HeapAnalyzer constructor( * and then computes the shortest strong reference path from that instance to the GC roots. */ fun checkForLeaks( - heapDump: HeapDump + heapDump: HeapDump, + labelers: List ): HeapAnalysis { val analysisStartNanoTime = System.nanoTime() @@ -121,7 +120,9 @@ class HeapAnalyzer constructor( val pathResults = findShortestPaths(heapDump, parser, leakingWeakRefs, gcRootIds) - buildLeakTraces(heapDump, pathResults, parser, leakingWeakRefs, analysisResults) + buildLeakTraces( + heapDump, labelers, pathResults, parser, leakingWeakRefs, analysisResults + ) addRemainingInstancesWithNoPath(parser, leakingWeakRefs, analysisResults) @@ -247,7 +248,7 @@ class HeapAnalyzer constructor( parser: HprofParser, leakingWeakRefs: List, gcRootIds: MutableList - ): List { + ): List { listener.onProgressUpdate(FINDING_SHORTEST_PATHS) val pathFinder = ShortestPathFinder(heapDump.excludedRefs) @@ -256,6 +257,7 @@ class HeapAnalyzer constructor( private fun buildLeakTraces( heapDump: HeapDump, + labelers: List, pathResults: List, parser: HprofParser, leakingWeakRefs: MutableList, @@ -278,7 +280,7 @@ class HeapAnalyzer constructor( ) } - val leakTrace = buildLeakTrace(parser, heapDump, pathResult.leakingNode) + val leakTrace = buildLeakTrace(parser, heapDump, pathResult.leakingNode, labelers) // TODO Compute retained heap size val retainedSize = null @@ -310,7 +312,8 @@ class HeapAnalyzer constructor( private fun buildLeakTrace( parser: HprofParser, heapDump: HeapDump, - leakingNode: LeakNode + leakingNode: LeakNode, + labelers: List ): LeakTrace { val elements = ArrayList() // We iterate from the leak to the GC root @@ -318,10 +321,11 @@ class HeapAnalyzer constructor( var node: LeakNode? = ChildNode(ignored, null, leakingNode, null) while (node is ChildNode) { - val element = buildLeakElement(parser, node) - if (element != null) { - elements.add(0, element) + val labels = mutableListOf() + for (labeler in labelers) { + labels.addAll(labeler.computeLabels(parser, node)) } + elements.add(0, buildLeakElement(parser, node, labels)) node = node.parent } @@ -368,7 +372,7 @@ class HeapAnalyzer constructor( expectedReachability[0] = Reachability.reachable("it's a GC root") } - when(expectedReachability[lastElementIndex].status) { + when (expectedReachability[lastElementIndex].status) { UNKNOWN, REACHABLE -> { expectedReachability[lastElementIndex] = Reachability.unreachable("RefWatcher was watching this") @@ -415,13 +419,11 @@ class HeapAnalyzer constructor( private fun buildLeakElement( parser: HprofParser, - node: ChildNode - ): LeakTraceElement? { + node: ChildNode, + labels: List + ): LeakTraceElement { val objectId = node.parent.instance - val holderType: Holder - var extra: String? = null - val record = parser.retrieveRecordById(objectId) val leakReferences = describeFields(parser, record) @@ -436,55 +438,19 @@ class HeapAnalyzer constructor( else -> throw IllegalStateException("Unexpected record type for $record") } - val className = classHierarchy[0] - - if (record is ClassDumpRecord) { - holderType = CLASS + val holderType = if (record is ClassDumpRecord) { + CLASS } else if (record is ObjectArrayDumpRecord) { - holderType = ARRAY + ARRAY } else { - - val instance = parser.hydrateInstance(record as InstanceDumpRecord) - - if (instance.classHierarchy.any { it.className == Thread::class.java.name }) { - holderType = THREAD - val nameField = instance.fieldValueOrNull("name") - // Sometimes we can't find the String at the expected memory address in the heap dump. - // See https://github.com/square/leakcanary/issues/417 - val threadName = - if (nameField != null) parser.retrieveString(nameField) else "Thread name not available" - extra = "(named '$threadName')" - } else if (className.matches(ANONYMOUS_CLASS_NAME_PATTERN.toRegex())) { - - val parentClassName = instance.classHierarchy[1].className - if (parentClassName == "java.lang.Object") { - holderType = OBJECT - try { - // This is an anonymous class implementing an interface. The API does not give access - // to the interfaces implemented by the class. We check if it's in the class path and - // use that instead. - val actualClass = Class.forName(instance.classHierarchy[0].className) - val interfaces = actualClass.interfaces - extra = if (interfaces.isNotEmpty()) { - val implementedInterface = interfaces[0] - "(anonymous implementation of " + implementedInterface.name + ")" - } else { - "(anonymous subclass of java.lang.Object)" - } - } catch (ignored: ClassNotFoundException) { - } - } else { - holderType = OBJECT - // Makes it easier to figure out which anonymous class we're looking at. - extra = "(anonymous subclass of $parentClassName)" - } + if (classHierarchy.any { it == Thread::class.java.name }) { + THREAD } else { - holderType = OBJECT + OBJECT } } return LeakTraceElement( - node.leakReference, holderType, classHierarchy, extra, - node.exclusion, leakReferences + node.leakReference, holderType, classHierarchy, node.exclusion, leakReferences, labels ) } @@ -556,7 +522,7 @@ class HeapAnalyzer constructor( } companion object { - - private const val ANONYMOUS_CLASS_NAME_PATTERN = "^.+\\$\\d+$" + internal const val ANONYMOUS_CLASS_NAME_PATTERN = "^.+\\$\\d+$" + internal val ANONYMOUS_CLASS_NAME_PATTERN_REGEX = ANONYMOUS_CLASS_NAME_PATTERN.toRegex() } } diff --git a/leakcanary-analyzer/src/main/java/leakcanary/Labeler.kt b/leakcanary-analyzer/src/main/java/leakcanary/Labeler.kt new file mode 100644 index 0000000000..855d07b519 --- /dev/null +++ b/leakcanary-analyzer/src/main/java/leakcanary/Labeler.kt @@ -0,0 +1,68 @@ +package leakcanary + +import leakcanary.HeapValue.ObjectReference +import leakcanary.LeakNode.ChildNode +import leakcanary.Record.HeapDumpRecord.ObjectRecord.InstanceDumpRecord + +interface Labeler { + + /** + * Note: this is a bit confusing but for a given node you should really be printing attributes + * of node.parent.instance + * TODO Make this less confusing. + */ + fun computeLabels( + parser: HprofParser, + node: ChildNode + ): List + + object InstanceDefaultLabeler : Labeler { + override fun computeLabels( + parser: HprofParser, + node: ChildNode + ): List { + val objectId = node.parent.instance + val record = parser.retrieveRecordById(objectId) + if (record is InstanceDumpRecord) { + val labels = mutableListOf() + val instance = parser.hydrateInstance(record) + val className = instance.classHierarchy[0].className + + if (instance.classHierarchy.any { it.className == Thread::class.java.name }) { + val nameField = instance.fieldValueOrNull("name") + // Sometimes we can't find the String at the expected memory address in the heap dump. + // See https://github.com/square/leakcanary/issues/417 + val threadName = + if (nameField != null) parser.retrieveString(nameField) else "not available" + labels.add("Thread name: '$threadName'") + } else if (className.matches(HeapAnalyzer.ANONYMOUS_CLASS_NAME_PATTERN_REGEX)) { + val parentClassName = instance.classHierarchy[1].className + if (parentClassName == "java.lang.Object") { + try { + // This is an anonymous class implementing an interface. The API does not give access + // to the interfaces implemented by the class. We check if it's in the class path and + // use that instead. + val actualClass = Class.forName(instance.classHierarchy[0].className) + val interfaces = actualClass.interfaces + labels.add( + if (interfaces.isNotEmpty()) { + val implementedInterface = interfaces[0] + "Anonymous class implementing ${implementedInterface.name}" + } else { + "Anonymous subclass of java.lang.Object" + } + ) + } catch (ignored: ClassNotFoundException) { + } + } else { + // Makes it easier to figure out which anonymous class we're looking at. + labels.add("Anonymous subclass of $parentClassName") + } + } + return labels + } else { + return emptyList() + } + } + } +} \ No newline at end of file diff --git a/leakcanary-analyzer/src/main/java/leakcanary/internal/LeakNode.kt b/leakcanary-analyzer/src/main/java/leakcanary/LeakNode.kt similarity index 68% rename from leakcanary-analyzer/src/main/java/leakcanary/internal/LeakNode.kt rename to leakcanary-analyzer/src/main/java/leakcanary/LeakNode.kt index 3d40548758..3a3d7e99fc 100644 --- a/leakcanary-analyzer/src/main/java/leakcanary/internal/LeakNode.kt +++ b/leakcanary-analyzer/src/main/java/leakcanary/LeakNode.kt @@ -1,9 +1,6 @@ -package leakcanary.internal +package leakcanary -import leakcanary.Exclusion -import leakcanary.LeakReference - -internal sealed class LeakNode { +sealed class LeakNode { abstract val instance: Long class RootNode( diff --git a/leakcanary-analyzer/src/main/java/leakcanary/internal/ShortestPathFinder.kt b/leakcanary-analyzer/src/main/java/leakcanary/internal/ShortestPathFinder.kt index 62a6814170..52e5bff82e 100644 --- a/leakcanary-analyzer/src/main/java/leakcanary/internal/ShortestPathFinder.kt +++ b/leakcanary-analyzer/src/main/java/leakcanary/internal/ShortestPathFinder.kt @@ -20,6 +20,7 @@ import leakcanary.Exclusion import leakcanary.HeapValue import leakcanary.HeapValue.ObjectReference import leakcanary.HprofParser +import leakcanary.LeakNode import leakcanary.LeakReference import leakcanary.LeakTraceElement.Type.ARRAY_ENTRY import leakcanary.LeakTraceElement.Type.INSTANCE_FIELD @@ -31,8 +32,8 @@ import leakcanary.ObjectIdMetadata.STRING import leakcanary.Record.HeapDumpRecord.ObjectRecord.ClassDumpRecord import leakcanary.Record.HeapDumpRecord.ObjectRecord.InstanceDumpRecord import leakcanary.Record.HeapDumpRecord.ObjectRecord.ObjectArrayDumpRecord -import leakcanary.internal.LeakNode.ChildNode -import leakcanary.internal.LeakNode.RootNode +import leakcanary.LeakNode.ChildNode +import leakcanary.LeakNode.RootNode import java.util.ArrayDeque import java.util.Deque import java.util.LinkedHashMap diff --git a/leakcanary-analyzer/src/test/java/leakcanary/internal/HeapAnalyzerTest.kt b/leakcanary-analyzer/src/test/java/leakcanary/internal/HeapAnalyzerTest.kt index cb58f5f6ed..4593d28e59 100644 --- a/leakcanary-analyzer/src/test/java/leakcanary/internal/HeapAnalyzerTest.kt +++ b/leakcanary-analyzer/src/test/java/leakcanary/internal/HeapAnalyzerTest.kt @@ -46,7 +46,7 @@ class HeapAnalyzerTest { val heapDump = HeapDump.builder(file) .excludedRefs(defaultExcludedRefs.build()) .build() - val leaks = heapAnalyzer.checkForLeaks(heapDump) + val leaks = heapAnalyzer.checkForLeaks(heapDump, emptyList()) val now = System.nanoTime() val elapsed = (now - time) / 1000000 diff --git a/leakcanary-android-core/src/main/java/leakcanary/AndroidLabelers.kt b/leakcanary-android-core/src/main/java/leakcanary/AndroidLabelers.kt new file mode 100644 index 0000000000..79b0379b01 --- /dev/null +++ b/leakcanary-android-core/src/main/java/leakcanary/AndroidLabelers.kt @@ -0,0 +1,89 @@ +package leakcanary + +import android.app.Application +import android.content.res.Resources.NotFoundException +import android.view.View +import leakcanary.HeapValue.IntValue +import leakcanary.HeapValue.ObjectReference +import leakcanary.LeakNode.ChildNode +import leakcanary.Record.HeapDumpRecord.ObjectRecord.InstanceDumpRecord +import java.util.ArrayList + +enum class AndroidLabelers : Labeler { + + FRAGMENT_LABELER { + override fun computeLabels( + parser: HprofParser, + node: ChildNode + ): List { + val objectId = node.parent.instance + val record = parser.retrieveRecordById(objectId) + if (record is InstanceDumpRecord) { + val className = parser.className(record.classId) + if (className == "androidx.fragment.app.Fragment" || className == "android.app.Fragment") { + val instance = parser.hydrateInstance(record) + val mTag = instance.fieldValueOrNull("mTag") + if (mTag is ObjectReference && !mTag.isNull) { + val mTag = parser.retrieveString(mTag) + if (mTag.isNotEmpty()) { + return listOf("Fragment.mTag=$mTag") + } + } + } + } + return emptyList() + } + }; + + class ViewLabeler( + private val application: Application + ) : Labeler { + override fun computeLabels( + parser: HprofParser, + node: ChildNode + ): List { + val objectId = node.parent.instance + val record = parser.retrieveRecordById(objectId) + if (record is InstanceDumpRecord) { + val instance = parser.hydrateInstance(record) + if (instance.isInstanceOf(View::class.java.name)) { + val viewLabels = mutableListOf() + val mID = instance.fieldValueOrNull("mID") + if (mID is IntValue) { + if (mID.value != 0) { + try { + val name = application.resources.getResourceEntryName(mID.value) + viewLabels.add("View.mID=R.id.$name (${mID.value})") + } catch (ignored: NotFoundException) { + viewLabels.add("View.mID=${mID.value} (name not found)") + } + } else { + viewLabels.add("View.mID=0") + } + } + val mWindowAttachCount = instance.fieldValueOrNull("mWindowAttachCount") + if (mWindowAttachCount is IntValue) { + viewLabels.add("View.mWindowAttachCount=${mWindowAttachCount.value}") + } + return viewLabels + } + } + return emptyList() + } + } + + companion object { + fun defaultAndroidLabelers(application: Application): List { + val labelers = ArrayList() + labelers.add(Labeler.InstanceDefaultLabeler) + labelers.add( + ViewLabeler( + application + ) + ) + labelers.addAll(values()) + return labelers + } + } + +} \ No newline at end of file diff --git a/leakcanary-android-core/src/main/java/leakcanary/AndroidReachabilityInspectors.kt b/leakcanary-android-core/src/main/java/leakcanary/AndroidReachabilityInspectors.kt index b997d13ea6..506db68f7a 100644 --- a/leakcanary-android-core/src/main/java/leakcanary/AndroidReachabilityInspectors.kt +++ b/leakcanary-android-core/src/main/java/leakcanary/AndroidReachabilityInspectors.kt @@ -102,7 +102,7 @@ enum class AndroidReachabilityInspectors(private val inspectorClass: Class> = AndroidReachabilityInspectors.defaultAndroidInspectors(), + val labelers: List = AndroidLabelers.defaultAndroidLabelers( + InternalLeakCanary.application + ), /** * Note: this is currently not implemented in the new heap parser. */ diff --git a/leakcanary-android-core/src/main/java/leakcanary/internal/DisplayLeakAdapter.kt b/leakcanary-android-core/src/main/java/leakcanary/internal/DisplayLeakAdapter.kt index 552e0afbdb..298ff94935 100644 --- a/leakcanary-android-core/src/main/java/leakcanary/internal/DisplayLeakAdapter.kt +++ b/leakcanary-android-core/src/main/java/leakcanary/internal/DisplayLeakAdapter.kt @@ -233,8 +233,12 @@ internal class DisplayLeakAdapter private constructor( UNREACHABLE -> "YES (${reachability.reason})" } - htmlString += "    Leaking: $reachabilityString
" + val indentation = " ".repeat(4) + htmlString += "$indentationLeaking: $reachabilityString
" + element.labels.forEach { label -> + htmlString += "$indentation$label
" + } val reference = element.reference if (reference != null) { @@ -251,7 +255,7 @@ internal class DisplayLeakAdapter private constructor( referenceName = "$referenceName" } - htmlString += "    $styledClassName.${if (maybeLeakCause) "$referenceName" else referenceName}" + htmlString += "$indentation$styledClassName.${if (maybeLeakCause) "$referenceName" else referenceName}" } val exclusion = element.exclusion @@ -271,10 +275,6 @@ internal class DisplayLeakAdapter private constructor( element: LeakTraceElement ): Spanned { var htmlString = "" - if (element.extra != null) { - htmlString += " " + element.extra + "" - } - val exclusion = element.exclusion if (exclusion != null) { htmlString += "

Excluded by rule" diff --git a/leakcanary-android-core/src/main/java/leakcanary/internal/InternalLeakCanary.kt b/leakcanary-android-core/src/main/java/leakcanary/internal/InternalLeakCanary.kt index 8c53b77e93..30b668c5a0 100644 --- a/leakcanary-android-core/src/main/java/leakcanary/internal/InternalLeakCanary.kt +++ b/leakcanary-android-core/src/main/java/leakcanary/internal/InternalLeakCanary.kt @@ -19,13 +19,11 @@ import android.util.Log import com.squareup.leakcanary.core.BuildConfig import com.squareup.leakcanary.core.R import leakcanary.AnalysisResult -import leakcanary.CanaryLog import leakcanary.GcTrigger import leakcanary.HeapDump import leakcanary.LeakCanary import leakcanary.LeakSentry import leakcanary.internal.activity.LeakActivity -import java.lang.Exception internal object InternalLeakCanary : LeakSentryListener { @@ -33,9 +31,12 @@ internal object InternalLeakCanary : LeakSentryListener { private lateinit var heapDumpTrigger: HeapDumpTrigger + lateinit var application: Application + @Volatile private var isInAnalyzerProcess: Boolean? = null override fun onLeakSentryInstalled(application: Application) { + this.application = application if (isInAnalyzerProcess(application)) { return } diff --git a/leakcanary-android-instrumentation/src/androidTest/java/leakcanary/HeapAnalyzerComparisonTest.kt b/leakcanary-android-instrumentation/src/androidTest/java/leakcanary/HeapAnalyzerComparisonTest.kt index 6725ecc5e7..6761a92189 100644 --- a/leakcanary-android-instrumentation/src/androidTest/java/leakcanary/HeapAnalyzerComparisonTest.kt +++ b/leakcanary-android-instrumentation/src/androidTest/java/leakcanary/HeapAnalyzerComparisonTest.kt @@ -111,7 +111,7 @@ class HeapAnalyzerComparisonTest { countMemory2.run() val secondAnalysis = HeapAnalyzer(listener) - .checkForLeaks(heapDump) as HeapAnalysisSuccess + .checkForLeaks(heapDump, LeakCanary.config.labelers) as HeapAnalysisSuccess val memoryUsedSecondInMb = (secondMaxMemoryUsed - memoryBeforeSecond) / 1048576L CanaryLog.d( diff --git a/leakcanary-android-instrumentation/src/main/java/leakcanary/InstrumentationLeakDetector.kt b/leakcanary-android-instrumentation/src/main/java/leakcanary/InstrumentationLeakDetector.kt index be64a41081..14f13cb0e0 100644 --- a/leakcanary-android-instrumentation/src/main/java/leakcanary/InstrumentationLeakDetector.kt +++ b/leakcanary-android-instrumentation/src/main/java/leakcanary/InstrumentationLeakDetector.kt @@ -180,7 +180,7 @@ class InstrumentationLeakDetector { val listener = AnalyzerProgressListener.NONE val heapAnalyzer = HeapAnalyzer(listener) - val heapAnalysis = heapAnalyzer.checkForLeaks(heapDump) + val heapAnalysis = heapAnalyzer.checkForLeaks(heapDump, config.labelers) CanaryLog.d("Heap Analysis:\n%s", heapAnalysis) diff --git a/leakcanary-android/src/main/java/leakcanary/internal/HeapAnalyzerService.kt b/leakcanary-android/src/main/java/leakcanary/internal/HeapAnalyzerService.kt index b8ca4ced6c..6d3eb29282 100644 --- a/leakcanary-android/src/main/java/leakcanary/internal/HeapAnalyzerService.kt +++ b/leakcanary-android/src/main/java/leakcanary/internal/HeapAnalyzerService.kt @@ -19,8 +19,9 @@ import android.content.Intent import com.squareup.leakcanary.R import leakcanary.AnalyzerProgressListener import leakcanary.CanaryLog -import leakcanary.HeapDump import leakcanary.HeapAnalyzer +import leakcanary.HeapDump +import leakcanary.LeakCanary /** * This service runs in a main app process. @@ -37,7 +38,7 @@ internal class HeapAnalyzerService : ForegroundService( } val heapDump = intent.getSerializableExtra(HeapAnalyzers.HEAPDUMP_EXTRA) as HeapDump val heapAnalyzer = HeapAnalyzer(this) - val heapAnalysis = heapAnalyzer.checkForLeaks(heapDump) + val heapAnalysis = heapAnalyzer.checkForLeaks(heapDump, LeakCanary.config.labelers) AnalysisResultService.sendResult(this, heapAnalysis) } diff --git a/leakcanary-haha/src/main/java/leakcanary/HeapValue.kt b/leakcanary-haha/src/main/java/leakcanary/HeapValue.kt index 0002d859b8..901ab2a37a 100644 --- a/leakcanary-haha/src/main/java/leakcanary/HeapValue.kt +++ b/leakcanary-haha/src/main/java/leakcanary/HeapValue.kt @@ -1,7 +1,11 @@ package leakcanary sealed class HeapValue { - data class ObjectReference(val value: Long) : HeapValue() + data class ObjectReference(val value: Long) : HeapValue() { + val isNull + get() = value == 0L + } + data class BooleanValue(val value: Boolean) : HeapValue() data class CharValue(val value: Char) : HeapValue() data class FloatValue(val value: Float) : HeapValue() diff --git a/leakcanary-haha/src/main/java/leakcanary/HprofParser.kt b/leakcanary-haha/src/main/java/leakcanary/HprofParser.kt index 8f4defef7d..e44bbc7e23 100644 --- a/leakcanary-haha/src/main/java/leakcanary/HprofParser.kt +++ b/leakcanary-haha/src/main/java/leakcanary/HprofParser.kt @@ -715,6 +715,10 @@ class HprofParser private constructor( return objectRecord } + /** + * Note: it's reasonable to call this repeatedly, there will be no disk reads as long as + * the classes are in the [objectCache] LruCache + */ fun hydrateClassHierarchy(classId: Long): List { var currentClassId = classId val classHierarchy = mutableListOf() @@ -741,6 +745,10 @@ class HprofParser private constructor( return classHierarchy } + /** + * Note: it's reasonable to call this repeatedly, there will be no disk reads as long as + * the classes are in the [objectCache] LruCache + */ fun hydrateInstance(instanceRecord: InstanceDumpRecord): HydratedInstance { val classHierarchy = hydrateClassHierarchy(instanceRecord.classId) diff --git a/leakcanary-haha/src/main/java/leakcanary/HydratedInstance.kt b/leakcanary-haha/src/main/java/leakcanary/HydratedInstance.kt index da32a8c842..a87c2a0e2a 100644 --- a/leakcanary-haha/src/main/java/leakcanary/HydratedInstance.kt +++ b/leakcanary-haha/src/main/java/leakcanary/HydratedInstance.kt @@ -38,4 +38,8 @@ class HydratedInstance( } return false } + + fun isInstanceOf(className: String): Boolean { + return classHierarchy.any { it.className == className } + } } \ No newline at end of file