Skip to content

Commit 8da852c

Browse files
romtsnclaude
andauthored
fix(android): Make FirstDrawDoneListener cleanup OnGlobalLayoutListener after use (#5567)
* fix(android): Make FirstDrawDoneListener cleanup OnGlobalLayoutListener after use The OnGlobalLayoutListener registered in onDraw() to defer removal of the OnDrawListener was never itself removed. In single-Activity apps (e.g. React Native), this caused an unbounded per-navigation leak on the ViewTreeObserver, accumulating one listener per registerForNextDraw call. Make the OnGlobalLayoutListener remove itself after firing. Fixes JAVA-545 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * changelog --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9c501bb commit 8da852c

3 files changed

Lines changed: 43 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Fixes
6+
7+
- Fix `FirstDrawDoneListener` leaking an `OnGlobalLayoutListener` per registration ([#5567](https://github.com/getsentry/sentry-java/pull/5567))
8+
59
### Features
610

711
- Add experimental `SentrySQLiteDriver` to `sentry-android-sqlite` for instrumenting `androidx.sqlite.SQLiteDriver` ([#5563](https://github.com/getsentry/sentry-java/pull/5563))

sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FirstDrawDoneListener.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,14 @@ public void onDraw() {
112112
// OnDrawListeners cannot be removed within onDraw, so we remove it with a
113113
// GlobalLayoutListener
114114
view.getViewTreeObserver()
115-
.addOnGlobalLayoutListener(() -> view.getViewTreeObserver().removeOnDrawListener(this));
115+
.addOnGlobalLayoutListener(
116+
new ViewTreeObserver.OnGlobalLayoutListener() {
117+
@Override
118+
public void onGlobalLayout() {
119+
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
120+
view.getViewTreeObserver().removeOnDrawListener(FirstDrawDoneListener.this);
121+
}
122+
});
116123
mainThreadHandler.postAtFrontOfQueue(callback);
117124
}
118125

sentry-android-core/src/test/java/io/sentry/android/core/internal/util/FirstDrawDoneListenerTest.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,37 @@ class FirstDrawDoneListenerTest {
128128
assertTrue(fixture.onDrawListeners.isEmpty())
129129
}
130130

131+
@Test
132+
fun `OnGlobalLayoutListener is removed after cleanup`() {
133+
val view = fixture.getSut()
134+
135+
// Initialize mOnGlobalLayoutListeners via a dummy add/remove
136+
val dummyGlobalListener = ViewTreeObserver.OnGlobalLayoutListener {}
137+
view.viewTreeObserver.addOnGlobalLayoutListener(dummyGlobalListener)
138+
view.viewTreeObserver.removeOnGlobalLayoutListener(dummyGlobalListener)
139+
140+
// CopyOnWriteArray wraps an internal ArrayList called mData
141+
val copyOnWriteArray: Any = view.viewTreeObserver.getProperty("mOnGlobalLayoutListeners")
142+
val mDataField = copyOnWriteArray.javaClass.getDeclaredField("mData")
143+
mDataField.isAccessible = true
144+
145+
@Suppress("UNCHECKED_CAST")
146+
fun globalLayoutListeners(): ArrayList<*> = mDataField.get(copyOnWriteArray) as ArrayList<*>
147+
148+
assertTrue(globalLayoutListeners().isEmpty())
149+
150+
FirstDrawDoneListener.registerForNextDraw(view, {}, fixture.buildInfo)
151+
152+
// onDraw registers a cleanup OnGlobalLayoutListener
153+
view.viewTreeObserver.dispatchOnDraw()
154+
assertFalse(globalLayoutListeners().isEmpty())
155+
156+
// onGlobalLayout fires the cleanup, which removes both the draw and layout listeners
157+
view.viewTreeObserver.dispatchOnGlobalLayout()
158+
assertTrue(globalLayoutListeners().isEmpty())
159+
assertTrue(fixture.onDrawListeners.isEmpty())
160+
}
161+
131162
@Test
132163
fun `registerForNextDraw calls the given callback on the main thread after onDraw`() {
133164
val view = fixture.getSut()

0 commit comments

Comments
 (0)