Commit f10cded
Fix activity context leak in LogBoxDialogSurfaceDelegate (facebook#56232)
Summary:
Pull Request resolved: facebook#56232
## Problem
LeakCanary detected a memory leak in React Native Android apps using LogBox in bridgeless mode where a destroyed Activity is retained through the LogBox surface delegate chain:
```
GC Root: Global variable in native code
│
├─ LogBoxModule instance
│ ↓ LogBoxModule.surfaceDelegate
├─ LogBoxDialogSurfaceDelegate instance
│ ↓ LogBoxDialogSurfaceDelegate.reactRootView
├─ ReactSurfaceView instance
│ View.mContext references a destroyed activity
│ ↓ View.mContext
╰→ Activity instance (mDestroyed = true)
```
## Root Cause
The leak has two contributing factors:
1. **`hide()` doesn't clean up the root view** — `LogBoxDialogSurfaceDelegate.hide()` dismisses the dialog but keeps `reactRootView` alive, retaining a reference to the destroyed Activity via `View.mContext`.
2. **`destroyRootView()` is a no-op in bridgeless mode** — `ReactHostImplDevHelper.destroyRootView()` did nothing, so the ReactSurface was never detached from `ReactHostImpl.attachedSurfaces`.
Factor 2 made Factor 1 unfixable: calling `destroyContentView()` in `hide()` would null `reactRootView`, but the surface remained registered as attached. On the next `show()` call, `createRootView("LogBox")` would return null (because `isSurfaceWithModuleNameAttached("LogBox")` was still true), breaking LogBox reopen entirely.
## Fix
### 1. Implement `destroyRootView()` in `ReactHostImplDevHelper`
The bridgeless `destroyRootView()` was a no-op. The fix casts the root view to `ReactSurfaceView` to access the underlying `ReactSurfaceImpl`, then properly tears it down:
- `surface.stop()` — synchronously removes from `attachedSurfaces`
- `surface.detach()` — nulls host back-reference
- `surface.clear()` — removes child views
To enable this, `ReactSurfaceView.surface` visibility is changed from `private` to `internal` (both classes are in the same `com.facebook.react.runtime` package).
### 2. Add `destroyContentView()` to `hide()` in `LogBoxDialogSurfaceDelegate`
Now that `destroyRootView()` properly detaches the surface, `hide()` safely destroys the content view, breaking the reference chain that retained the destroyed Activity.
### 3. Make `show()` self-sufficient
`show()` now recreates the content view when it's missing (e.g., after `hide()` destroyed it), instead of returning early.
## Why this is safe
1. **LogBox reopen works.** After `hide()` detaches the surface, `createRootView("LogBox")` succeeds because `isSurfaceWithModuleNameAttached("LogBox")` returns false.
2. **`destroyContentView()` is idempotent.** Null-checks `reactRootView` before cleanup.
3. **`destroyRootView()` is safe for non-ReactSurfaceView.** The `as?` cast returns null for bridge-mode `ReactRootView` instances — bridge mode already has its own implementation.
4. **Dev-only impact.** LogBox is excluded from release builds.
5. **`internal` visibility is appropriate.** Both `ReactSurfaceView` and `ReactHostImplDevHelper` are in the same package.
Changelog: [Android][Fixed] - Fixed activity context memory leak in LogBoxDialogSurfaceDelegate when using bridgeless mode
Reviewed By: javache
Differential Revision: D97825193
fbshipit-source-id: d059e40f846be72c296c8617b21b7d7082e781831 parent 7b3277f commit f10cded
File tree
3 files changed
+19
-3
lines changed- packages/react-native/ReactAndroid/src/main/java/com/facebook/react
- devsupport
- runtime
3 files changed
+19
-3
lines changedLines changed: 10 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
45 | 45 | | |
46 | 46 | | |
47 | 47 | | |
48 | | - | |
| 48 | + | |
49 | 49 | | |
50 | 50 | | |
51 | 51 | | |
| |||
56 | 56 | | |
57 | 57 | | |
58 | 58 | | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
59 | 67 | | |
60 | 68 | | |
61 | 69 | | |
| |||
69 | 77 | | |
70 | 78 | | |
71 | 79 | | |
| 80 | + | |
72 | 81 | | |
73 | 82 | | |
74 | 83 | | |
| |||
Lines changed: 8 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
70 | 70 | | |
71 | 71 | | |
72 | 72 | | |
73 | | - | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
74 | 81 | | |
75 | 82 | | |
76 | 83 | | |
| |||
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
39 | | - | |
| 39 | + | |
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
| |||
0 commit comments