ITADN

[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)

#57862Openchengjwk 创建于 21 天前
Needs: Author FeedbackNeeds: Repro
C
chengjwkcommented
## 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](https://github.com/facebook/react-native/pull/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](https://github.com/react/react-native/issues/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)_
1 条评论