fix(Android): stop reading uninitialized state from requestLayout() so R8 optimization cannot crash it - #4574
Conversation
…ruction-time guard
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (3)
Included review availability: Your plan provides up to 8 included reviews per hour; 5 remain after this review. 📝 WalkthroughWalkthroughAndroid layout scheduling no longer uses ChangesAndroid layout scheduling
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: ⚪ Minimal · up to The PR removes constructor-time reads that can crash optimized Android builds and preserves deferred layout scheduling; no actionable merge-blocking risk remains beyond normal checks and review. Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
2a66437 to
0873001
Compare
…o R8 optimization cannot crash it
0873001 to
55e1bf3
Compare
| @Suppress("SENSELESS_COMPARISON") // mLayoutCallback can be null here since this method can be called in init | ||
| if (!isLayoutEnqueued && layoutCallback != null) { | ||
| if (!isLayoutEnqueued) { | ||
| isLayoutEnqueued = true | ||
| // we use NATIVE_ANIMATED_MODULE choreographer queue because it allows us to catch the current | ||
| // looper loop instead of enqueueing the update in the next loop causing a one frame delay. | ||
| ReactChoreographer | ||
| .getInstance() | ||
| .postFrameCallback( | ||
| ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE, | ||
| layoutCallback, | ||
| ) | ||
| .postFrameCallback(ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE) { | ||
| isLayoutEnqueued = false | ||
| forceMeasureAndLayoutPass() | ||
| } |
There was a problem hiding this comment.
This won't fly that easily unfortunately.
It changes the timing when the callback is posted, e.g. it fires an additional callback, when previously layoutCallback == null.
This is a very fragile code that would require careful testing & there is lack of capacity on our side at the moment.
Fixes #4586
Description
With R8 optimization enabled, the app crashes as soon as the first screen is mounted:
ViewGroup's constructor callsrequestLayout()(throughinitViewGroupandsetFlags) before the subclass is initialized.ScreenContaineroverridesrequestLayout()and readslayoutCallback, which is still null at that point. The code guarded this withlayoutCallback != nullplus@Suppress("SENSELESS_COMPARISON"). R8's optimizer sees afinalfield written once with a non-null object, decides the check can never fail, and deletes it. Reproduced with R8 9.4.14. R8 8.12.14, which AGP 8.11 bundles, still keeps the check.So the library only works with R8 optimization off. AGP 9 rejects
proguard-android.txtby default, the file that carried-dontoptimize, and Google's guidance isproguard-android-optimize.txt, so apps following it hit this.CustomToolbarandTabsHosthave the same pattern.TabsHostis not legacy code.An earlier revision of this PR typed the fields as nullable. That only changes Kotlin metadata, not bytecode, and the post-R8 dex came out identical to the crashing build. This revision removes the fields instead.
No existing issue covers this.
Changes
requestLayout()no longer reads anything initialized aftersuper(). It is gated by Booleans, whose default value is correct while the superclass constructor runs, and posts a fresh callback each time.StackHostalready works this way.legacy/ScreenContainer.kt: removed thelayoutCallbackfield, its body is nowforceSubtreeMeasureAndLayoutPass().legacy/CustomToolbar.kt: same, asforceMeasureAndLayoutPass().tabs/host/TabsHost.kt: took over the two pending flags fromTabsHostLayoutCoordinator, which held no other state, and that class is deleted. Its two scheduling methods and their docs moved intoTabsHost.One behaviour difference: a
requestLayout()during construction used to be skipped and now posts one deferred measure/layout pass, which runs on the fully constructed view.StackHostalready does this. Kotlin emits no write for= falseinitializers, so a flag set during construction is not reset afterwards and nothing posts twice.StackHostandStackHeaderSubviewalso overriderequestLayout()and are unaffected: one guards on aBoolean, the other uses a safe call.Test plan
Reproduced on react-native-screens 4.25.0: release build,
minifyEnabled true,proguard-android-optimize.txt, R8 9.4.14 viaclasspath("com.android.tools:r8:9.4.14")insettings.gradle, rendering aScreenStack. The code is unchanged in 4.27.0 and onmain.Verified by reading the post-R8 dex of
ScreenContainer.requestLayoutwithdexdump -d, R8 9.4.14, no keep rules for this library:postFrameCallbackwith a new callback, no field read left. The app launches and renders with no fatal exception.Compiles clean against React Native 0.87.0-rc.3 (
:react-native-screens:compileDebugKotlininFabricExample), zero errors, no new warnings on the changed files. No automated test is added because the failure only exists after R8 optimization runs.Checklist