mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
fix: avoid ConcurrentModificationException (#49109)
Summary:
Fixes a `ConcurrentModificationException` when iterating over `TextWatcher` `mListeners` array.
If you open Android open source code (`TextView` class), then we can see that Android iterates with `for/n` loop (not `for/:`):
```java
void sendAfterTextChanged(Editable text) {
if (mListeners != null) {
final ArrayList<TextWatcher> list = mListeners;
final int count = list.size();
for (int i = 0; i < count; i++) {
list.get(i).afterTextChanged(text);
}
}
notifyListeningManagersAfterTextChanged();
hideErrorIfUnchanged();
}
```
<hr>
We can catch the `ConcurrentModificationException` with old code, when for example we have 3 listeners:
- 0 is `EmojiTextWatcher` (seems like it's added by OS);
- 1 is `OnlyChangeIfRequiredMaskedTextChangedListener` (added by `react-native-text-input-mask`);
- 2 is a listener that attached by `react-native-keyboard-controller`.
On every afterTextChanged [input-mask-android](https://github.com/RedMadRobot/input-mask-android/tree/df452edc0c52a37e5082adcfc3d05d77b5aa34e8) [removes](https://github.com/RedMadRobot/input-mask-android/blob/df452edc0c52a37e5082adcfc3d05d77b5aa34e8/inputmask/src/main/kotlin/com/redmadrobot/inputmask/MaskedTextChangedListener.kt#L212) the listener and [adds](https://github.com/RedMadRobot/input-mask-android/blob/df452edc0c52a37e5082adcfc3d05d77b5aa34e8/inputmask/src/main/kotlin/com/redmadrobot/inputmask/MaskedTextChangedListener.kt#L231) it back.
The oversimplified version of the code can be next:
```java
public class MyClass {
public static void main(String args[]) {
ArrayList<Integer> mListeners = new ArrayList<>();
mListeners.add(0);
mListeners.add(1);
mListeners.add(2);
Iterator<Integer> iterator = mListeners.iterator();
while (iterator.hasNext()) {
Integer listener = iterator.next();
// Check if the listener is equal to 1
// 1 is OnlyChangeIfRequiredMaskedTextChangedListener and we simulate the behavior of this class
if (listener == 1) {
int i = mListeners.indexOf(listener);
if (i >= 0) {
mListeners.remove(i);
}
// Add the removed element at the end
mListeners.add(listener);
}
}
// Print the modified list
System.out.println(mListeners);
}
}
```
Key points are:
- if we have only [0, 1] listener, then it works well and `ConcurrentModificationException` will not be thrown, because we modify last element;
- if we have `[0, 1, 2]` then exception will be thrown.
So in this PR I decided to re-work code to match what Android has. With `for/n` approach `ConcurrentModificationException` will not be thrown, because we don't check array immutability in this case.
More information also can be found here: https://github.com/kirillzyusko/react-native-keyboard-controller/issues/324
## Changelog:
[ANDROID] [CHANGED] - avoid `ConcurrentModificationException` when iterating over `mListeners` `TextWatcher` array
Pull Request resolved: https://github.com/facebook/react-native/pull/49109
Reviewed By: cortinico
Differential Revision: D69050984
Pulled By: javache
fbshipit-source-id: 9c6a7a428467fa5e546d70549dfcc91d6b2e58d2
This commit is contained in:
committed by
Facebook GitHub Bot
parent
880fd927a9
commit
243aecc095
+3
-3
@@ -80,8 +80,8 @@ import com.facebook.react.views.text.internal.span.ReactStrikethroughSpan;
|
||||
import com.facebook.react.views.text.internal.span.ReactTextPaintHolderSpan;
|
||||
import com.facebook.react.views.text.internal.span.ReactUnderlineSpan;
|
||||
import com.facebook.react.views.text.internal.span.TextInlineImageSpan;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* A wrapper around the EditText that lets us better control what happens when an EditText gets
|
||||
@@ -111,7 +111,7 @@ public class ReactEditText extends AppCompatEditText {
|
||||
/** A count of events sent to JS or C++. */
|
||||
protected int mNativeEventCount;
|
||||
|
||||
private @Nullable ArrayList<TextWatcher> mListeners;
|
||||
private @Nullable CopyOnWriteArrayList<TextWatcher> mListeners;
|
||||
private @Nullable TextWatcherDelegator mTextWatcherDelegator;
|
||||
private int mStagedInputType;
|
||||
protected boolean mContainsImages;
|
||||
@@ -390,7 +390,7 @@ public class ReactEditText extends AppCompatEditText {
|
||||
@Override
|
||||
public void addTextChangedListener(TextWatcher watcher) {
|
||||
if (mListeners == null) {
|
||||
mListeners = new ArrayList<>();
|
||||
mListeners = new CopyOnWriteArrayList<>();
|
||||
super.addTextChangedListener(getTextWatcherDelegator());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user