Skip to content

Commit 972f399

Browse files
dryganetsfacebook-github-bot
authored andcommitted
This change fixes currently broken ReactContext listeners mechanism. (#22318)
Summary: This mechanism is heavily abused inside of the react-native and inside of the various native modules. The main problem is that people don't remove their listeners and as result, we have memory leaks. Some modules like UIManager, NativeAnimatedModule have resources holding Activity context. Those modules are held through a pretty long chain of dependencies. In order to allow GC to collect those listeners, I replaced the CopyOnWriteSet by WeakHashMap and synchronized access. It is not such a big deal in terms of performance as those listeners are not called/modified too frequently but this prevents hard to debug memory leaks. Changelog: ---------- Help reviewers and the release process by writing your own changelog entry. When the change doesn't impact React Native developers, it may be ommitted from the changelog for brevity. See below for an example. [Android] [Fixed] - ReactContext - lifecycle listeners don't cause the leaks even if not removed. Pull Request resolved: #22318 Reviewed By: mdvacca Differential Revision: D13106915 Pulled By: hramos fbshipit-source-id: d506e5035a7f7bea1b57a6308fb5d9b5fcb277a7
1 parent b2d3052 commit 972f399

File tree

2 files changed

+140
-38
lines changed

2 files changed

+140
-38
lines changed

ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContext.java

Lines changed: 54 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import com.facebook.react.bridge.queue.ReactQueueConfiguration;
1919
import com.facebook.react.common.LifecycleState;
2020
import java.lang.ref.WeakReference;
21-
import java.util.concurrent.CopyOnWriteArraySet;
2221
import javax.annotation.Nullable;
2322

2423
/**
@@ -32,10 +31,35 @@ public class ReactContext extends ContextWrapper {
3231
"ReactContext#getJSModule should only happen once initialize() has been called on your " +
3332
"native module.";
3433

35-
private final CopyOnWriteArraySet<LifecycleEventListener> mLifecycleEventListeners =
36-
new CopyOnWriteArraySet<>();
37-
private final CopyOnWriteArraySet<ActivityEventListener> mActivityEventListeners =
38-
new CopyOnWriteArraySet<>();
34+
private final SynchronizedWeakHashSet<LifecycleEventListener> mLifecycleEventListeners =
35+
new SynchronizedWeakHashSet<>();
36+
private final SynchronizedWeakHashSet<ActivityEventListener> mActivityEventListeners =
37+
new SynchronizedWeakHashSet<>();
38+
39+
40+
private final GuardedIteration<LifecycleEventListener> mResumeIteration =
41+
new GuardedIteration<LifecycleEventListener>() {
42+
@Override
43+
public void onIterate(LifecycleEventListener listener) {
44+
listener.onHostResume();
45+
}
46+
};
47+
48+
private final GuardedIteration<LifecycleEventListener> mPauseIteration =
49+
new GuardedIteration<LifecycleEventListener>() {
50+
@Override
51+
public void onIterate(LifecycleEventListener listener) {
52+
listener.onHostPause();
53+
}
54+
};
55+
56+
private final GuardedIteration<LifecycleEventListener> mDestroyIteration =
57+
new GuardedIteration<LifecycleEventListener>() {
58+
@Override
59+
public void onIterate(LifecycleEventListener listener) {
60+
listener.onHostDestroy();
61+
}
62+
};
3963

4064
private LifecycleState mLifecycleState = LifecycleState.BEFORE_CREATE;
4165

@@ -188,26 +212,19 @@ public void onHostResume(@Nullable Activity activity) {
188212
mLifecycleState = LifecycleState.RESUMED;
189213
mCurrentActivity = new WeakReference(activity);
190214
ReactMarker.logMarker(ReactMarkerConstants.ON_HOST_RESUME_START);
191-
for (LifecycleEventListener listener : mLifecycleEventListeners) {
192-
try {
193-
listener.onHostResume();
194-
} catch (RuntimeException e) {
195-
handleException(e);
196-
}
197-
}
215+
mLifecycleEventListeners.iterate(mResumeIteration);
198216
ReactMarker.logMarker(ReactMarkerConstants.ON_HOST_RESUME_END);
199217
}
200218

201-
public void onNewIntent(@Nullable Activity activity, Intent intent) {
219+
public void onNewIntent(@Nullable Activity activity, final Intent intent) {
202220
UiThreadUtil.assertOnUiThread();
203221
mCurrentActivity = new WeakReference(activity);
204-
for (ActivityEventListener listener : mActivityEventListeners) {
205-
try {
222+
mActivityEventListeners.iterate(new GuardedIteration<ActivityEventListener>() {
223+
@Override
224+
public void onIterate(ActivityEventListener listener) {
206225
listener.onNewIntent(intent);
207-
} catch (RuntimeException e) {
208-
handleException(e);
209226
}
210-
}
227+
});
211228
}
212229

213230
/**
@@ -216,13 +233,7 @@ public void onNewIntent(@Nullable Activity activity, Intent intent) {
216233
public void onHostPause() {
217234
mLifecycleState = LifecycleState.BEFORE_RESUME;
218235
ReactMarker.logMarker(ReactMarkerConstants.ON_HOST_PAUSE_START);
219-
for (LifecycleEventListener listener : mLifecycleEventListeners) {
220-
try {
221-
listener.onHostPause();
222-
} catch (RuntimeException e) {
223-
handleException(e);
224-
}
225-
}
236+
mLifecycleEventListeners.iterate(mPauseIteration);
226237
ReactMarker.logMarker(ReactMarkerConstants.ON_HOST_PAUSE_END);
227238
}
228239

@@ -232,13 +243,7 @@ public void onHostPause() {
232243
public void onHostDestroy() {
233244
UiThreadUtil.assertOnUiThread();
234245
mLifecycleState = LifecycleState.BEFORE_CREATE;
235-
for (LifecycleEventListener listener : mLifecycleEventListeners) {
236-
try {
237-
listener.onHostDestroy();
238-
} catch (RuntimeException e) {
239-
handleException(e);
240-
}
241-
}
246+
mLifecycleEventListeners.iterate(mDestroyIteration);
242247
mCurrentActivity = null;
243248
}
244249

@@ -256,14 +261,13 @@ public void destroy() {
256261
/**
257262
* Should be called by the hosting Fragment in {@link Fragment#onActivityResult}
258263
*/
259-
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
260-
for (ActivityEventListener listener : mActivityEventListeners) {
261-
try {
264+
public void onActivityResult(final Activity activity, final int requestCode, final int resultCode, final Intent data) {
265+
mActivityEventListeners.iterate(new GuardedIteration<ActivityEventListener>() {
266+
@Override
267+
public void onIterate(ActivityEventListener listener) {
262268
listener.onActivityResult(activity, requestCode, resultCode, data);
263-
} catch (RuntimeException e) {
264-
handleException(e);
265269
}
266-
}
270+
});
267271
}
268272

269273
public void assertOnUiQueueThread() {
@@ -359,4 +363,16 @@ public JavaScriptContextHolder getJavaScriptContextHolder() {
359363
return mCatalystInstance.getJavaScriptContextHolder();
360364
}
361365

366+
private abstract class GuardedIteration<T> implements SynchronizedWeakHashSet.Iteration<T> {
367+
@Override
368+
public void iterate(T listener) {
369+
try {
370+
onIterate(listener);
371+
} catch (RuntimeException e) {
372+
handleException(e);
373+
}
374+
}
375+
376+
public abstract void onIterate(T listener);
377+
}
362378
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright (c) Facebook, Inc. and its affiliates.
2+
3+
// This source code is licensed under the MIT license found in the
4+
// LICENSE file in the root directory of this source tree.
5+
package com.facebook.react.bridge;
6+
7+
import android.util.Pair;
8+
9+
import java.util.ArrayDeque;
10+
import java.util.Queue;
11+
import java.util.WeakHashMap;
12+
13+
/**
14+
* Thread-safe Set based on the WeakHashMap.
15+
*
16+
* Doesn't implement the `iterator` method because it's tricky to support modifications
17+
* to the collection while somebody is using an `Iterator` to iterate over it.
18+
*
19+
* Instead, it provides an `iterate` method for traversing the collection. Any add/remove operations
20+
* that occur during iteration are postponed until the iteration has completed.
21+
*/
22+
public class SynchronizedWeakHashSet<T> {
23+
private WeakHashMap<T, Void> mMap = new WeakHashMap<>();
24+
private Queue<Pair<T, Command>> mPendingOperations = new ArrayDeque<>();
25+
private boolean mIterating;
26+
27+
public boolean contains(T item) {
28+
synchronized (mMap) {
29+
return mMap.containsKey(item);
30+
}
31+
}
32+
33+
public void add(T item) {
34+
synchronized (mMap) {
35+
if (mIterating) {
36+
mPendingOperations.add(new Pair<>(item, Command.ADD));
37+
} else {
38+
mMap.put(item, null);
39+
}
40+
}
41+
}
42+
43+
public void remove(T item) {
44+
synchronized (mMap) {
45+
if (mIterating) {
46+
mPendingOperations.add(new Pair<>(item, Command.REMOVE));
47+
} else {
48+
mMap.remove(item);
49+
}
50+
}
51+
}
52+
53+
public void iterate(Iteration<T> iterated) {
54+
synchronized (mMap) {
55+
// Protection from modification during iteration on the same thread
56+
mIterating = true;
57+
for (T listener: mMap.keySet()) {
58+
iterated.iterate(listener);
59+
}
60+
mIterating = false;
61+
62+
while (!mPendingOperations.isEmpty()) {
63+
Pair<T, Command> pair = mPendingOperations.poll();
64+
switch (pair.second) {
65+
case ADD:
66+
mMap.put(pair.first, null);
67+
break;
68+
case REMOVE:
69+
mMap.remove(pair.first);
70+
break;
71+
default:
72+
throw new AssertionException("Unsupported command" + pair.second);
73+
}
74+
}
75+
}
76+
}
77+
78+
public interface Iteration<T> {
79+
void iterate(T item);
80+
}
81+
82+
private enum Command {
83+
ADD,
84+
REMOVE
85+
}
86+
}

0 commit comments

Comments
 (0)