Skip to content

Commit a2eb043

Browse files
lukmccallpiaskowyk
andauthored
fix: Unsubscribe dangling listeners (#6696)
## Summary Unsubscribes listeners that were not removed correctly during the app reload. ## Test plan I tested it with a new project using Expo (SDK 52) and with Fabric enabled. I reloaded the app several times and performed a heap dump to verify that everything was removed correctly. --------- Co-authored-by: Krzysztof Piaskowy <[email protected]>
1 parent f4319a0 commit a2eb043

File tree

6 files changed

+55
-2
lines changed

6 files changed

+55
-2
lines changed

packages/react-native-reanimated/android/src/fabric/java/com/swmansion/reanimated/ReaCompatibility.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ public void registerFabricEventListener(NodesManager nodesManager) {
2121
}
2222
}
2323

24+
public void unregisterFabricEventListener(NodesManager nodesManager) {
25+
if (fabricUIManager != null) {
26+
fabricUIManager.getEventDispatcher().removeListener(nodesManager);
27+
}
28+
}
29+
2430
public void synchronouslyUpdateUIProps(int viewTag, ReadableMap uiProps) {
2531
fabricUIManager.synchronouslyUpdateViewOnUIThread(viewTag, uiProps);
2632
}

packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/NodesManager.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import com.facebook.react.uimanager.UIManagerReanimatedHelper;
3131
import com.facebook.react.uimanager.common.UIManagerType;
3232
import com.facebook.react.uimanager.events.Event;
33+
import com.facebook.react.uimanager.events.EventDispatcher;
3334
import com.facebook.react.uimanager.events.EventDispatcherListener;
3435
import com.facebook.react.uimanager.events.RCTEventEmitter;
3536
import com.swmansion.reanimated.layoutReanimation.AnimationsManager;
@@ -111,6 +112,7 @@ public interface OnAnimationFrame {
111112
public Set<String> uiProps = Collections.emptySet();
112113
public Set<String> nativeProps = Collections.emptySet();
113114
private ReaCompatibility compatibility;
115+
private @Nullable Runnable mUnsubscribe = null;
114116

115117
public NativeProxy getNativeProxy() {
116118
return mNativeProxy;
@@ -131,6 +133,15 @@ public void invalidate() {
131133
mNativeProxy.invalidate();
132134
mNativeProxy = null;
133135
}
136+
137+
if (compatibility != null) {
138+
compatibility.unregisterFabricEventListener(this);
139+
}
140+
141+
if (mUnsubscribe != null) {
142+
mUnsubscribe.run();
143+
mUnsubscribe = null;
144+
}
134145
}
135146

136147
public void initWithContext(ReactApplicationContext reactApplicationContext) {
@@ -184,8 +195,10 @@ protected void doFrameGuarded(long frameTimeNanos) {
184195
// Events are handled in the native modules thread in the `onEventDispatch()` method.
185196
// This method indirectly uses `mChoreographerCallback` which was created after event
186197
// registration, creating race condition
187-
Objects.requireNonNull(UIManagerHelper.getEventDispatcher(context, uiManagerType))
188-
.addListener(this);
198+
EventDispatcher eventDispatcher =
199+
Objects.requireNonNull(UIManagerHelper.getEventDispatcher(context, uiManagerType));
200+
eventDispatcher.addListener(this);
201+
mUnsubscribe = () -> eventDispatcher.removeListener(this);
189202

190203
mAnimationManager = new AnimationsManager(mContext, mUIManager);
191204
}

packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/Utils.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,14 @@ public static float convertToFloat(Object value) {
4646
}
4747
return 0;
4848
}
49+
50+
public static Runnable combineRunnables(final Runnable... runnables) {
51+
return new Runnable() {
52+
public void run() {
53+
for (Runnable r : runnables) {
54+
r.run();
55+
}
56+
}
57+
};
58+
}
4959
}

packages/react-native-reanimated/android/src/paper/java/com/swmansion/reanimated/ReaCompatibility.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,7 @@ public ReaCompatibility(ReactApplicationContext reactApplicationContext) {}
88

99
public void registerFabricEventListener(NodesManager nodesManager) {}
1010

