Repository navigation
Fix: unmounted secureTextEntry TextInput retained (with its Activity) by the measurement cache - #59470
Fix: unmounted secureTextEntry TextInput retained (with its Activity) by the measurement cache#59470A-Kimpo wants to merge 1 commit into
Conversation
…cache Added logic to strip specific spans from the text buffer to prevent memory leaks.
|
Hi @A-Kimpo! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
|
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks! |
|
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks! |
Summary:
Fixes #59469.
ReactEditText.updateCachedSpannable()copies the current text, with all its spans, intoTextLayoutManager's spannable cache. The entry is evicted only fromReactEditText.finalize().For a password field the Editable carries
PasswordTransformationMethod.Visiblewhile the last typed character is shown. That span is not aNoCopySpan, so it ends up in the copy. It references the Editable. The Editable'sTextView.ChangeWatcherreferences theReactEditText, and the view'sThemedReactContexthas the Activity as its base context. The cache therefore keeps the view reachable,finalize()never runs, and the entry is never evicted.As a result, every unmounted
<TextInput secureTextEntry />that received input stays in memory for the life of the process, together with its ancestors and its typed text. After the Activity is recreated, the old Activity stays too.The copy only feeds measurement, so this change keeps only the spans that can affect it:
CharacterStyleandParagraphStyle(Android's own style spans) and React Native'sReactSpans. Editor bookkeeping spans are dropped. Eviction stays infinalize(), as #58869 requires, because a background layout can still measure a TextInput after its view was deleted. The view simply becomes collectable again.Changelog:
[ANDROID] [FIXED] - An unmounted
TextInputwithsecureTextEntryis no longer retained, together with its Activity, by the TextInput measurement cacheTest Plan:
Reproducer: a fresh 0.88.0 app with no other dependencies (https://github.com/A-Kimpo/rn-secure-textinput-leak). One
TextInputgets a newkeyon every mount, plus mount/unmount andsecureTextEntrytoggles. It is a release build withdebuggable trueand React Native built from source. It runs on an emulator: API 34, arm64. The test typesabc123, unmounts and mounts 5 times, then takesam dumpheap -gand counts strongly reachable instances.ReactEditTextreachableMainActivityreachable after Activity recreationTextInputTextInputThe same runs on 0.87.1 gave the same numbers.
Measurement is unchanged. An auto-growing
multilineTextInput, which is measured from the cached spannable, reports the same height viaonLayoutbefore and after the change: 48 dp empty and 104 dp with four lines, two runs each, on 0.88.0 and 0.87.1.