react / react/react-native

[Android][Fabric] Fatal "Cannot remove child at index" — focus re-request inside ViewGroup.removeViewInternal throws "descendant of this view" during Fabric removal (0.86.0)

Offen
#57,862 3 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Dieses Issue hat noch niemand übernommen.

Needs: Attention Needs: Repro
Vorherrschende Sprache
C++
Sterne
127k
Forks
25.3k
Ø Merge
1 T. 23 Std.
Gemergte PRs (30 T.)
4

Beschreibung

Description

With an accessibility service enabled, removing a focused view during a Fabric mount batch reliably (if intermittently) kills the app with:

java.lang.IllegalStateException: Cannot remove child at index N from parent ViewGroup [tag],
only M children in parent. Warning: childCount may be incorrect!
  at com.facebook.react.fabric.mounting.SurfaceMountingManager.removeViewAt(SurfaceMountingManager.kt:525)
Caused by: java.lang.IllegalArgumentException: parameter must be a descendant of this view
  at android.view.ViewGroup.offsetRectBetweenParentAndChild(ViewGroup.java:6478)

ReactHostImpl.handleHostException then calls destroy() unconditionally, so this is fatal rather than recoverable.

Root cause

The IllegalArgumentException is not thrown by the removal itself — it comes from the accessibility event that the focus re-request fires inside removeViewInternal. Captured on device, read bottom-up:

MountItemDispatcher.dispatchMountItems / IntBufferBatchMountItem.execute
SurfaceMountingManager.removeViewAt(SurfaceMountingManager.kt:504)
ReactClippingViewManager.removeViewAt(ReactClippingViewManager.kt:68)
android.view.ViewGroup.removeViewAt(ViewGroup.java:5702)
android.view.ViewGroup.removeViewInternal(ViewGroup.java:5775)
android.view.View.rootViewRequestFocus(View.java:8827)
android.view.View.requestFocus -> ViewGroup.onRequestFocusInDescendants (recursion)
android.view.View.handleFocusGainInternal(View.java:8591)
android.view.View.onFocusChanged(View.java:8953)
android.view.View.sendAccessibilityEvent(View.java:9178)
android.view.ViewGroup.dispatchPopulateAccessibilityEventInternal(ViewGroup.java:3700)   (~10 deep)
android.view.ViewGroup$ChildListForAccessibility.init(ViewGroup.java:9334)
android.view.ViewGroup$ViewLocationHolder.init(ViewGroup.java:9520)
android.view.ViewGroup.offsetDescendantRectToMyCoords(ViewGroup.java:6407)
android.view.ViewGroup.offsetRectBetweenParentAndChild(ViewGroup.java:6478)   <- throws

So: the removed child held focus → removeViewInternal calls rootViewRequestFocus() → the focus gain fires an accessibility event → ChildListForAccessibility sorts children via offsetDescendantRectToMyCoords while the removed child is still mid-detach → IllegalArgumentException.

Because removeFromArray() has already run at that point, the removal has actually succeeded and the view tree is consistent. But the exception propagates out of ViewGroup.removeViewAt, and SurfaceMountingManager.removeViewAt's catch (e: RuntimeException) converts it into the fatal IllegalStateException quoted above — reporting a childCount inconsistency that isn't the real problem.

Why #56182 doesn't cover this

#56182 added a soft-catch for exactly this IllegalArgumentException — but only around addChildrenForAccessibility, i.e. the accessibility build path. main still has nothing on the removal path, and ReactViewGroup does not override any of removeViewAt / removeViews / removeViewsInLayout / removeAllViewsInLayout.

Note this is a different bug from #57800, which is the Fabric differ emitting a Remove against a wrong parent tag and throws earlier in the same method (SurfaceMountingManager.kt:444, "Unable to remove a view from a view that is not a ViewGroup", no Caused by). This one always carries the descendant of this view cause and requires an active accessibility service; that one requires neither.

Suggested fix

Mirror the #56182 pattern onto the removal path — wrap the super calls in ReactViewGroup's removal methods and soft-catch an IllegalArgumentException whose message contains descendant of this view. Since the child is already detached when it throws, swallowing leaves the tree consistent.

We currently ship precisely that as a build-time ASM transform (RN core is a prebuilt AAR for us, so a source patch would need react.internal.buildFromSource). With it in place the throw is caught, the app survives, and no Cannot remove child follows — verified on device with the stack above.

Steps to reproduce

No deterministic reproducer — it's a race between a Fabric removal and an accessibility service's tree traversal, and we could not force it with scripted input (adb shell input) even across ~20 list remounts and ~30 fling gestures. It surfaces readily in ordinary manual use. Conditions that reproduce it for us:

  1. Android 16 or 17 device with at least one accessibility service enabled (no TalkBack needed — a password manager's autofill service is enough).
  2. A long virtualized list whose rows contain heavy focusable native views (video players, WebViews), with removeClippedSubviews enabled so that recycling produces frequent native view removals.
  3. Scroll the list, or remount it (e.g. by changing its key).
  4. Watch for Cannot remove child at index with a Caused by: parameter must be a descendant of this view.

Two notes for anyone trying to reproduce: the removed view must hold focus for rootViewRequestFocus() to run, and the default logcat ring buffer rotates too fast under WebView logging to catch it after the fact (adb logcat -G 32M helps).

React Native Version

0.86.0

Affected Platforms

Runtime - Android

Areas

Fabric - The New Renderer

Output of npx @react-native-community/cli info

React Native: 0.86.0 (new architecture, bridgeless)
React: 19.2.3
Expo: 57.0.8
Hermes: bundled
Devices observed crashing: Android 16, and Android 17 (Pixel 9 Pro, CP2A.260805.005)
AGP: 8.12.0, Gradle: 9.3.1
Accessibility services active during repro: a password manager's autofill service, plus a
third-party status-bar utility (neither is TalkBack; either alone is sufficient)

— drafted by Claude (AI)

Beitragsleitfaden

Beitragsleitfaden öffnen

Erste Schritte

  1. Lies das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreib ins Issue, dass du es übernimmst — das erspart doppelte Arbeit.
  3. Forke das Repository und arbeite in einem Branch.
  4. Öffne einen Pull Request, der die Issue-Nummer nennt.

Rechercherichtung

Beginne mit den Entfernungsmethoden von ReactViewGroup und vergleiche die Barrierefreiheitsbehandlung in #56182; verfolge anschließend SurfaceMountingManager.removeViewAt und ReactClippingViewManager.removeViewAt. Als erledigt gilt die Aufgabe, wenn die mit der Barrierefreiheit zusammenhängende IllegalArgumentException nicht mehr aus der Fabric-Entfernung entweicht und das beschriebene Android-Barrierefreiheitsszenario den fatalen Fehler Cannot remove child vermeidet.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

Tech-Stack
android, kotlin, react-native
Bereich
mobile
Issue-Typ
Bug
Schwierigkeit
4/5
Geschätzter Aufwand
3-5 Tage
Aktivitätsstatus
Ruhig
Klarheit
Größtenteils klar
Anfängerfreundlichkeit
45/100

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.