11+
public void unregisterFabricEventListener(NodesManager nodesManager) {}
12+
1113
public void synchronouslyUpdateUIProps(int viewTag, ReadableMap uiProps) {}
1214
}

packages/react-native-reanimated/android/src/reactNativeVersionPatch/ReanimatedUIManager/74/com/swmansion/reanimated/ReanimatedModule.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ private interface UIThreadOperation {
6363
private ArrayList<UIThreadOperation> mOperations = new ArrayList<>();
6464
private @Nullable NodesManager mNodesManager;
6565
private final WorkletsModule mWorkletsModule;
66+
private Runnable mUnsubscribe = () -> {};
6667

6768
public ReanimatedModule(ReactApplicationContext reactContext) {
6869
super(reactContext);
@@ -81,14 +82,22 @@ public void initialize() {
8182
UIManager uiManager = reactCtx.getFabricUIManager();
8283
if (uiManager instanceof FabricUIManager) {
8384
((FabricUIManager) uiManager).addUIManagerEventListener(this);
85+
mUnsubscribe =
86+
Utils.combineRunnables(
87+
mUnsubscribe,
88+
() -> ((FabricUIManager) uiManager).removeUIManagerEventListener(this));
8489
} else {
8590
throw new RuntimeException("[Reanimated] Failed to obtain instance of FabricUIManager.");
8691
}
8792
} else {
8893
UIManagerModule uiManager = reactCtx.getNativeModule(UIManagerModule.class);
8994
uiManager.addUIManagerListener(this);
95+
mUnsubscribe =
96+
Utils.combineRunnables(mUnsubscribe, () -> uiManager.removeUIManagerListener(this));
9097
}
9198
reactCtx.addLifecycleEventListener(this);
99+
mUnsubscribe =
100+
Utils.combineRunnables(mUnsubscribe, () -> reactCtx.removeLifecycleEventListener(this));
92101
}
93102

94103
@Override
@@ -176,5 +185,7 @@ public void invalidate() {
176185
if (mNodesManager != null) {
177186
mNodesManager.invalidate();
178187
}
188+
189+
mUnsubscribe.run();
179190
}
180191
}

packages/react-native-reanimated/android/src/reactNativeVersionPatch/ReanimatedUIManager/latest/com/swmansion/reanimated/ReanimatedModule.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ private interface UIThreadOperation {
6363
private ArrayList<UIThreadOperation> mOperations = new ArrayList<>();
6464
private @Nullable NodesManager mNodesManager;
6565
private final WorkletsModule mWorkletsModule;
66+
private Runnable mUnsubscribe = () -> {};
6667

6768
public ReanimatedModule(ReactApplicationContext reactContext) {
6869
super(reactContext);
@@ -81,15 +82,23 @@ public void initialize() {
8182
UIManager uiManager = reactCtx.getFabricUIManager();
8283
if (uiManager instanceof FabricUIManager) {
8384
((FabricUIManager) uiManager).addUIManagerEventListener(this);
85+
mUnsubscribe =
86+
Utils.combineRunnables(
87+
mUnsubscribe,
88+
() -> ((FabricUIManager) uiManager).removeUIManagerEventListener(this));
8489
} else {
8590
throw new RuntimeException("[Reanimated] Failed to obtain instance of FabricUIManager.");
8691
}
8792
} else {
8893
UIManagerModule uiManager =
8994
Objects.requireNonNull(reactCtx.getNativeModule(UIManagerModule.class));
9095
uiManager.addUIManagerListener(this);
96+
mUnsubscribe =
97+
Utils.combineRunnables(mUnsubscribe, () -> uiManager.removeUIManagerListener(this));
9198
}
9299
reactCtx.addLifecycleEventListener(this);
100+
mUnsubscribe =
101+
Utils.combineRunnables(mUnsubscribe, () -> reactCtx.removeLifecycleEventListener(this));
93102
}
94103

95104
@Override
@@ -174,5 +183,7 @@ public void invalidate() {
174183
if (mNodesManager != null) {
175184
mNodesManager.invalidate();
176185
}
186+
187+
mUnsubscribe.run();
177188
}
178189
}

0 commit comments

Comments
 (0)