From a093fe5f2fae4e9996b0cbffdfccdce8e58e8cf1 Mon Sep 17 00:00:00 2001 From: Xin Chen Date: Fri, 13 May 2022 18:40:17 -0700 Subject: [PATCH] Queue the event for preallocated but not mounted view to dispatch later Summary: This diff fixed an edge case that event dispatching is failed after pre-allocation of a view and before the view is mounted. When a cached image is loaded, we will dispatch the event to JS immediately. This is could happen after the view is created during pre-allocation phase, when the event emitter is not instantiated yet. In that case, we will see [an error](https://github.com/facebook/react-native/blob/main/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java#L927) and the event will effectively be ignored. To fix that we introduced a queue in this diff for those events. They will be dispatched in order when the view is mounted and the event emitter is non-null. Changelog: [Android][Fixed] - Fixed an edge case that event dispatching is failed after pre-allocation of a view and before the view is mounted. Reviewed By: mullender Differential Revision: D36331914 fbshipit-source-id: cd065b0b36978cb5f0aac793d8d16f07a48f0881 --- .../react/config/ReactFeatureFlags.java | 3 + .../react/fabric/FabricUIManager.java | 15 +++- .../fabric/mounting/MountingManager.java | 11 +++ .../mounting/SurfaceMountingManager.java | 76 +++++++++++++++++++ 4 files changed, 103 insertions(+), 2 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java b/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java index ceaa0f3a5c2999..1516c841e1dab3 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java +++ b/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java @@ -111,6 +111,9 @@ public static boolean isMapBufferSerializationEnabled() { public static boolean dispatchPointerEvents = false; + /** Feature Flag to enable the pending event queue in fabric before mounting views */ + public static boolean enableFabricPendingEventQueue = false; + /** Feature Flag to control RN Android scrollEventThrottle prop. */ public static boolean enableScrollEventThrottle = false; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java index eac64d218a81f3..75d9791e8234a7 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java @@ -59,6 +59,7 @@ import com.facebook.react.fabric.mounting.MountItemDispatcher; import com.facebook.react.fabric.mounting.MountingManager; import com.facebook.react.fabric.mounting.SurfaceMountingManager; +import com.facebook.react.fabric.mounting.SurfaceMountingManager.ViewEvent; import com.facebook.react.fabric.mounting.mountitems.DispatchIntCommandMountItem; import com.facebook.react.fabric.mounting.mountitems.DispatchStringCommandMountItem; import com.facebook.react.fabric.mounting.mountitems.IntBufferBatchMountItem; @@ -923,8 +924,18 @@ public void receiveEvent( EventEmitterWrapper eventEmitter = mMountingManager.getEventEmitter(surfaceId, reactTag); if (eventEmitter == null) { - // This can happen if the view has disappeared from the screen (because of async events) - FLog.d(TAG, "Unable to invoke event: " + eventName + " for reactTag: " + reactTag); + if (ReactFeatureFlags.enableFabricPendingEventQueue + && mMountingManager.getViewExists(reactTag)) { + // The view is preallocated and created. However, it hasn't been mounted yet. We will have + // access to the event emitter later when the view is mounted. For now just save the event + // in the view state and trigger it later. + mMountingManager.enqueuePendingEvent( + reactTag, + new ViewEvent(eventName, params, eventCategory, canCoalesceEvent, customCoalesceKey)); + } else { + // This can happen if the view has disappeared from the screen (because of async events) + FLog.d(TAG, "Unable to invoke event: " + eventName + " for reactTag: " + reactTag); + } return; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.java index 30ebf9dc040c84..f602b2a4f8377d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.java @@ -26,6 +26,7 @@ import com.facebook.react.common.mapbuffer.MapBuffer; import com.facebook.react.fabric.FabricUIManager; import com.facebook.react.fabric.events.EventEmitterWrapper; +import com.facebook.react.fabric.mounting.SurfaceMountingManager.ViewEvent; import com.facebook.react.fabric.mounting.mountitems.MountItem; import com.facebook.react.touch.JSResponderHandler; import com.facebook.react.uimanager.RootViewManager; @@ -422,4 +423,14 @@ public long measureMapBuffer( public void initializeViewManager(String componentName) { mViewManagerRegistry.get(componentName); } + + public void enqueuePendingEvent(int reactTag, ViewEvent viewEvent) { + @Nullable SurfaceMountingManager smm = getSurfaceManagerForView(reactTag); + if (smm == null) { + // Cannot queue event without valid surface mountng manager. Do nothing here. + return; + } + + smm.enqueuePendingEvent(reactTag, viewEvent); + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.java index 94b06743bb105a..176ac68cba5fd2 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.java @@ -25,6 +25,7 @@ import com.facebook.react.bridge.RetryableMountingLayerException; import com.facebook.react.bridge.SoftAssertions; import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.bridge.WritableMap; import com.facebook.react.common.build.ReactBuildConfig; import com.facebook.react.common.mapbuffer.ReadableMapBuffer; import com.facebook.react.config.ReactFeatureFlags; @@ -43,9 +44,12 @@ import com.facebook.react.uimanager.ViewGroupManager; import com.facebook.react.uimanager.ViewManager; import com.facebook.react.uimanager.ViewManagerRegistry; +import com.facebook.react.uimanager.events.EventCategoryDef; import com.facebook.react.views.view.ReactMapBufferViewManager; import com.facebook.react.views.view.ReactViewManagerWrapper; import java.util.HashSet; +import java.util.LinkedList; +import java.util.Queue; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; @@ -849,6 +853,7 @@ public void updateState(final int reactTag, @Nullable StateWrapper stateWrapper) } } + /** We update the event emitter from the main thread when the view is mounted. */ @UiThread public void updateEventEmitter(int reactTag, @NonNull EventEmitterWrapper eventEmitter) { UiThreadUtil.assertOnUiThread(); @@ -870,6 +875,20 @@ public void updateEventEmitter(int reactTag, @NonNull EventEmitterWrapper eventE if (previousEventEmitterWrapper != eventEmitter && previousEventEmitterWrapper != null) { previousEventEmitterWrapper.destroy(); } + + if (viewState.mPendingEventQueue != null) { + // Invoke pending event queued to the view state + for (ViewEvent viewEvent : viewState.mPendingEventQueue) { + if (viewEvent.canCoalesceEvent()) { + eventEmitter.invokeUnique( + viewEvent.getEventName(), viewEvent.getParams(), viewEvent.getCustomCoalesceKey()); + } else { + eventEmitter.invoke( + viewEvent.getEventName(), viewEvent.getParams(), viewEvent.getEventCategory()); + } + } + viewState.mPendingEventQueue = null; + } } @UiThread @@ -1065,6 +1084,21 @@ public void printSurfaceState() { } } + @UiThread + public void enqueuePendingEvent(int reactTag, ViewEvent viewEvent) { + UiThreadUtil.assertOnUiThread(); + + ViewState viewState = mTagToViewState.get(reactTag); + if (viewState == null) { + // Cannot queue event without view state. Do nothing here. + return; + } + if (viewState.mPendingEventQueue == null) { + viewState.mPendingEventQueue = new LinkedList<>(); + } + viewState.mPendingEventQueue.add(viewEvent); + } + /** * This class holds view state for react tags. Objects of this class are stored into the {@link * #mTagToViewState}, and they should be updated in the same thread. @@ -1078,6 +1112,7 @@ private static class ViewState { @Nullable public ReadableMap mCurrentLocalData = null; @Nullable public StateWrapper mStateWrapper = null; @Nullable public EventEmitterWrapper mEventEmitter = null; + @Nullable public Queue mPendingEventQueue = null; private ViewState( int reactTag, @Nullable View view, @Nullable ReactViewManagerWrapper viewManager) { @@ -1112,4 +1147,45 @@ public String toString() { + isLayoutOnly; } } + + public static class ViewEvent { + private final String mEventName; + private final boolean mCanCoalesceEvent; + private final int mCustomCoalesceKey; + private final @EventCategoryDef int mEventCategory; + private @Nullable WritableMap mParams; + + public ViewEvent( + String eventName, + @Nullable WritableMap params, + @EventCategoryDef int eventCategory, + boolean canCoalesceEvent, + int customCoalesceKey) { + mEventName = eventName; + mParams = params; + mEventCategory = eventCategory; + mCanCoalesceEvent = canCoalesceEvent; + mCustomCoalesceKey = customCoalesceKey; + } + + public String getEventName() { + return mEventName; + } + + public boolean canCoalesceEvent() { + return mCanCoalesceEvent; + } + + public int getCustomCoalesceKey() { + return mCustomCoalesceKey; + } + + public @EventCategoryDef int getEventCategory() { + return mEventCategory; + } + + public @Nullable WritableMap getParams() { + return mParams; + } + } }