Skip to content

Commit b5b4703

Browse files
authored
[Android] Fix JankStats memory leak due to picking Dialog window (#705)
* [Android] Fix JankStats memory leak due to picking Dialog window * [Android] Fix JankStats memory leak due to picking Dialog window * Iterate through all views and find first not destroyed activity * Update docs * Make sure to stop listener after initial ON_CREATE detection * Add missing min sdk level for platform/jvm/common module
1 parent ad45c9f commit b5b4703

File tree

6 files changed

+54
-17
lines changed

6 files changed

+54
-17
lines changed

platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/events/performance/JankStatsMonitor.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,11 @@ internal class JankStatsMonitor(
132132
return
133133
}
134134
if (event == Lifecycle.Event.ON_CREATE) {
135-
windowManager.getCurrentWindow()?.let {
136-
setJankStatsForCurrentWindow(it)
137-
// We are done detecting initial Application ON_CREATE, we don't need to listen anymore
138-
processLifecycleOwner.lifecycle.removeObserver(this)
135+
windowManager.findFirstValidActivity()?.let {
136+
setJankStatsForCurrentWindow(it.window)
139137
}
138+
// We are done detecting initial Application ON_CREATE, we don't need to listen anymore
139+
processLifecycleOwner.lifecycle.removeObserver(this)
140140
}
141141
}
142142

platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/events/performance/JankStatsMonitorTest.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,7 @@ class JankStatsMonitorTest {
6969
window = activity.window
7070

7171
whenever(processLifecycleOwner.lifecycle).thenReturn(lifecycle)
72-
whenever(windowManager.getCurrentWindow()).thenReturn(window)
73-
whenever(windowManager.getFirstRootView()).thenReturn(window.decorView)
72+
whenever(windowManager.findFirstValidActivity()).thenReturn(activity)
7473

7574
whenever(runtime.isEnabled(RuntimeFeature.DROPPED_EVENTS_MONITORING)).thenReturn(true)
7675
whenever(runtime.getConfigValue(RuntimeConfig.MIN_JANK_FRAME_THRESHOLD_MS)).thenReturn(16)

platform/jvm/common/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ android {
1313

1414
compileSdk = 36
1515

16+
defaultConfig {
17+
minSdk = 23
18+
consumerProguardFiles("consumer-rules.pro")
19+
}
20+
1621
compileOptions {
1722
sourceCompatibility = JavaVersion.VERSION_1_8
1823
targetCompatibility = JavaVersion.VERSION_1_8

platform/jvm/common/src/main/kotlin/io/bitdrift/capture/common/IWindowManager.kt

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,13 @@
77

88
package io.bitdrift.capture.common
99

10+
import android.app.Activity
1011
import android.view.View
11-
import android.view.Window
1212

1313
/**
1414
* Provides access the Global Window Views, or `null` if no window is available.
1515
*/
1616
interface IWindowManager {
17-
/**
18-
* Returns the current [Window] if available.
19-
*
20-
* @return The current [Window], or `null` if no window is available.
21-
*/
22-
fun getCurrentWindow(): Window?
23-
2417
/**
2518
* Returns the root view of the current window if available.
2619
*
@@ -34,4 +27,16 @@ interface IWindowManager {
3427
* @return The root views of the current hierarchy, or an empty list if not available.
3528
*/
3629
fun getAllRootViews(): List<View>
30+
31+
/**
32+
* Finds the first valid (non-destroyed) activity from all available root views.
33+
*
34+
* For most cases, this returns the currently visible activity.
35+
*
36+
* When multiple activities are present (split-screen, PiP, etc), this returns
37+
* the first valid activity found in the iteration order.
38+
*
39+
* @return The first valid [android.app.Activity], or `null` if none found
40+
*/
41+
fun findFirstValidActivity(): Activity?
3742
}

platform/jvm/common/src/main/kotlin/io/bitdrift/capture/common/WindowManager.kt

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77

88
package io.bitdrift.capture.common
99

10+
import android.app.Activity
11+
import android.content.Context
12+
import android.content.ContextWrapper
1013
import android.os.Build
1114
import android.view.View
12-
import android.view.Window
1315
import android.view.inspector.WindowInspector
1416

1517
/**
@@ -25,8 +27,6 @@ class WindowManager(
2527
Class.forName("android.view.WindowManagerGlobal")
2628
}
2729

28-
override fun getCurrentWindow(): Window? = getFirstRootView()?.phoneWindow
29-
3030
override fun getFirstRootView(): View? = getAllRootViews().firstOrNull()
3131

3232
private val windowManagerGlobal: Any? by lazy(LazyThreadSafetyMode.NONE) {
@@ -61,4 +61,28 @@ class WindowManager(
6161
return emptyList()
6262
}
6363
}
64+
65+
override fun findFirstValidActivity(): Activity? =
66+
getAllRootViews().firstNotNullOfOrNull { view ->
67+
val activity = view.unwrapToActivity()
68+
if (activity != null && !activity.isDestroyed) {
69+
activity
70+
} else {
71+
null
72+
}
73+
}
74+
75+
private fun View.unwrapToActivity(): Activity? {
76+
val visited = mutableSetOf<Context>()
77+
var current: Context? = WindowSpy.pullWindow(this)?.context
78+
79+
while (current is ContextWrapper) {
80+
if (current is Activity) return current
81+
if (!visited.add(current)) {
82+
return null
83+
}
84+
current = current.baseContext
85+
}
86+
return null
87+
}
6488
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<resources>
3+
<bool name="leak_canary_watcher_watch_dismissed_dialogs">true</bool>
4+
</resources>

0 commit comments

Comments
 (0)