From 29eb632f1cb2ef5459253783eac43e5d7e999742 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Thu, 21 Jan 2021 23:44:45 -0800 Subject: [PATCH] Refactor MountingManager into MountingManager + SurfaceMountingManager Summary: This refactors MountingManager into a minimal API that shims into a more fully-featured SurfaceMountingManager. The SurfaceMountingManager keeps track of surface start/stop, surface ID, and surface Context. This solves a number of issues around (1) race conditions around StopSurface/StartSurface, (2) memory management of Views, (3) Concrete improvements: 1. Simpler to reason about race conditions around StopSurface/StartSurface. 2. 1:1 relationship between SurfaceId and all Views/tags. 3. When surface is stopped, all descendent Views can be GC'd immediately. 4. Fixed separation of concerns and leaky abstractions: surfaceId/rootTag and Surface Context are now stored and manipulated *only* in SurfaceMountingManager. 5. Simpler StopSurface flow: we simply remove references to all Views, and the Fragment (outside of the scope of this code) removes the RootView. This will trigger GC and we do ~0 work. Previously, we ran a REMOVE and DELETE instruction and kept track of each View in a HashMap. Now we can simply delete the map and move on. The caveat: NativeAnimated (or other native modules that go through UIManager). APIs like `updateProps` currently uses only the ReactTag and does not store SurfaceId. This is a good argument for moving away from ReactTag, at least in its current incarnation, but: for now this requires that you do a lookup of a ReactTag across N surfaces (worst-case) to determine which Surface a ReactTag is in. So, to summarize, the "con" of this approach is that now `getSurfaceManagerForViewEnforced` could be slower. It is used in: * NativeAnimatedModule calls `updateProps` through UIManager * FabricEventEmitter calls `receiveEvent` on FabricUIManager directly * On audit, I could find zero native callsites to `sendAccessibilityEvent` through UIManager Changelog: [Internal] Reviewed By: mdvacca Differential Revision: D26000781 fbshipit-source-id: 386ae40c4333f8c584e05818c404868dbee6ce73 --- .../com/facebook/react/bridge/UIManager.java | 2 +- .../react/fabric/FabricUIManager.java | 213 ++--- .../com/facebook/react/fabric/jni/Binding.cpp | 19 +- .../fabric/mounting/MountingManager.java | 897 ++++-------------- .../mounting/SurfaceMountingManager.java | 822 ++++++++++++++++ .../DispatchIntCommandMountItem.java | 6 +- .../DispatchStringCommandMountItem.java | 6 +- .../mountitems/IntBufferBatchMountItem.java | 55 +- .../mountitems/PreAllocateViewMountItem.java | 28 +- .../mountitems/SendAccessibilityEvent.java | 6 +- .../react/uimanager/UIManagerModule.java | 3 +- 11 files changed, 1186 insertions(+), 871 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.java diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/UIManager.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/UIManager.java index f07ec720b98ffa..ad837bf04eadda 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/UIManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/UIManager.java @@ -84,7 +84,7 @@ void updateRootLayoutSpecs( * layout-related propertied won't be handled properly. Make sure you know what you're doing * before calling this method :) * - * @param tag {@link int} that identifies the view that will be updated + * @param reactTag {@link int} that identifies the view that will be updated * @param props {@link ReadableMap} props that should be immediately updated in view */ @UiThread 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 09610be63d42eb..cdeed3160edce5 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java @@ -58,6 +58,7 @@ import com.facebook.react.fabric.events.EventEmitterWrapper; import com.facebook.react.fabric.events.FabricEventEmitter; import com.facebook.react.fabric.mounting.MountingManager; +import com.facebook.react.fabric.mounting.SurfaceMountingManager; import com.facebook.react.fabric.mounting.mountitems.DispatchCommandMountItem; import com.facebook.react.fabric.mounting.mountitems.DispatchIntCommandMountItem; import com.facebook.react.fabric.mounting.mountitems.DispatchStringCommandMountItem; @@ -83,7 +84,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CopyOnWriteArrayList; @@ -111,10 +111,6 @@ public class FabricUIManager implements UIManager, LifecycleEventListener { @NonNull private final MountingManager mMountingManager; @NonNull private final EventDispatcher mEventDispatcher; - @NonNull - private final ConcurrentHashMap mReactContextForRootTag = - new ConcurrentHashMap<>(); - @NonNull private final EventBeatManager mEventBeatManager; private boolean mInDispatch = false; @@ -183,13 +179,11 @@ public int addRootView( final int rootTag = ReactRootViewTagGenerator.getNextRootViewTag(); ReactRoot reactRootView = (ReactRoot) rootView; - // TODO T31905686: Combine with startSurface below ThemedReactContext reactContext = new ThemedReactContext( mReactApplicationContext, rootView.getContext(), reactRootView.getSurfaceID()); - mMountingManager.addRootView(rootTag, rootView); + mMountingManager.addRootView(rootTag, rootView, reactContext); String moduleName = reactRootView.getJSModuleName(); - mReactContextForRootTag.put(rootTag, reactContext); if (ENABLE_FABRIC_LOGS) { FLog.d(TAG, "Starting surface for module: %s and reactTag: %d", moduleName, rootTag); } @@ -223,8 +217,7 @@ public int startSurface( if (ENABLE_FABRIC_LOGS) { FLog.d(TAG, "Starting surface for module: %s and reactTag: %d", moduleName, rootTag); } - mMountingManager.addRootView(rootTag, rootView); - mReactContextForRootTag.put(rootTag, reactContext); + mMountingManager.addRootView(rootTag, rootView, reactContext); // If startSurface is executed in the UIThread then, it uses the ViewportOffset from the View, // Otherwise Fabric relies on calling {@link Binding#setConstraints} method to update the @@ -259,15 +252,8 @@ public void onRequestEventBeat() { @ThreadConfined(ANY) @Override public void stopSurface(final int surfaceID) { - mReactContextForRootTag.remove(surfaceID); mBinding.stopSurface(surfaceID); - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - mMountingManager.deleteRootView(surfaceID); - } - }); + mMountingManager.stopSurface(surfaceID); } @Override @@ -328,14 +314,8 @@ private void preallocateView( @Nullable Object stateWrapper, boolean isLayoutable) { - // This could be null if teardown/navigation away from a surface on the main thread happens - // while a commit is being processed in a different thread. By contract we expect this to be - // possible at teardown, but this race should *never* happen at startup. - @Nullable ThemedReactContext context = mReactContextForRootTag.get(rootTag); - addPreAllocateMountItem( new PreAllocateViewMountItem( - context, rootTag, reactTag, getFabricComponentName(componentName), @@ -350,12 +330,7 @@ private void preallocateView( @ThreadConfined(ANY) private MountItem createIntBufferBatchMountItem( int rootTag, int[] intBuffer, Object[] objBuffer, int commitNumber) { - // This could be null if teardown/navigation away from a surface on the main thread happens - // while a commit is being processed in a different thread. By contract we expect this to be - // possible at teardown, but this race should *never* happen at startup. - @Nullable ThemedReactContext reactContext = mReactContextForRootTag.get(rootTag); - - return new IntBufferBatchMountItem(rootTag, reactContext, intBuffer, objBuffer, commitNumber); + return new IntBufferBatchMountItem(rootTag, intBuffer, objBuffer, commitNumber); } @DoNotStrip @@ -398,7 +373,7 @@ private long measure( @DoNotStrip @SuppressWarnings("unused") private long measure( - int rootTag, + int surfaceId, String componentName, ReadableMap localData, ReadableMap props, @@ -409,16 +384,16 @@ private long measure( float maxHeight, @Nullable float[] attachmentsPositions) { - // This could be null if teardown/navigation away from a surface on the main thread happens - // while a commit is being processed in a different thread. By contract we expect this to be - // possible at teardown, but this race should *never* happen at startup. - @Nullable - ReactContext context = - rootTag < 0 ? mReactApplicationContext : mReactContextForRootTag.get(rootTag); - - // Don't both measuring if we can't get a context. - if (context == null) { - return 0; + ReactContext context; + if (surfaceId > 0) { + SurfaceMountingManager surfaceMountingManager = + mMountingManager.getSurfaceManagerEnforced(surfaceId, "measure"); + if (surfaceMountingManager.isStopped()) { + return 0; + } + context = surfaceMountingManager.getContext(); + } else { + context = mReactApplicationContext; } return mMountingManager.measure( @@ -435,22 +410,16 @@ private long measure( } /** - * @param surfaceID {@link int} surface ID + * @param surfaceId {@link int} surface ID * @param defaultTextInputPadding {@link float[]} output parameter will contain the default theme * padding used by RN Android TextInput. * @return if theme data is available in the output parameters. */ @DoNotStrip - public boolean getThemeData(int surfaceID, float[] defaultTextInputPadding) { - ThemedReactContext themedReactContext = mReactContextForRootTag.get(surfaceID); - if (themedReactContext == null) { - // TODO T68526882: Review if this should cause a crash instead. - ReactSoftException.logSoftException( - TAG, - new ReactNoCrashSoftException( - "Unable to find ThemedReactContext associated to surfaceID: " + surfaceID)); - return false; - } + public boolean getThemeData(int surfaceId, float[] defaultTextInputPadding) { + SurfaceMountingManager surfaceMountingManager = + mMountingManager.getSurfaceManagerEnforced(surfaceId, "getThemeData"); + ThemedReactContext themedReactContext = surfaceMountingManager.getContext(); float[] defaultTextInputPaddingForTheme = UIManagerHelper.getDefaultTextInputPadding(themedReactContext); defaultTextInputPadding[0] = defaultTextInputPaddingForTheme[PADDING_START_INDEX]; @@ -615,9 +584,16 @@ private void scheduleMountItem( } } + /** + * Try to dispatch MountItems. Returns true if any items were dispatched, false otherwise. A + * `false` return value doesn't indicate errors, it may just indicate there was no work to be + * done. + * + * @return + */ @UiThread @ThreadConfined(UI) - private void tryDispatchMountItems() { + private boolean tryDispatchMountItems() { // If we're already dispatching, don't reenter. // Reentrance can potentially happen a lot on Android in Fabric because // `updateState` from the @@ -627,7 +603,7 @@ private void tryDispatchMountItems() { // This is a pretty blunt tool, but we might not have better options since we really don't want // to execute anything out-of-order. if (mInDispatch) { - return; + return false; } final boolean didDispatchItems; @@ -661,6 +637,7 @@ private void tryDispatchMountItems() { tryDispatchMountItems(); } mReDispatchCounter = 0; + return didDispatchItems; } @Nullable @@ -694,35 +671,6 @@ private Collection getAndResetPreMountItems() { return drainConcurrentItemQueue(mPreMountItemsConcurrent); } - /** - * Check if a surfaceId is active and ready for MountItems to be executed against it. It is safe - * and cheap to call this repeatedly because we expect many operations to be batched with the same - * surfaceId in a row and we memoize the parameters and results. - * - * @param surfaceId - * @param context - * @return - */ - @UiThread - @ThreadConfined(UI) - private boolean isSurfaceActiveForExecution(int surfaceId, String context) { - boolean surfaceActive = mReactContextForRootTag.get(surfaceId) != null; - - // If there are many MountItems with the same SurfaceId, we only - // log a warning for the first one that is skipped. - if (!surfaceActive) { - ReactSoftException.logSoftException( - TAG, - new ReactNoCrashSoftException( - "dispatchMountItems: skipping " - + context - + ", because surface not available: " - + surfaceId)); - } - - return surfaceActive; - } - private static void printMountItem(MountItem mountItem, String prefix) { // If a MountItem description is split across multiple lines, it's because it's a // compound MountItem. Log each line separately. @@ -812,10 +760,7 @@ private boolean dispatchMountItems() { + preMountItemsToDispatch.size()); for (PreAllocateViewMountItem preMountItem : preMountItemsToDispatch) { - if (isSurfaceActiveForExecution( - preMountItem.getRootTag(), "dispatchMountItems PreAllocateViewMountItem")) { - preMountItem.execute(mMountingManager); - } + preMountItem.execute(mMountingManager); } Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); @@ -834,16 +779,6 @@ private boolean dispatchMountItems() { } try { - // Make sure surface associated with this MountItem has been started, and not stopped. - // TODO T68118357: clean up this logic and simplify this method overall - if (mountItem instanceof IntBufferBatchMountItem) { - IntBufferBatchMountItem batchMountItem = (IntBufferBatchMountItem) mountItem; - if (!isSurfaceActiveForExecution( - batchMountItem.getRootTag(), "dispatchMountItems IntBufferBatchMountItem")) { - continue; - } - } - mountItem.execute(mMountingManager); } catch (Throwable e) { // If there's an exception, we want to log diagnostics in prod and rethrow. @@ -866,6 +801,17 @@ private boolean dispatchMountItems() { return true; } + /** + * Detect if we still have processing time left in this frame. + * + * @param frameTimeNanos + * @return + */ + private boolean haveExceededNonBatchedFrameTime(long frameTimeNanos) { + long timeLeftInFrame = FRAME_TIME_MS - ((System.nanoTime() - frameTimeNanos) / 1000000); + return timeLeftInFrame < MAX_TIME_IN_FRAME_FOR_NON_BATCHED_OPERATIONS_MS; + } + @UiThread @ThreadConfined(UI) private void dispatchPreMountItems(long frameTimeNanos) { @@ -877,8 +823,7 @@ private void dispatchPreMountItems(long frameTimeNanos) { try { while (true) { - long timeLeftInFrame = FRAME_TIME_MS - ((System.nanoTime() - frameTimeNanos) / 1000000); - if (timeLeftInFrame < MAX_TIME_IN_FRAME_FOR_NON_BATCHED_OPERATIONS_MS) { + if (haveExceededNonBatchedFrameTime(frameTimeNanos)) { break; } @@ -889,10 +834,7 @@ private void dispatchPreMountItems(long frameTimeNanos) { break; } - if (isSurfaceActiveForExecution( - preMountItemToDispatch.getRootTag(), "dispatchPreMountItems")) { - preMountItemToDispatch.execute(mMountingManager); - } + preMountItemToDispatch.execute(mMountingManager); } } finally { mInDispatch = false; @@ -912,7 +854,7 @@ public void setBinding(Binding binding) { @UiThread @ThreadConfined(UI) public void updateRootLayoutSpecs( - final int rootTag, + final int surfaceId, final int widthMeasureSpec, final int heightMeasureSpec, final int offsetX, @@ -922,7 +864,9 @@ public void updateRootLayoutSpecs( FLog.d(TAG, "Updating Root Layout Specs"); } - ThemedReactContext reactContext = mReactContextForRootTag.get(rootTag); + SurfaceMountingManager surfaceMountingManager = + mMountingManager.getSurfaceManagerEnforced(surfaceId, "updateRootLayoutSpecs"); + ThemedReactContext reactContext = surfaceMountingManager.getContext(); boolean isRTL = false; boolean doLeftAndRightSwapInRTL = false; if (reactContext != null) { @@ -931,7 +875,7 @@ public void updateRootLayoutSpecs( } mBinding.setConstraints( - rootTag, + surfaceId, getMinSize(widthMeasureSpec), getMaxSize(widthMeasureSpec), getMinSize(heightMeasureSpec), @@ -976,21 +920,48 @@ public void onHostPause() { @Override public void onHostDestroy() {} - @Deprecated @Override + @Deprecated @AnyThread @ThreadConfined(ANY) public void dispatchCommand( final int reactTag, final int commandId, @Nullable final ReadableArray commandArgs) { - dispatchCommandMountItem(new DispatchIntCommandMountItem(reactTag, commandId, commandArgs)); + throw new UnsupportedOperationException( + "dispatchCommand called without surfaceId - Fabric dispatchCommand must be called through Fabric JSI API"); } @Override + @Deprecated @AnyThread @ThreadConfined(ANY) public void dispatchCommand( final int reactTag, final String commandId, @Nullable final ReadableArray commandArgs) { - dispatchCommandMountItem(new DispatchStringCommandMountItem(reactTag, commandId, commandArgs)); + throw new UnsupportedOperationException( + "dispatchCommand called without surfaceId - Fabric dispatchCommand must be called through Fabric JSI API"); + } + + @Deprecated + @AnyThread + @ThreadConfined(ANY) + public void dispatchCommand( + final int surfaceId, + final int reactTag, + final int commandId, + @Nullable final ReadableArray commandArgs) { + dispatchCommandMountItem( + new DispatchIntCommandMountItem(surfaceId, reactTag, commandId, commandArgs)); + } + + @DoNotStrip + @AnyThread + @ThreadConfined(ANY) + public void dispatchCommand( + final int surfaceId, + final int reactTag, + final String commandId, + @Nullable final ReadableArray commandArgs) { + dispatchCommandMountItem( + new DispatchStringCommandMountItem(surfaceId, reactTag, commandId, commandArgs)); } @AnyThread @@ -1003,12 +974,15 @@ private void dispatchCommandMountItem(DispatchCommandMountItem command) { @AnyThread @ThreadConfined(ANY) public void sendAccessibilityEvent(int reactTag, int eventType) { - addMountItem(new SendAccessibilityEvent(reactTag, eventType)); + // Can be called from native, not just JS - we need to migrate the native callsites + // before removing this entirely. + addMountItem(new SendAccessibilityEvent(-1, reactTag, eventType)); } + @DoNotStrip @AnyThread @ThreadConfined(ANY) - public void sendAccessibilityEventFromJS(int reactTag, String eventTypeJS) { + public void sendAccessibilityEventFromJS(int surfaceId, int reactTag, String eventTypeJS) { int eventType; if ("focus".equals(eventTypeJS)) { eventType = AccessibilityEvent.TYPE_VIEW_FOCUSED; @@ -1020,7 +994,7 @@ public void sendAccessibilityEventFromJS(int reactTag, String eventTypeJS) { throw new IllegalArgumentException( "sendAccessibilityEventFromJS: invalid eventType " + eventTypeJS); } - addMountItem(new SendAccessibilityEvent(reactTag, eventType)); + addMountItem(new SendAccessibilityEvent(surfaceId, reactTag, eventType)); } /** @@ -1032,12 +1006,16 @@ public void sendAccessibilityEventFromJS(int reactTag, String eventTypeJS) { */ @DoNotStrip public void setJSResponder( - final int reactTag, final int initialReactTag, final boolean blockNativeResponder) { + final int surfaceId, + final int reactTag, + final int initialReactTag, + final boolean blockNativeResponder) { addMountItem( new MountItem() { @Override public void execute(MountingManager mountingManager) { - mountingManager.setJSResponder(reactTag, initialReactTag, blockNativeResponder); + mountingManager.setJSResponder( + surfaceId, reactTag, initialReactTag, blockNativeResponder); } }); } @@ -1161,8 +1139,13 @@ public void doFrameGuarded(long frameTimeNanos) { try { dispatchPreMountItems(frameTimeNanos); + boolean dispatchedMountItems = tryDispatchMountItems(); - tryDispatchMountItems(); + // Only if we did no work (besides preallocation) and have time left, evict stale + // SurfaceMountingManagers + if (!dispatchedMountItems && !haveExceededNonBatchedFrameTime(frameTimeNanos)) { + mMountingManager.evictStaleSurfaces(); + } } catch (Exception ex) { FLog.e(TAG, "Exception thrown when executing UIFrameGuarded", ex); stop(); diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.cpp b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.cpp index b6161d0db0938e..3cb4afdd5dace9 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.cpp +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.cpp @@ -1062,7 +1062,7 @@ void Binding::schedulerDidDispatchCommand( static auto dispatchCommand = jni::findClassStatic(Binding::UIManagerJavaDescriptor) - ->getMethod( + ->getMethod( "dispatchCommand"); local_ref command = make_jstring(commandName); @@ -1071,7 +1071,11 @@ void Binding::schedulerDidDispatchCommand( castReadableArray(ReadableNativeArray::newObjectCxxArgs(args)); dispatchCommand( - localJavaUIManager, shadowView.tag, command.get(), argsArray.get()); + localJavaUIManager, + shadowView.surfaceId, + shadowView.tag, + command.get(), + argsArray.get()); } void Binding::schedulerDidSendAccessibilityEvent( @@ -1088,10 +1092,14 @@ void Binding::schedulerDidSendAccessibilityEvent( static auto sendAccessibilityEventFromJS = jni::findClassStatic(Binding::UIManagerJavaDescriptor) - ->getMethod("sendAccessibilityEventFromJS"); + ->getMethod( + "sendAccessibilityEventFromJS"); sendAccessibilityEventFromJS( - localJavaUIManager, shadowView.tag, eventTypeStr.get()); + localJavaUIManager, + shadowView.surfaceId, + shadowView.tag, + eventTypeStr.get()); } void Binding::schedulerDidSetJSResponder( @@ -1107,10 +1115,11 @@ void Binding::schedulerDidSetJSResponder( static auto setJSResponder = jni::findClassStatic(Binding::UIManagerJavaDescriptor) - ->getMethod("setJSResponder"); + ->getMethod("setJSResponder"); setJSResponder( localJavaUIManager, + shadowView.surfaceId, shadowView.tag, initialShadowView.tag, (jboolean)blockNativeResponder); 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 a22d94046d512a..774a357be78fe0 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 @@ -9,41 +9,29 @@ import static com.facebook.infer.annotation.ThreadConfined.ANY; -import android.content.Context; import android.view.View; -import android.view.ViewGroup; -import android.view.ViewParent; import androidx.annotation.AnyThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import com.facebook.common.logging.FLog; -import com.facebook.infer.annotation.Assertions; import com.facebook.infer.annotation.ThreadConfined; +import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactSoftException; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.ReadableNativeMap; import com.facebook.react.bridge.RetryableMountingLayerException; -import com.facebook.react.bridge.SoftAssertions; import com.facebook.react.bridge.UiThreadUtil; -import com.facebook.react.common.build.ReactBuildConfig; import com.facebook.react.fabric.FabricUIManager; import com.facebook.react.fabric.events.EventEmitterWrapper; import com.facebook.react.fabric.mounting.mountitems.MountItem; import com.facebook.react.touch.JSResponderHandler; import com.facebook.react.uimanager.IllegalViewOperationException; -import com.facebook.react.uimanager.ReactStylesDiffMap; -import com.facebook.react.uimanager.RootView; import com.facebook.react.uimanager.RootViewManager; -import com.facebook.react.uimanager.StateWrapper; import com.facebook.react.uimanager.ThemedReactContext; -import com.facebook.react.uimanager.ViewGroupManager; -import com.facebook.react.uimanager.ViewManager; import com.facebook.react.uimanager.ViewManagerRegistry; import com.facebook.yoga.YogaMeasureMode; -import java.util.LinkedHashMap; -import java.util.TreeSet; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** @@ -52,678 +40,238 @@ */ public class MountingManager { public static final String TAG = MountingManager.class.getSimpleName(); - private static final boolean SHOW_CHANGED_VIEW_HIERARCHIES = ReactBuildConfig.DEBUG && false; - @NonNull private final ConcurrentHashMap mTagToViewState; // any thread - @NonNull private final LinkedHashMap> mRootTagToTags; // UI thread only + @NonNull + private final ConcurrentHashMap mSurfaceIdToManager = + new ConcurrentHashMap<>(); // any thread + + private volatile int mNumStaleSurfaces = 0; + + @Nullable private SurfaceMountingManager mMostRecentSurfaceMountingManager; + @NonNull private final JSResponderHandler mJSResponderHandler = new JSResponderHandler(); @NonNull private final ViewManagerRegistry mViewManagerRegistry; @NonNull private final RootViewManager mRootViewManager = new RootViewManager(); public MountingManager(@NonNull ViewManagerRegistry viewManagerRegistry) { - mTagToViewState = new ConcurrentHashMap<>(); - mRootTagToTags = new LinkedHashMap<>(); mViewManagerRegistry = viewManagerRegistry; } - private static void logViewHierarchy(ViewGroup parent, boolean recurse) { - int parentTag = parent.getId(); - FLog.e(TAG, " "); - for (int i = 0; i < parent.getChildCount(); i++) { - FLog.e( - TAG, - " "); - } - FLog.e(TAG, " "); - - if (recurse) { - FLog.e(TAG, "Displaying Ancestors:"); - ViewParent ancestor = parent.getParent(); - while (ancestor != null) { - ViewGroup ancestorViewGroup = (ancestor instanceof ViewGroup ? (ViewGroup) ancestor : null); - int ancestorId = ancestorViewGroup == null ? View.NO_ID : ancestorViewGroup.getId(); - FLog.e( - TAG, - ""); - ancestor = ancestor.getParent(); - } - } - } - /** - * This mutates the rootView, which is an Android View, so this should only be called on the UI - * thread. + * Evict stale SurfaceManagers. * - * @param reactRootTag - * @param rootView + *

The reasoning here is that we want SurfaceManagers to stay around for a little while after + * the Surface is stopped, to gracefully handle race conditions with (1) native libraries like + * NativeAnimatedModule, (2) events emitted to nodes on the surface, (3) queued imperative calls + * like dispatchCommand or sendAccessibilityEvent. + * + *

Without keeping the SurfaceManager around, those race conditions would result in us not + * being able to resolve a tag at all, meaning some operation is happening with a totally invalid, + * unknown tag. However, we want to fail gracefully since it's common for operations to be queued + * up and races to happen with StopSurface. This way, we can distinguish between those race + * conditions and other totally invalid operations on non-existing nodes. */ - @AnyThread - public void addRootView(final int reactRootTag, @NonNull final View rootView) { - mTagToViewState.put( - reactRootTag, new ViewState(reactRootTag, rootView, mRootViewManager, true)); - - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - if (rootView.getId() != View.NO_ID) { - FLog.e( - TAG, - "Trying to add RootTag to RootView that already has a tag: existing tag: [%d] new tag: [%d]", - rootView.getId(), - reactRootTag); - throw new IllegalViewOperationException( - "Trying to add a root view with an explicit id already set. React Native uses " - + "the id field to track react tags and will overwrite this field. If that is fine, " - + "explicitly overwrite the id field to View.NO_ID before calling addRootView."); - } - rootView.setId(reactRootTag); - mRootTagToTags.put(reactRootTag, new TreeSet()); - } - }); - } - - /** Delete rootView and all children recursively. */ @UiThread - public void deleteRootView(int reactRootTag) { - ViewState rootViewState = mTagToViewState.get(reactRootTag); - if (rootViewState != null && rootViewState.mView != null) { - dropView(rootViewState.mView, true); - } - - // Iterate through all tags of reactRootTag, delete all retained Views associated with tags - // Tags that could be left-over, even after `dropView` is called: PreAllocated Views that were - // never properly deleted, for example. There might be other causes of leaks as well. - // This doesn't remove Views from the View Hierarchy, it just ensures that we don't leak - // memory by holding onto native Views. - TreeSet tags = mRootTagToTags.get(reactRootTag); - mRootTagToTags.remove(reactRootTag); - for (int tag : tags) { - mTagToViewState.remove(tag); - } - } - - /** Releases all references to given native View. */ - @UiThread - private void dropView(@NonNull View view, boolean deleteImmediately) { + public void evictStaleSurfaces() { UiThreadUtil.assertOnUiThread(); - final int reactTag = view.getId(); - ViewState state = getViewState(reactTag); - ViewManager viewManager = state.mViewManager; - - if (!state.mIsRoot && viewManager != null) { - // For non-root views we notify viewmanager with {@link ViewManager#onDropInstance} - viewManager.onDropViewInstance(view); - } - if (view instanceof ViewGroup && viewManager instanceof ViewGroupManager) { - final ViewGroup viewGroup = (ViewGroup) view; - final ViewGroupManager viewGroupManager = getViewGroupManager(state); - - // As documented elsewhere, sometimes when a child is removed from a parent, that change - // is not immediately available in the hierarchy until a future UI tick. This can cause - // inconsistent child counts, etc, but it can _also_ cause us to drop views that shouldn't, - // because they're removed from the parent but that change isn't immediately visible. So, - // we do two things: 1) delay this logic until the next UI thread tick, 2) ignore children - // who don't report the expected parent. - // For most cases, we _do not_ want this logic to run, anyway, since it either means that we - // don't have a correct set of MountingInstructions; or it means that we're tearing down an - // entire screen, in which case we can safely delete everything immediately, not having - // executed any remove instructions immediately before this. - if (deleteImmediately) { - dropChildren(reactTag, viewGroup, viewGroupManager); - } else { - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - dropChildren(reactTag, viewGroup, viewGroupManager); - } - }); - } + if (mNumStaleSurfaces == 0) { + return; } - mTagToViewState.remove(reactTag); - } + mNumStaleSurfaces = 0; - @UiThread - private void dropChildren( - int reactTag, - @NonNull ViewGroup viewGroup, - @NonNull ViewGroupManager viewGroupManager) { - for (int i = viewGroupManager.getChildCount(viewGroup) - 1; i >= 0; i--) { - View child = viewGroupManager.getChildAt(viewGroup, i); - if (getNullableViewState(child.getId()) != null) { - if (SHOW_CHANGED_VIEW_HIERARCHIES) { - FLog.e( - TAG, - "Automatically dropping view that is still attached to a parent being dropped. Parent: [" - + reactTag - + "] child: [" - + child.getId() - + "]"); - } - ViewParent childParent = child.getParent(); - if (childParent == null || !childParent.equals(viewGroup)) { - int childParentId = - (childParent == null - ? -1 - : (childParent instanceof ViewGroup ? ((ViewGroup) childParent).getId() : -1)); - FLog.e( - TAG, - "Recursively deleting children of [" - + reactTag - + "] but parent of child [" - + child.getId() - + "] is [" - + childParentId - + "]"); + for (Map.Entry entry : mSurfaceIdToManager.entrySet()) { + SurfaceMountingManager surfaceMountingManager = entry.getValue(); + int surfacedId = entry.getKey(); + if (surfaceMountingManager.isStopped()) { + if (surfaceMountingManager.shouldKeepAliveStoppedSurface()) { + mNumStaleSurfaces++; } else { - dropView(child, true); + FLog.e(TAG, "Evicting stale SurfaceMountingManager: [%d]", surfacedId); + mSurfaceIdToManager.remove(surfacedId); + + if (surfaceMountingManager == mMostRecentSurfaceMountingManager) { + mMostRecentSurfaceMountingManager = null; + } } } - viewGroupManager.removeViewAt(viewGroup, i); } } - @UiThread - public void addViewAt(final int parentTag, final int tag, final int index) { - UiThreadUtil.assertOnUiThread(); - ViewState parentViewState = getViewState(parentTag); - if (!(parentViewState.mView instanceof ViewGroup)) { - String message = - "Unable to add a view into a view that is not a ViewGroup. ParentTag: " - + parentTag - + " - Tag: " - + tag - + " - Index: " - + index; - FLog.e(TAG, message); - throw new IllegalStateException(message); - } - final ViewGroup parentView = (ViewGroup) parentViewState.mView; - ViewState viewState = getViewState(tag); - final View view = viewState.mView; - if (view == null) { - throw new IllegalStateException( - "Unable to find view for viewState " + viewState + " and tag " + tag); - } - - // Display children before inserting - if (SHOW_CHANGED_VIEW_HIERARCHIES) { - FLog.e(TAG, "addViewAt: [" + tag + "] -> [" + parentTag + "] idx: " + index + " BEFORE"); - logViewHierarchy(parentView, false); - } - - try { - getViewGroupManager(parentViewState).addView(parentView, view, index); - } catch (IllegalStateException e) { - // Wrap error with more context for debugging - throw new IllegalStateException( - "addViewAt: failed to insert view [" - + tag - + "] into parent [" - + parentTag - + "] at index " - + index, - e); - } - - // Display children after inserting - if (SHOW_CHANGED_VIEW_HIERARCHIES) { - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - FLog.e( - TAG, "addViewAt: [" + tag + "] -> [" + parentTag + "] idx: " + index + " AFTER"); - logViewHierarchy(parentView, false); - } - }); - } - } - - @NonNull - private ViewState getViewState(int tag) { - ViewState viewState = mTagToViewState.get(tag); - if (viewState == null) { - throw new RetryableMountingLayerException("Unable to find viewState view for tag " + tag); + /** + * This mutates the rootView, which is an Android View, so this should only be called on the UI + * thread. + * + * @param surfaceId + * @param rootView + */ + @AnyThread + public void addRootView( + final int surfaceId, @NonNull final View rootView, ThemedReactContext themedReactContext) { + SurfaceMountingManager surfaceMountingManager = + new SurfaceMountingManager( + surfaceId, + rootView, + mJSResponderHandler, + mViewManagerRegistry, + mRootViewManager, + themedReactContext); + + // There could technically be a race condition here if addRootView is called twice from + // different threads, though this is (probably) extremely unlikely, and likely an error. + // This logic to protect against race conditions is a holdover from older code, and we don't + // know if it actually happens in practice - so, we're logging soft exceptions for now. + // This *will* crash in Debug mode, but not in production. + mSurfaceIdToManager.putIfAbsent(surfaceId, surfaceMountingManager); + if (mSurfaceIdToManager.get(surfaceId) != surfaceMountingManager) { + ReactSoftException.logSoftException( + TAG, + new IllegalViewOperationException( + "Called addRootView more than once for the SurfaceId [" + surfaceId + "]")); } - return viewState; - } - - public boolean getViewExists(int tag) { - return mTagToViewState.get(tag) != null; - } - private @Nullable ViewState getNullableViewState(int tag) { - return mTagToViewState.get(tag); + mMostRecentSurfaceMountingManager = mSurfaceIdToManager.get(surfaceId); } - @Deprecated - public void receiveCommand(int reactTag, int commandId, @Nullable ReadableArray commandArgs) { - ViewState viewState = getNullableViewState(reactTag); - - // It's not uncommon for JS to send events as/after a component is being removed from the - // view hierarchy. For example, TextInput may send a "blur" command in response to the view - // disappearing. Throw `ReactNoCrashSoftException` so they're logged but don't crash in dev - // for now. - if (viewState == null) { - throw new RetryableMountingLayerException( - "Unable to find viewState for tag: " + reactTag + " for commandId: " + commandId); - } - - if (viewState.mViewManager == null) { - throw new RetryableMountingLayerException("Unable to find viewManager for tag " + reactTag); - } - - if (viewState.mView == null) { - throw new RetryableMountingLayerException( - "Unable to find viewState view for tag " + reactTag); + @AnyThread + public void stopSurface(final int surfaceId) { + SurfaceMountingManager surfaceMountingManager = mSurfaceIdToManager.get(surfaceId); + if (surfaceMountingManager != null) { + surfaceMountingManager.stopSurface(); + mNumStaleSurfaces++; + + if (surfaceMountingManager == mMostRecentSurfaceMountingManager) { + mMostRecentSurfaceMountingManager = null; + } + } else { + ReactSoftException.logSoftException( + TAG, + new IllegalViewOperationException( + "Cannot call StopSurface on non-existent surface: [" + surfaceId + "]")); } - viewState.mViewManager.receiveCommand(viewState.mView, commandId, commandArgs); + // We do not evict surfaces right away; the SurfaceMountingManager will stay in memory for a bit + // longer. See SurfaceMountingManager.stopSurface and + // evictStaleSurfaces for more details. } - public void receiveCommand( - int reactTag, @NonNull String commandId, @Nullable ReadableArray commandArgs) { - ViewState viewState = getNullableViewState(reactTag); - - // It's not uncommon for JS to send events as/after a component is being removed from the - // view hierarchy. For example, TextInput may send a "blur" command in response to the view - // disappearing. Throw `ReactNoCrashSoftException` so they're logged but don't crash in dev - // for now. - if (viewState == null) { - throw new RetryableMountingLayerException( - "Unable to find viewState for tag: " + reactTag + " for commandId: " + commandId); - } - - if (viewState.mViewManager == null) { - throw new RetryableMountingLayerException( - "Unable to find viewState manager for tag " + reactTag); - } - - if (viewState.mView == null) { - throw new RetryableMountingLayerException( - "Unable to find viewState view for tag " + reactTag); - } - - viewState.mViewManager.receiveCommand(viewState.mView, commandId, commandArgs); + @Nullable + public SurfaceMountingManager getSurfaceManager(int surfaceId) { + return mSurfaceIdToManager.get(surfaceId); } - public void sendAccessibilityEvent(int reactTag, int eventType) { - ViewState viewState = getViewState(reactTag); - - if (viewState.mViewManager == null) { - throw new RetryableMountingLayerException( - "Unable to find viewState manager for tag " + reactTag); - } + @NonNull + public SurfaceMountingManager getSurfaceManagerEnforced(int surfaceId, String context) { + SurfaceMountingManager surfaceMountingManager = getSurfaceManager(surfaceId); - if (viewState.mView == null) { + if (surfaceMountingManager == null) { throw new RetryableMountingLayerException( - "Unable to find viewState view for tag " + reactTag); + "Unable to find SurfaceMountingManager for surfaceId: [" + + surfaceId + + "]. Context: " + + context); } - viewState.mView.sendAccessibilityEvent(eventType); + return surfaceMountingManager; } - @SuppressWarnings("unchecked") // prevents unchecked conversion warn of the type - private static @NonNull ViewGroupManager getViewGroupManager( - @NonNull ViewState viewState) { - if (viewState.mViewManager == null) { - throw new IllegalStateException("Unable to find ViewManager for view: " + viewState); - } - return (ViewGroupManager) viewState.mViewManager; - } - - @UiThread - public void removeViewAt(final int tag, final int parentTag, int index) { - UiThreadUtil.assertOnUiThread(); - ViewState viewState = getNullableViewState(parentTag); - - if (viewState == null) { - ReactSoftException.logSoftException( - MountingManager.TAG, - new IllegalStateException( - "Unable to find viewState for tag: " + parentTag + " for removeViewAt")); - return; - } - - final ViewGroup parentView = (ViewGroup) viewState.mView; - - if (parentView == null) { - throw new IllegalStateException("Unable to find view for tag " + parentTag); - } - - if (SHOW_CHANGED_VIEW_HIERARCHIES) { - // Display children before deleting any - FLog.e(TAG, "removeViewAt: [" + tag + "] -> [" + parentTag + "] idx: " + index + " BEFORE"); - logViewHierarchy(parentView, false); - } - - ViewGroupManager viewGroupManager = getViewGroupManager(viewState); - - // Verify that the view we're about to remove has the same tag we expect - View view = viewGroupManager.getChildAt(parentView, index); - int actualTag = (view != null ? view.getId() : -1); - if (actualTag != tag) { - int tagActualIndex = -1; - int parentChildrenCount = parentView.getChildCount(); - for (int i = 0; i < parentChildrenCount; i++) { - if (parentView.getChildAt(i).getId() == tag) { - tagActualIndex = i; - break; + /** + * Get SurfaceMountingManager associated with a ReactTag. Unfortunately, this requires lookups + * over N maps, where N is the number of active or recently-stopped Surfaces. Each lookup will + * cost `log(M)` operations where M is the number of reactTags in the surface, so the total cost + * per lookup is `O(N * log(M))`. + * + *

To mitigate this cost, we attempt to keep track of the "most recent" SurfaceMountingManager + * and do lookups in it first. For the vast majority of use-cases, except for events or operations + * sent to off-screen surfaces, or use-cases where multiple surfaces are visible and interactable, + * this will reduce the lookup time to `O(log(M))`. Someone smarter than me could probably figure + * out an amortized time. + * + * @param reactTag + * @return + */ + @Nullable + public SurfaceMountingManager getSurfaceManagerForView(int reactTag) { + if (mMostRecentSurfaceMountingManager != null + && mMostRecentSurfaceMountingManager.getViewExists(reactTag)) { + return mMostRecentSurfaceMountingManager; + } + + for (Map.Entry entry : mSurfaceIdToManager.entrySet()) { + SurfaceMountingManager smm = entry.getValue(); + if (smm != mMostRecentSurfaceMountingManager && smm.getViewExists(reactTag)) { + if (mMostRecentSurfaceMountingManager == null) { + mMostRecentSurfaceMountingManager = smm; } + return smm; } - - // TODO T74425739: previously, we did not do this check and `removeViewAt` would be executed - // below, sometimes crashing there. *However*, interestingly enough, `removeViewAt` would not - // complain if you removed views from an already-empty parent. This seems necessary currently - // for certain ViewManagers that remove their own children - like BottomSheet? - // This workaround seems not-great, but for now, we just return here for - // backwards-compatibility. Essentially, if a view has already been removed from the - // hierarchy, we treat it as a noop. - if (tagActualIndex == -1) { - FLog.e( - TAG, - "removeViewAt: [" - + tag - + "] -> [" - + parentTag - + "] @" - + index - + ": view already removed from parent! Children in parent: " - + parentChildrenCount); - return; - } - - // Here we are guaranteed that the view is still in the View hierarchy, just - // at a different index. In debug mode we'll crash here; in production, we'll remove - // the child from the parent and move on. - // This is an issue that is safely recoverable 95% of the time. If this allows corruption - // of the view hierarchy and causes bugs or a crash after this point, there will be logs - // indicating that this happened. - // This is likely *only* necessary because of Fabric's LayoutAnimations implementation. - // If we can fix the bug there, or remove the need for LayoutAnimation index adjustment - // entirely, we can just throw this exception without regression user experience. - logViewHierarchy(parentView, true); - ReactSoftException.logSoftException( - TAG, - new IllegalStateException( - "Tried to remove view [" - + tag - + "] of parent [" - + parentTag - + "] at index " - + index - + ", but got view tag " - + actualTag - + " - actual index of view: " - + tagActualIndex)); - index = tagActualIndex; - } - - try { - viewGroupManager.removeViewAt(parentView, index); - } catch (RuntimeException e) { - // Note: `getChildCount` may not always be accurate! - // We don't currently have a good explanation other than, in situations where you - // would empirically expect to see childCount > 0, the childCount is reported as 0. - // This is likely due to a ViewManager overriding getChildCount or some other methods - // in a way that is strictly incorrect, but potentially only visible here. - // The failure mode is actually that in `removeViewAt`, a NullPointerException is - // thrown when we try to perform an operation on a View that doesn't exist, and - // is therefore null. - // We try to add some extra diagnostics here, but we always try to remove the View - // from the hierarchy first because detecting by looking at childCount will not work. - // - // Note that the lesson here is that `getChildCount` is not /required/ to adhere to - // any invariants. If you add 9 children to a parent, the `getChildCount` of the parent - // may not be equal to 9. This apparently causes no issues with Android and is common - // enough that we shouldn't try to change this invariant, without a lot of thought. - int childCount = viewGroupManager.getChildCount(parentView); - - logViewHierarchy(parentView, true); - - throw new IllegalStateException( - "Cannot remove child at index " - + index - + " from parent ViewGroup [" - + parentView.getId() - + "], only " - + childCount - + " children in parent. Warning: childCount may be incorrect!", - e); - } - - // Display children after deleting any - if (SHOW_CHANGED_VIEW_HIERARCHIES) { - final int finalIndex = index; - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - FLog.e( - TAG, - "removeViewAt: [" - + tag - + "] -> [" - + parentTag - + "] idx: " - + finalIndex - + " AFTER"); - logViewHierarchy(parentView, false); - } - }); } + return null; } - @UiThread - public void createView( - @NonNull ThemedReactContext themedReactContext, - @NonNull String componentName, - int rootTag, - int reactTag, - @Nullable ReadableMap props, - @Nullable StateWrapper stateWrapper, - boolean isLayoutable) { - if (getNullableViewState(reactTag) != null) { - return; - } - - View view = null; - ViewManager viewManager = null; - - ReactStylesDiffMap propsDiffMap = null; - if (props != null) { - propsDiffMap = new ReactStylesDiffMap(props); - } + @NonNull + @AnyThread + public SurfaceMountingManager getSurfaceManagerForViewEnforced(int reactTag) { + SurfaceMountingManager surfaceMountingManager = getSurfaceManagerForView(reactTag); - if (isLayoutable) { - viewManager = mViewManagerRegistry.get(componentName); - // View Managers are responsible for dealing with initial state and props. - view = - viewManager.createView( - reactTag, themedReactContext, propsDiffMap, stateWrapper, mJSResponderHandler); - view.setId(reactTag); + if (surfaceMountingManager == null) { + throw new RetryableMountingLayerException( + "Unable to find SurfaceMountingManager for tag: [" + reactTag + "]"); } - ViewState viewState = new ViewState(reactTag, view, viewManager); - viewState.mCurrentProps = propsDiffMap; - viewState.mCurrentState = (stateWrapper != null ? stateWrapper.getState() : null); - - mTagToViewState.put(reactTag, viewState); - mRootTagToTags.get(rootTag).add(reactTag); + return surfaceMountingManager; } - @UiThread - public void updateProps(int reactTag, @Nullable ReadableMap props) { - if (props == null) { - return; - } - UiThreadUtil.assertOnUiThread(); - ViewState viewState = getViewState(reactTag); - viewState.mCurrentProps = new ReactStylesDiffMap(props); - View view = viewState.mView; - - if (view == null) { - throw new IllegalStateException("Unable to find view for tag " + reactTag); - } - - Assertions.assertNotNull(viewState.mViewManager) - .updateProperties(view, viewState.mCurrentProps); + public boolean getViewExists(int reactTag) { + return getSurfaceManagerForView(reactTag) != null; } - @UiThread - public void updateLayout(int reactTag, int x, int y, int width, int height, int displayType) { + @Deprecated + public void receiveCommand( + int surfaceId, int reactTag, int commandId, @Nullable ReadableArray commandArgs) { UiThreadUtil.assertOnUiThread(); - - ViewState viewState = getViewState(reactTag); - // Do not layout Root Views - if (viewState.mIsRoot) { - return; - } - - View viewToUpdate = viewState.mView; - if (viewToUpdate == null) { - throw new IllegalStateException("Unable to find View for tag: " + reactTag); - } - - viewToUpdate.measure( - View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), - View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)); - - ViewParent parent = viewToUpdate.getParent(); - if (parent instanceof RootView) { - parent.requestLayout(); - } - - // TODO: T31905686 Check if the parent of the view has to layout the view, or the child has - // to lay itself out. see NativeViewHierarchyManager.updateLayout - viewToUpdate.layout(x, y, x + width, y + height); - - // displayType: 0 represents display: 'none' - int visibility = displayType == 0 ? View.INVISIBLE : View.VISIBLE; - if (viewToUpdate.getVisibility() != visibility) { - viewToUpdate.setVisibility(visibility); - } + getSurfaceManagerEnforced(surfaceId, "receiveCommand:int") + .receiveCommand(reactTag, commandId, commandArgs); } - @UiThread - public void updatePadding(int reactTag, int left, int top, int right, int bottom) { + public void receiveCommand( + int surfaceId, int reactTag, @NonNull String commandId, @Nullable ReadableArray commandArgs) { UiThreadUtil.assertOnUiThread(); - - ViewState viewState = getViewState(reactTag); - // Do not layout Root Views - if (viewState.mIsRoot) { - return; - } - - View viewToUpdate = viewState.mView; - if (viewToUpdate == null) { - throw new IllegalStateException("Unable to find View for tag: " + reactTag); - } - - ViewManager viewManager = viewState.mViewManager; - if (viewManager == null) { - throw new IllegalStateException("Unable to find ViewManager for view: " + viewState); - } - - //noinspection unchecked - viewManager.setPadding(viewToUpdate, left, top, right, bottom); + getSurfaceManagerEnforced(surfaceId, "receiveCommand:string") + .receiveCommand(reactTag, commandId, commandArgs); } - @UiThread - public void deleteView(int rootTag, int reactTag) { + /** + * Send an accessibility eventType to a Native View. eventType is any valid `AccessibilityEvent.X` + * value. + * + *

Why accept `-1` SurfaceId? Currently there are calls to + * UIManagerModule.sendAccessibilityEvent which is a legacy API and accepts only reactTag. We will + * have to investigate and migrate away from those calls over time. + * + * @param surfaceId + * @param reactTag + * @param eventType + */ + public void sendAccessibilityEvent(int surfaceId, int reactTag, int eventType) { UiThreadUtil.assertOnUiThread(); - ViewState viewState = getNullableViewState(reactTag); - - if (viewState == null) { - ReactSoftException.logSoftException( - MountingManager.TAG, - new IllegalStateException( - "Unable to find viewState for tag: " + reactTag + " for deleteView")); - return; - } - - // To delete we simply remove the tag from the registry. - // In the past we called dropView here, but we want to rely on either - // (1) the correct set of MountInstructions being sent to the platform - // and/or (2) dropView being called by stopSurface. - // If Views are orphaned at this stage and leaked, it's a problem in - // the differ or LayoutAnimations, not MountingManager. - // Additionally, as documented in `dropView`, we cannot always trust a - // view's children to be up-to-date. - mTagToViewState.remove(reactTag); - mRootTagToTags.get(rootTag).remove(reactTag); - - // For non-root views we notify viewmanager with {@link ViewManager#onDropInstance} - ViewManager viewManager = viewState.mViewManager; - if (!viewState.mIsRoot && viewManager != null) { - viewManager.onDropViewInstance(viewState.mView); + if (surfaceId != -1) { + getSurfaceManagerForViewEnforced(reactTag).sendAccessibilityEvent(reactTag, eventType); + } else { + getSurfaceManagerEnforced(surfaceId, "sendAccessibilityEvent") + .sendAccessibilityEvent(reactTag, eventType); } } @UiThread - public void updateState(final int reactTag, @Nullable StateWrapper stateWrapper) { + public void updateProps(int reactTag, @Nullable ReadableMap props) { UiThreadUtil.assertOnUiThread(); - ViewState viewState = getViewState(reactTag); - @Nullable ReadableNativeMap newState = stateWrapper == null ? null : stateWrapper.getState(); - - viewState.mCurrentState = newState; - - ViewManager viewManager = viewState.mViewManager; - - if (viewManager == null) { - throw new IllegalStateException("Unable to find ViewManager for tag: " + reactTag); - } - Object extraData = - viewManager.updateState(viewState.mView, viewState.mCurrentProps, stateWrapper); - if (extraData != null) { - viewManager.updateExtraData(viewState.mView, extraData); - } - } - - @UiThread - public void preallocateView( - @NonNull ThemedReactContext reactContext, - String componentName, - int rootTag, - int reactTag, - @Nullable ReadableMap props, - @Nullable StateWrapper stateWrapper, - boolean isLayoutable) { - - if (getNullableViewState(reactTag) != null) { - throw new IllegalStateException( - "View for component " + componentName + " with tag " + reactTag + " already exists."); - } - - // Views can be preallocated before the surface is started - if (mRootTagToTags.get(rootTag) == null) { - mRootTagToTags.put(rootTag, new TreeSet()); + if (props == null) { + return; } - createView(reactContext, componentName, rootTag, reactTag, props, stateWrapper, isLayoutable); - } - - @UiThread - public void updateEventEmitter(int reactTag, @NonNull EventEmitterWrapper eventEmitter) { - UiThreadUtil.assertOnUiThread(); - ViewState viewState = mTagToViewState.get(reactTag); - if (viewState == null) { - // TODO T62717437 - Use a flag to determine that these event emitters belong to virtual nodes - // only. - viewState = new ViewState(reactTag, null, null); - mTagToViewState.put(reactTag, viewState); - } - viewState.mEventEmitter = eventEmitter; + getSurfaceManagerForViewEnforced(reactTag).updateProps(reactTag, props); } /** @@ -745,43 +293,52 @@ public void updateEventEmitter(int reactTag, @NonNull EventEmitterWrapper eventE */ @UiThread public synchronized void setJSResponder( - int reactTag, int initialReactTag, boolean blockNativeResponder) { - if (!blockNativeResponder) { - mJSResponderHandler.setJSResponder(initialReactTag, null); - return; - } - - ViewState viewState = getViewState(reactTag); - View view = viewState.mView; - if (initialReactTag != reactTag && view instanceof ViewParent) { - // In this case, initialReactTag corresponds to a virtual/layout-only View, and we already - // have a parent of that View in reactTag, so we can use it. - mJSResponderHandler.setJSResponder(initialReactTag, (ViewParent) view); - return; - } else if (view == null) { - SoftAssertions.assertUnreachable("Cannot find view for tag " + reactTag + "."); - return; - } + int surfaceId, int reactTag, int initialReactTag, boolean blockNativeResponder) { + UiThreadUtil.assertOnUiThread(); - if (viewState.mIsRoot) { - SoftAssertions.assertUnreachable( - "Cannot block native responder on " + reactTag + " that is a root view"); - } - mJSResponderHandler.setJSResponder(initialReactTag, view.getParent()); + getSurfaceManagerEnforced(surfaceId, "setJSResponder") + .setJSResponder(reactTag, initialReactTag, blockNativeResponder); } /** - * Clears the JS Responder specified by {@link #setJSResponder(int, int, boolean)}. After this - * method is called, all the touch events are going to be handled by JS. + * Clears the JS Responder specified by {@link #setJSResponder(int, int, int, boolean)}. After + * this method is called, all the touch events are going to be handled by JS. */ @UiThread public void clearJSResponder() { mJSResponderHandler.clearJSResponder(); } + @AnyThread + @ThreadConfined(ANY) + public @Nullable EventEmitterWrapper getEventEmitter(int reactTag) { + SurfaceMountingManager surfaceMountingManager = getSurfaceManagerForView(reactTag); + if (surfaceMountingManager == null) { + return null; + } + return surfaceMountingManager.getEventEmitter(reactTag); + } + + /** + * Measure a component, given localData, props, state, and measurement information. This needs to + * remain here for now - and not in SurfaceMountingManager - because sometimes measures are made + * outside of the context of a Surface; especially from C++ before StartSurface is called. + * + * @param context + * @param componentName + * @param localData + * @param props + * @param state + * @param width + * @param widthMode + * @param height + * @param heightMode + * @param attachmentsPositions + * @return + */ @AnyThread public long measure( - @NonNull Context context, + @NonNull ReactContext context, @NonNull String componentName, @NonNull ReadableMap localData, @NonNull ReadableMap props, @@ -806,57 +363,7 @@ public long measure( attachmentsPositions); } - @AnyThread - @ThreadConfined(ANY) - public @Nullable EventEmitterWrapper getEventEmitter(int reactTag) { - ViewState viewState = getNullableViewState(reactTag); - return viewState == null ? null : viewState.mEventEmitter; - } - public void initializeViewManager(String componentName) { mViewManagerRegistry.get(componentName); } - - /** - * 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. - */ - private static class ViewState { - @Nullable final View mView; - final int mReactTag; - final boolean mIsRoot; - @Nullable final ViewManager mViewManager; - @Nullable public ReactStylesDiffMap mCurrentProps = null; - @Nullable public ReadableMap mCurrentLocalData = null; - @Nullable public ReadableMap mCurrentState = null; - @Nullable public EventEmitterWrapper mEventEmitter = null; - - private ViewState(int reactTag, @Nullable View view, @Nullable ViewManager viewManager) { - this(reactTag, view, viewManager, false); - } - - private ViewState(int reactTag, @Nullable View view, ViewManager viewManager, boolean isRoot) { - mReactTag = reactTag; - mView = view; - mIsRoot = isRoot; - mViewManager = viewManager; - } - - @Override - public String toString() { - boolean isLayoutOnly = mViewManager == null; - return "ViewState [" - + mReactTag - + "] - isRoot: " - + mIsRoot - + " - props: " - + mCurrentProps - + " - localData: " - + mCurrentLocalData - + " - viewManager: " - + mViewManager - + " - isLayoutOnly: " - + isLayoutOnly; - } - } } 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 new file mode 100644 index 00000000000000..6581e8aeedbc78 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.java @@ -0,0 +1,822 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.fabric.mounting; + +import static com.facebook.infer.annotation.ThreadConfined.ANY; + +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import androidx.annotation.AnyThread; +import androidx.annotation.NonNull; +import androidx.annotation.UiThread; +import com.facebook.common.logging.FLog; +import com.facebook.infer.annotation.Assertions; +import com.facebook.infer.annotation.ThreadConfined; +import com.facebook.react.bridge.ReactSoftException; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableNativeMap; +import com.facebook.react.bridge.RetryableMountingLayerException; +import com.facebook.react.bridge.SoftAssertions; +import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.common.build.ReactBuildConfig; +import com.facebook.react.fabric.events.EventEmitterWrapper; +import com.facebook.react.touch.JSResponderHandler; +import com.facebook.react.uimanager.IllegalViewOperationException; +import com.facebook.react.uimanager.ReactStylesDiffMap; +import com.facebook.react.uimanager.RootView; +import com.facebook.react.uimanager.RootViewManager; +import com.facebook.react.uimanager.StateWrapper; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.ViewGroupManager; +import com.facebook.react.uimanager.ViewManager; +import com.facebook.react.uimanager.ViewManagerRegistry; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import javax.annotation.Nullable; + +public class SurfaceMountingManager { + public static final String TAG = SurfaceMountingManager.class.getSimpleName(); + private static final boolean SHOW_CHANGED_VIEW_HIERARCHIES = ReactBuildConfig.DEBUG && false; + private static final long KEEPALIVE_MILLISECONDS = 1000; + + private volatile boolean mIsStopped = false; + + @NonNull private final ThemedReactContext mThemedReactContext; + + // These are all non-null, until StopSurface is called + private ConcurrentHashMap mTagToViewState = + new ConcurrentHashMap<>(); // any thread + private JSResponderHandler mJSResponderHandler; + private ViewManagerRegistry mViewManagerRegistry; + private RootViewManager mRootViewManager; + + // This is null *until* StopSurface is called. + private Set mTagSetForStoppedSurface; + private long mLastSuccessfulQueryTime = -1; + + private final int mSurfaceId; + + public SurfaceMountingManager( + int surfaceId, + @NonNull final View rootView, + @NonNull JSResponderHandler jsResponderHandler, + @NonNull ViewManagerRegistry viewManagerRegistry, + @NonNull RootViewManager rootViewManager, + @NonNull ThemedReactContext context) { + mSurfaceId = surfaceId; + mJSResponderHandler = jsResponderHandler; + mViewManagerRegistry = viewManagerRegistry; + mRootViewManager = rootViewManager; + mThemedReactContext = context; + + addRootView(rootView); + } + + public boolean isStopped() { + return mIsStopped; + } + + public boolean shouldKeepAliveStoppedSurface() { + assert mIsStopped; + return (System.currentTimeMillis() - mLastSuccessfulQueryTime) < KEEPALIVE_MILLISECONDS; + } + + public ThemedReactContext getContext() { + return mThemedReactContext; + } + + private static void logViewHierarchy(ViewGroup parent, boolean recurse) { + int parentTag = parent.getId(); + FLog.e(TAG, " "); + for (int i = 0; i < parent.getChildCount(); i++) { + FLog.e( + TAG, + " "); + } + FLog.e(TAG, " "); + + if (recurse) { + FLog.e(TAG, "Displaying Ancestors:"); + ViewParent ancestor = parent.getParent(); + while (ancestor != null) { + ViewGroup ancestorViewGroup = (ancestor instanceof ViewGroup ? (ViewGroup) ancestor : null); + int ancestorId = ancestorViewGroup == null ? View.NO_ID : ancestorViewGroup.getId(); + FLog.e( + TAG, + ""); + ancestor = ancestor.getParent(); + } + } + } + + public boolean getViewExists(int tag) { + // If Surface stopped, check if tag *was* associated with this Surface, even though it's been + // deleted. This helps distinguish between scenarios where an invalid tag is referenced, vs + // race conditions where an imperative method is called on a tag during/just after StopSurface. + if (mTagSetForStoppedSurface != null && mTagSetForStoppedSurface.contains(tag)) { + mLastSuccessfulQueryTime = System.currentTimeMillis(); + return true; + } + if (mTagToViewState == null) { + return false; + } + return mTagToViewState.containsKey(tag); + } + + @AnyThread + private void addRootView(@NonNull final View rootView) { + // Since this is called from the constructor, we know the surface cannot have stopped yet. + mTagToViewState.put(mSurfaceId, new ViewState(mSurfaceId, rootView, mRootViewManager, true)); + + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + // The CPU has ticked since `addRootView` was called, so the surface could technically + // have already stopped here. + if (isStopped()) { + return; + } + + if (rootView.getId() != View.NO_ID) { + FLog.e( + TAG, + "Trying to add RootTag to RootView that already has a tag: existing tag: [%d] new tag: [%d]", + rootView.getId(), + mSurfaceId); + throw new IllegalViewOperationException( + "Trying to add a root view with an explicit id already set. React Native uses " + + "the id field to track react tags and will overwrite this field. If that is fine, " + + "explicitly overwrite the id field to View.NO_ID before calling addRootView."); + } + rootView.setId(mSurfaceId); + } + }); + } + + /** + * Stop surface and all operations within it. Garbage-collect Views (caller is responsible for + * removing RootView from View layer). + * + *

Delete rootView from cache. Since RN does not control the RootView, in a sense, the fragment + * is responsible for actually removing the RootView from the hierarchy / tearing down the + * fragment. + * + *

In the original version(s) of this function, we recursively went through all children of the + * View and dropped those Views as well; ad infinitum. This was before we had a + * SurfaceMountingManager, and all tags were in one global map. Doing this was particularly + * important in the case of StopSurface, where race conditions between threads meant you couldn't + * rely on DELETE instructions actually deleting all Views in the Surface. + * + *

Now that we have SurfaceMountingManager, we can simply drop our local reference to the View. + * Since it will be removed from the View hierarchy entirely (outside of the scope of this class), + * garbage collection will take care of destroying it and all descendents. + */ + @AnyThread + public void stopSurface() { + if (isStopped()) { + return; + } + + // Prevent more views from being created, or the hierarchy from being manipulated at all + mIsStopped = true; + + Runnable runnable = + new Runnable() { + @Override + public void run() { + // Evict all views from cache and memory + mLastSuccessfulQueryTime = System.currentTimeMillis(); + mTagSetForStoppedSurface = mTagToViewState.keySet(); + mTagToViewState = null; + mJSResponderHandler = null; + mRootViewManager = null; + } + }; + + if (UiThreadUtil.isOnUiThread()) { + runnable.run(); + } else { + UiThreadUtil.runOnUiThread(runnable); + } + } + + @UiThread + public void addViewAt(final int parentTag, final int tag, final int index) { + UiThreadUtil.assertOnUiThread(); + if (isStopped()) { + return; + } + + ViewState parentViewState = getViewState(parentTag); + if (!(parentViewState.mView instanceof ViewGroup)) { + String message = + "Unable to add a view into a view that is not a ViewGroup. ParentTag: " + + parentTag + + " - Tag: " + + tag + + " - Index: " + + index; + FLog.e(TAG, message); + throw new IllegalStateException(message); + } + final ViewGroup parentView = (ViewGroup) parentViewState.mView; + ViewState viewState = getViewState(tag); + final View view = viewState.mView; + if (view == null) { + throw new IllegalStateException( + "Unable to find view for viewState " + viewState + " and tag " + tag); + } + + // Display children before inserting + if (SHOW_CHANGED_VIEW_HIERARCHIES) { + FLog.e(TAG, "addViewAt: [" + tag + "] -> [" + parentTag + "] idx: " + index + " BEFORE"); + logViewHierarchy(parentView, false); + } + + try { + getViewGroupManager(parentViewState).addView(parentView, view, index); + } catch (IllegalStateException e) { + // Wrap error with more context for debugging + throw new IllegalStateException( + "addViewAt: failed to insert view [" + + tag + + "] into parent [" + + parentTag + + "] at index " + + index, + e); + } + + // Display children after inserting + if (SHOW_CHANGED_VIEW_HIERARCHIES) { + // Why are we calling `runOnUiThread`? We're already on the UI thread, right?! + // Yes - but if you get the children of the View here and display them, *it might show you + // the previous children*. Without getting too much into Android internals, basically if we + // wait a tick, everything is what we expect. + // tldr is that `parent.children == []; parent.addView(x); parent.children == []` + // and you need to wait a tick for `parent.children == [x]`. + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + FLog.e( + TAG, "addViewAt: [" + tag + "] -> [" + parentTag + "] idx: " + index + " AFTER"); + logViewHierarchy(parentView, false); + } + }); + } + } + + @UiThread + public void removeViewAt(final int tag, final int parentTag, int index) { + if (isStopped()) { + return; + } + + UiThreadUtil.assertOnUiThread(); + ViewState viewState = getNullableViewState(parentTag); + + // TODO: throw exception here? + if (viewState == null) { + ReactSoftException.logSoftException( + MountingManager.TAG, + new IllegalStateException( + "Unable to find viewState for tag: [" + parentTag + "] for removeViewAt")); + return; + } + + final ViewGroup parentView = (ViewGroup) viewState.mView; + + if (parentView == null) { + throw new IllegalStateException("Unable to find view for tag [" + parentTag + "]"); + } + + if (SHOW_CHANGED_VIEW_HIERARCHIES) { + // Display children before deleting any + FLog.e(TAG, "removeViewAt: [" + tag + "] -> [" + parentTag + "] idx: " + index + " BEFORE"); + logViewHierarchy(parentView, false); + } + + ViewGroupManager viewGroupManager = getViewGroupManager(viewState); + + // Verify that the view we're about to remove has the same tag we expect + View view = viewGroupManager.getChildAt(parentView, index); + int actualTag = (view != null ? view.getId() : -1); + if (actualTag != tag) { + int tagActualIndex = -1; + int parentChildrenCount = parentView.getChildCount(); + for (int i = 0; i < parentChildrenCount; i++) { + if (parentView.getChildAt(i).getId() == tag) { + tagActualIndex = i; + break; + } + } + + // TODO T74425739: previously, we did not do this check and `removeViewAt` would be executed + // below, sometimes crashing there. *However*, interestingly enough, `removeViewAt` would not + // complain if you removed views from an already-empty parent. This seems necessary currently + // for certain ViewManagers that remove their own children - like BottomSheet? + // This workaround seems not-great, but for now, we just return here for + // backwards-compatibility. Essentially, if a view has already been removed from the + // hierarchy, we treat it as a noop. + if (tagActualIndex == -1) { + FLog.e( + TAG, + "removeViewAt: [" + + tag + + "] -> [" + + parentTag + + "] @" + + index + + ": view already removed from parent! Children in parent: " + + parentChildrenCount); + return; + } + + // Here we are guaranteed that the view is still in the View hierarchy, just + // at a different index. In debug mode we'll crash here; in production, we'll remove + // the child from the parent and move on. + // This is an issue that is safely recoverable 95% of the time. If this allows corruption + // of the view hierarchy and causes bugs or a crash after this point, there will be logs + // indicating that this happened. + // This is likely *only* necessary because of Fabric's LayoutAnimations implementation. + // If we can fix the bug there, or remove the need for LayoutAnimation index adjustment + // entirely, we can just throw this exception without regression user experience. + logViewHierarchy(parentView, true); + ReactSoftException.logSoftException( + TAG, + new IllegalStateException( + "Tried to remove view [" + + tag + + "] of parent [" + + parentTag + + "] at index " + + index + + ", but got view tag " + + actualTag + + " - actual index of view: " + + tagActualIndex)); + index = tagActualIndex; + } + + try { + viewGroupManager.removeViewAt(parentView, index); + } catch (RuntimeException e) { + // Note: `getChildCount` may not always be accurate! + // We don't currently have a good explanation other than, in situations where you + // would empirically expect to see childCount > 0, the childCount is reported as 0. + // This is likely due to a ViewManager overriding getChildCount or some other methods + // in a way that is strictly incorrect, but potentially only visible here. + // The failure mode is actually that in `removeViewAt`, a NullPointerException is + // thrown when we try to perform an operation on a View that doesn't exist, and + // is therefore null. + // We try to add some extra diagnostics here, but we always try to remove the View + // from the hierarchy first because detecting by looking at childCount will not work. + // + // Note that the lesson here is that `getChildCount` is not /required/ to adhere to + // any invariants. If you add 9 children to a parent, the `getChildCount` of the parent + // may not be equal to 9. This apparently causes no issues with Android and is common + // enough that we shouldn't try to change this invariant, without a lot of thought. + int childCount = viewGroupManager.getChildCount(parentView); + + logViewHierarchy(parentView, true); + + throw new IllegalStateException( + "Cannot remove child at index " + + index + + " from parent ViewGroup [" + + parentView.getId() + + "], only " + + childCount + + " children in parent. Warning: childCount may be incorrect!", + e); + } + + // Display children after deleting any + if (SHOW_CHANGED_VIEW_HIERARCHIES) { + final int finalIndex = index; + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + FLog.e( + TAG, + "removeViewAt: [" + + tag + + "] -> [" + + parentTag + + "] idx: " + + finalIndex + + " AFTER"); + logViewHierarchy(parentView, false); + } + }); + } + } + + @UiThread + public void createView( + @NonNull String componentName, + int reactTag, + @Nullable ReadableMap props, + @Nullable StateWrapper stateWrapper, + boolean isLayoutable) { + if (isStopped()) { + return; + } + if (getNullableViewState(reactTag) != null) { + return; + } + + View view = null; + ViewManager viewManager = null; + + ReactStylesDiffMap propsDiffMap = null; + if (props != null) { + propsDiffMap = new ReactStylesDiffMap(props); + } + + if (isLayoutable) { + viewManager = mViewManagerRegistry.get(componentName); + // View Managers are responsible for dealing with initial state and props. + view = + viewManager.createView( + reactTag, mThemedReactContext, propsDiffMap, stateWrapper, mJSResponderHandler); + view.setId(reactTag); + } + + ViewState viewState = new ViewState(reactTag, view, viewManager); + viewState.mCurrentProps = propsDiffMap; + viewState.mCurrentState = (stateWrapper != null ? stateWrapper.getState() : null); + + mTagToViewState.put(reactTag, viewState); + } + + public void updateProps(int reactTag, ReadableMap props) { + if (isStopped()) { + return; + } + + ViewState viewState = getViewState(reactTag); + viewState.mCurrentProps = new ReactStylesDiffMap(props); + View view = viewState.mView; + + if (view == null) { + throw new IllegalStateException("Unable to find view for tag [" + reactTag + "]"); + } + + Assertions.assertNotNull(viewState.mViewManager) + .updateProperties(view, viewState.mCurrentProps); + } + + @Deprecated + public void receiveCommand(int reactTag, int commandId, @Nullable ReadableArray commandArgs) { + if (isStopped()) { + return; + } + + ViewState viewState = getNullableViewState(reactTag); + + // It's not uncommon for JS to send events as/after a component is being removed from the + // view hierarchy. For example, TextInput may send a "blur" command in response to the view + // disappearing. Throw `ReactNoCrashSoftException` so they're logged but don't crash in dev + // for now. + if (viewState == null) { + throw new RetryableMountingLayerException( + "Unable to find viewState for tag: [" + reactTag + "] for commandId: " + commandId); + } + + if (viewState.mViewManager == null) { + throw new RetryableMountingLayerException("Unable to find viewManager for tag " + reactTag); + } + + if (viewState.mView == null) { + throw new RetryableMountingLayerException( + "Unable to find viewState view for tag " + reactTag); + } + + viewState.mViewManager.receiveCommand(viewState.mView, commandId, commandArgs); + } + + public void receiveCommand( + int reactTag, @NonNull String commandId, @Nullable ReadableArray commandArgs) { + if (isStopped()) { + return; + } + + ViewState viewState = getNullableViewState(reactTag); + + // It's not uncommon for JS to send events as/after a component is being removed from the + // view hierarchy. For example, TextInput may send a "blur" command in response to the view + // disappearing. Throw `ReactNoCrashSoftException` so they're logged but don't crash in dev + // for now. + if (viewState == null) { + throw new RetryableMountingLayerException( + "Unable to find viewState for tag: " + reactTag + " for commandId: " + commandId); + } + + if (viewState.mViewManager == null) { + throw new RetryableMountingLayerException( + "Unable to find viewState manager for tag " + reactTag); + } + + if (viewState.mView == null) { + throw new RetryableMountingLayerException( + "Unable to find viewState view for tag " + reactTag); + } + + viewState.mViewManager.receiveCommand(viewState.mView, commandId, commandArgs); + } + + public void sendAccessibilityEvent(int reactTag, int eventType) { + if (isStopped()) { + return; + } + + ViewState viewState = getViewState(reactTag); + + if (viewState.mViewManager == null) { + throw new RetryableMountingLayerException( + "Unable to find viewState manager for tag " + reactTag); + } + + if (viewState.mView == null) { + throw new RetryableMountingLayerException( + "Unable to find viewState view for tag " + reactTag); + } + + viewState.mView.sendAccessibilityEvent(eventType); + } + + @UiThread + public void updateLayout(int reactTag, int x, int y, int width, int height, int displayType) { + if (isStopped()) { + return; + } + + ViewState viewState = getViewState(reactTag); + // Do not layout Root Views + if (viewState.mIsRoot) { + return; + } + + View viewToUpdate = viewState.mView; + if (viewToUpdate == null) { + throw new IllegalStateException("Unable to find View for tag: " + reactTag); + } + + viewToUpdate.measure( + View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)); + + ViewParent parent = viewToUpdate.getParent(); + if (parent instanceof RootView) { + parent.requestLayout(); + } + + // TODO: T31905686 Check if the parent of the view has to layout the view, or the child has + // to lay itself out. see NativeViewHierarchyManager.updateLayout + viewToUpdate.layout(x, y, x + width, y + height); + + // displayType: 0 represents display: 'none' + int visibility = displayType == 0 ? View.INVISIBLE : View.VISIBLE; + if (viewToUpdate.getVisibility() != visibility) { + viewToUpdate.setVisibility(visibility); + } + } + + @UiThread + public void updatePadding(int reactTag, int left, int top, int right, int bottom) { + UiThreadUtil.assertOnUiThread(); + if (isStopped()) { + return; + } + + ViewState viewState = getViewState(reactTag); + // Do not layout Root Views + if (viewState.mIsRoot) { + return; + } + + View viewToUpdate = viewState.mView; + if (viewToUpdate == null) { + throw new IllegalStateException("Unable to find View for tag: " + reactTag); + } + + ViewManager viewManager = viewState.mViewManager; + if (viewManager == null) { + throw new IllegalStateException("Unable to find ViewManager for view: " + viewState); + } + + //noinspection unchecked + viewManager.setPadding(viewToUpdate, left, top, right, bottom); + } + + @UiThread + public void updateState(final int reactTag, @Nullable StateWrapper stateWrapper) { + UiThreadUtil.assertOnUiThread(); + if (isStopped()) { + return; + } + + ViewState viewState = getViewState(reactTag); + @Nullable ReadableNativeMap newState = stateWrapper == null ? null : stateWrapper.getState(); + + viewState.mCurrentState = newState; + + ViewManager viewManager = viewState.mViewManager; + + if (viewManager == null) { + throw new IllegalStateException("Unable to find ViewManager for tag: " + reactTag); + } + Object extraData = + viewManager.updateState(viewState.mView, viewState.mCurrentProps, stateWrapper); + if (extraData != null) { + viewManager.updateExtraData(viewState.mView, extraData); + } + } + + @UiThread + public void updateEventEmitter(int reactTag, @NonNull EventEmitterWrapper eventEmitter) { + UiThreadUtil.assertOnUiThread(); + if (isStopped()) { + return; + } + + ViewState viewState = mTagToViewState.get(reactTag); + if (viewState == null) { + // TODO T62717437 - Use a flag to determine that these event emitters belong to virtual nodes + // only. + viewState = new ViewState(reactTag, null, null); + mTagToViewState.put(reactTag, viewState); + } + viewState.mEventEmitter = eventEmitter; + } + + @UiThread + public synchronized void setJSResponder( + int reactTag, int initialReactTag, boolean blockNativeResponder) { + UiThreadUtil.assertOnUiThread(); + if (isStopped()) { + return; + } + + if (!blockNativeResponder) { + mJSResponderHandler.setJSResponder(initialReactTag, null); + return; + } + + ViewState viewState = getViewState(reactTag); + View view = viewState.mView; + if (initialReactTag != reactTag && view instanceof ViewParent) { + // In this case, initialReactTag corresponds to a virtual/layout-only View, and we already + // have a parent of that View in reactTag, so we can use it. + mJSResponderHandler.setJSResponder(initialReactTag, (ViewParent) view); + return; + } else if (view == null) { + SoftAssertions.assertUnreachable("Cannot find view for tag [" + reactTag + "]."); + return; + } + + if (viewState.mIsRoot) { + SoftAssertions.assertUnreachable( + "Cannot block native responder on [" + reactTag + "] that is a root view"); + } + mJSResponderHandler.setJSResponder(initialReactTag, view.getParent()); + } + + @UiThread + public void deleteView(int reactTag) { + UiThreadUtil.assertOnUiThread(); + if (isStopped()) { + return; + } + + ViewState viewState = getNullableViewState(reactTag); + + if (viewState == null) { + ReactSoftException.logSoftException( + MountingManager.TAG, + new IllegalStateException( + "Unable to find viewState for tag: " + reactTag + " for deleteView")); + return; + } + + // To delete we simply remove the tag from the registry. + // We want to rely on the correct set of MountInstructions being sent to the platform, + // or StopSurface being called, so we do not handle deleting descendents of the View. + mTagToViewState.remove(reactTag); + + // For non-root views we notify viewmanager with {@link ViewManager#onDropInstance} + ViewManager viewManager = viewState.mViewManager; + if (!viewState.mIsRoot && viewManager != null) { + viewManager.onDropViewInstance(viewState.mView); + } + } + + @UiThread + public void preallocateView( + String componentName, + int reactTag, + @Nullable ReadableMap props, + @Nullable StateWrapper stateWrapper, + boolean isLayoutable) { + UiThreadUtil.assertOnUiThread(); + if (isStopped()) { + return; + } + + if (getNullableViewState(reactTag) != null) { + throw new IllegalStateException( + "View for component " + componentName + " with tag " + reactTag + " already exists."); + } + + createView(componentName, reactTag, props, stateWrapper, isLayoutable); + } + + @AnyThread + @ThreadConfined(ANY) + public @Nullable EventEmitterWrapper getEventEmitter(int reactTag) { + ViewState viewState = getNullableViewState(reactTag); + return viewState == null ? null : viewState.mEventEmitter; + } + + @NonNull + private ViewState getViewState(int tag) { + ViewState viewState = mTagToViewState.get(tag); + if (viewState == null) { + throw new RetryableMountingLayerException("Unable to find viewState view for tag " + tag); + } + return viewState; + } + + private @Nullable ViewState getNullableViewState(int tag) { + return mTagToViewState.get(tag); + } + + @SuppressWarnings("unchecked") // prevents unchecked conversion warn of the type + private static @NonNull ViewGroupManager getViewGroupManager( + @NonNull ViewState viewState) { + if (viewState.mViewManager == null) { + throw new IllegalStateException("Unable to find ViewManager for view: " + viewState); + } + return (ViewGroupManager) viewState.mViewManager; + } + + /** + * 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. + */ + private static class ViewState { + @Nullable final View mView; + final int mReactTag; + final boolean mIsRoot; + @Nullable final ViewManager mViewManager; + @Nullable public ReactStylesDiffMap mCurrentProps = null; + @Nullable public ReadableMap mCurrentLocalData = null; + @Nullable public ReadableMap mCurrentState = null; + @Nullable public EventEmitterWrapper mEventEmitter = null; + + private ViewState(int reactTag, @Nullable View view, @Nullable ViewManager viewManager) { + this(reactTag, view, viewManager, false); + } + + private ViewState(int reactTag, @Nullable View view, ViewManager viewManager, boolean isRoot) { + mReactTag = reactTag; + mView = view; + mIsRoot = isRoot; + mViewManager = viewManager; + } + + @Override + public String toString() { + boolean isLayoutOnly = mViewManager == null; + return "ViewState [" + + mReactTag + + "] - isRoot: " + + mIsRoot + + " - props: " + + mCurrentProps + + " - localData: " + + mCurrentLocalData + + " - viewManager: " + + mViewManager + + " - isLayoutOnly: " + + isLayoutOnly; + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/DispatchIntCommandMountItem.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/DispatchIntCommandMountItem.java index 9d525755bfa38e..176abe1f270d16 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/DispatchIntCommandMountItem.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/DispatchIntCommandMountItem.java @@ -14,12 +14,14 @@ public class DispatchIntCommandMountItem extends DispatchCommandMountItem { + private final int mSurfaceId; private final int mReactTag; private final int mCommandId; private final @Nullable ReadableArray mCommandArgs; public DispatchIntCommandMountItem( - int reactTag, int commandId, @Nullable ReadableArray commandArgs) { + int surfaceId, int reactTag, int commandId, @Nullable ReadableArray commandArgs) { + mSurfaceId = surfaceId; mReactTag = reactTag; mCommandId = commandId; mCommandArgs = commandArgs; @@ -27,7 +29,7 @@ public DispatchIntCommandMountItem( @Override public void execute(@NonNull MountingManager mountingManager) { - mountingManager.receiveCommand(mReactTag, mCommandId, mCommandArgs); + mountingManager.receiveCommand(mSurfaceId, mReactTag, mCommandId, mCommandArgs); } @Override diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/DispatchStringCommandMountItem.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/DispatchStringCommandMountItem.java index 0b04e25f161d0e..361f92ad6197db 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/DispatchStringCommandMountItem.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/DispatchStringCommandMountItem.java @@ -14,12 +14,14 @@ public class DispatchStringCommandMountItem extends DispatchCommandMountItem { + private final int mSurfaceId; private final int mReactTag; @NonNull private final String mCommandId; private final @Nullable ReadableArray mCommandArgs; public DispatchStringCommandMountItem( - int reactTag, @NonNull String commandId, @Nullable ReadableArray commandArgs) { + int surfaceId, int reactTag, @NonNull String commandId, @Nullable ReadableArray commandArgs) { + mSurfaceId = surfaceId; mReactTag = reactTag; mCommandId = commandId; mCommandArgs = commandArgs; @@ -27,7 +29,7 @@ public DispatchStringCommandMountItem( @Override public void execute(@NonNull MountingManager mountingManager) { - mountingManager.receiveCommand(mReactTag, mCommandId, mCommandArgs); + mountingManager.receiveCommand(mSurfaceId, mReactTag, mCommandId, mCommandArgs); } @Override diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/IntBufferBatchMountItem.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/IntBufferBatchMountItem.java index 7aa0d46a2d0b1d..02106de603dcdd 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/IntBufferBatchMountItem.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/IntBufferBatchMountItem.java @@ -8,10 +8,10 @@ package com.facebook.react.fabric.mounting.mountitems; import static com.facebook.react.fabric.FabricComponents.getFabricComponentName; +import static com.facebook.react.fabric.FabricUIManager.ENABLE_FABRIC_LOGS; import static com.facebook.react.fabric.FabricUIManager.IS_DEVELOPMENT_ENVIRONMENT; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import com.facebook.common.logging.FLog; import com.facebook.proguard.annotations.DoNotStrip; import com.facebook.react.bridge.ReactMarker; @@ -19,8 +19,8 @@ import com.facebook.react.bridge.ReadableMap; import com.facebook.react.fabric.events.EventEmitterWrapper; import com.facebook.react.fabric.mounting.MountingManager; +import com.facebook.react.fabric.mounting.SurfaceMountingManager; import com.facebook.react.uimanager.StateWrapper; -import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.systrace.Systrace; /** @@ -50,26 +50,18 @@ public class IntBufferBatchMountItem implements MountItem { static final int INSTRUCTION_UPDATE_EVENT_EMITTER = 256; static final int INSTRUCTION_UPDATE_PADDING = 512; - private final int mRootTag; + private final int mSurfaceId; private final int mCommitNumber; - @NonNull private final ThemedReactContext mContext; - @NonNull private final int[] mIntBuffer; @NonNull private final Object[] mObjBuffer; private final int mIntBufferLen; private final int mObjBufferLen; - public IntBufferBatchMountItem( - int rootTag, - @Nullable ThemedReactContext context, - int[] intBuf, - Object[] objBuf, - int commitNumber) { - mRootTag = rootTag; + public IntBufferBatchMountItem(int surfaceId, int[] intBuf, Object[] objBuf, int commitNumber) { + mSurfaceId = surfaceId; mCommitNumber = commitNumber; - mContext = context; mIntBuffer = intBuf; mObjBuffer = objBuf; @@ -119,13 +111,21 @@ private static EventEmitterWrapper castToEventEmitter(Object obj) { @Override public void execute(@NonNull MountingManager mountingManager) { - if (mContext == null) { + SurfaceMountingManager surfaceMountingManager = mountingManager.getSurfaceManager(mSurfaceId); + if (surfaceMountingManager == null) { FLog.e( TAG, - "Cannot execute batch of %s MountItems; no context. Hopefully this is because StopSurface was called.", - TAG); + "Skipping batch of MountItems; no SurfaceMountingManager found for [%d].", + mSurfaceId); + return; + } + if (surfaceMountingManager.isStopped()) { + FLog.e(TAG, "Skipping batch of MountItems; was stopped [%d].", mSurfaceId); return; } + if (ENABLE_FABRIC_LOGS) { + FLog.d(TAG, "Executing IntBufferBatchMountItem on surface [%d]", mSurfaceId); + } beginMarkers("mountViews"); @@ -137,26 +137,24 @@ public void execute(@NonNull MountingManager mountingManager) { for (int k = 0; k < numInstructions; k++) { if (type == INSTRUCTION_CREATE) { String componentName = getFabricComponentName((String) mObjBuffer[j++]); - mountingManager.createView( - mContext, + surfaceMountingManager.createView( componentName, - mRootTag, mIntBuffer[i++], castToProps(mObjBuffer[j++]), castToState(mObjBuffer[j++]), mIntBuffer[i++] == 1); } else if (type == INSTRUCTION_DELETE) { - mountingManager.deleteView(mRootTag, mIntBuffer[i++]); + surfaceMountingManager.deleteView(mIntBuffer[i++]); } else if (type == INSTRUCTION_INSERT) { int tag = mIntBuffer[i++]; int parentTag = mIntBuffer[i++]; - mountingManager.addViewAt(parentTag, tag, mIntBuffer[i++]); + surfaceMountingManager.addViewAt(parentTag, tag, mIntBuffer[i++]); } else if (type == INSTRUCTION_REMOVE) { - mountingManager.removeViewAt(mIntBuffer[i++], mIntBuffer[i++], mIntBuffer[i++]); + surfaceMountingManager.removeViewAt(mIntBuffer[i++], mIntBuffer[i++], mIntBuffer[i++]); } else if (type == INSTRUCTION_UPDATE_PROPS) { - mountingManager.updateProps(mIntBuffer[i++], castToProps(mObjBuffer[j++])); + surfaceMountingManager.updateProps(mIntBuffer[i++], castToProps(mObjBuffer[j++])); } else if (type == INSTRUCTION_UPDATE_STATE) { - mountingManager.updateState(mIntBuffer[i++], castToState(mObjBuffer[j++])); + surfaceMountingManager.updateState(mIntBuffer[i++], castToState(mObjBuffer[j++])); } else if (type == INSTRUCTION_UPDATE_LAYOUT) { int reactTag = mIntBuffer[i++]; int x = mIntBuffer[i++]; @@ -166,13 +164,14 @@ public void execute(@NonNull MountingManager mountingManager) { // The final buffer, layoutDirection, seems unused? i++; int displayType = mIntBuffer[i++]; - mountingManager.updateLayout(reactTag, x, y, width, height, displayType); + surfaceMountingManager.updateLayout(reactTag, x, y, width, height, displayType); } else if (type == INSTRUCTION_UPDATE_PADDING) { - mountingManager.updatePadding( + surfaceMountingManager.updatePadding( mIntBuffer[i++], mIntBuffer[i++], mIntBuffer[i++], mIntBuffer[i++], mIntBuffer[i++]); } else if (type == INSTRUCTION_UPDATE_EVENT_EMITTER) { - mountingManager.updateEventEmitter(mIntBuffer[i++], castToEventEmitter(mObjBuffer[j++])); + surfaceMountingManager.updateEventEmitter( + mIntBuffer[i++], castToEventEmitter(mObjBuffer[j++])); } else { throw new IllegalArgumentException( "Invalid type argument to IntBufferBatchMountItem: " + type + " at index: " + i); @@ -184,7 +183,7 @@ public void execute(@NonNull MountingManager mountingManager) { } public int getRootTag() { - return mRootTag; + return mSurfaceId; } public boolean shouldSchedule() { diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/PreAllocateViewMountItem.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/PreAllocateViewMountItem.java index 873d67714cb8fb..a39d5ff31c02f0 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/PreAllocateViewMountItem.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/PreAllocateViewMountItem.java @@ -17,30 +17,26 @@ import com.facebook.react.bridge.ReadableMap; import com.facebook.react.fabric.mounting.MountingManager; import com.facebook.react.uimanager.StateWrapper; -import com.facebook.react.uimanager.ThemedReactContext; /** {@link MountItem} that is used to pre-allocate views for JS components. */ public class PreAllocateViewMountItem implements MountItem { @NonNull private final String mComponent; - private final int mRootTag; + private final int mSurfaceId; private final int mReactTag; private final @Nullable ReadableMap mProps; private final @Nullable StateWrapper mStateWrapper; - private final @NonNull ThemedReactContext mContext; private final boolean mIsLayoutable; public PreAllocateViewMountItem( - @Nullable ThemedReactContext context, - int rootTag, + int surfaceId, int reactTag, @NonNull String component, @Nullable ReadableMap props, @NonNull StateWrapper stateWrapper, boolean isLayoutable) { - mContext = context; mComponent = component; - mRootTag = rootTag; + mSurfaceId = surfaceId; mProps = props; mStateWrapper = stateWrapper; mReactTag = reactTag; @@ -48,7 +44,7 @@ public PreAllocateViewMountItem( } public int getRootTag() { - return mRootTag; + return mSurfaceId; } @Override @@ -56,15 +52,9 @@ public void execute(@NonNull MountingManager mountingManager) { if (ENABLE_FABRIC_LOGS) { FLog.d(TAG, "Executing pre-allocation of: " + toString()); } - if (mContext == null) { - throw new IllegalStateException( - "Cannot execute PreAllocateViewMountItem without Context for ReactTag: " - + mReactTag - + " and rootTag: " - + mRootTag); - } - mountingManager.preallocateView( - mContext, mComponent, mRootTag, mReactTag, mProps, mStateWrapper, mIsLayoutable); + mountingManager + .getSurfaceManagerEnforced(mSurfaceId, "PreAllocateViewMountItem") + .preallocateView(mComponent, mReactTag, mProps, mStateWrapper, mIsLayoutable); } @Override @@ -74,8 +64,8 @@ public String toString() { .append(mReactTag) .append("] - component: ") .append(mComponent) - .append(" rootTag: ") - .append(mRootTag) + .append(" surfaceId: ") + .append(mSurfaceId) .append(" isLayoutable: ") .append(mIsLayoutable); diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/SendAccessibilityEvent.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/SendAccessibilityEvent.java index 5852ff1a78c643..9506857f1958e3 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/SendAccessibilityEvent.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/SendAccessibilityEvent.java @@ -16,10 +16,12 @@ public class SendAccessibilityEvent implements MountItem { private final String TAG = "Fabric.SendAccessibilityEvent"; + private final int mSurfaceId; private final int mReactTag; private final int mEventType; - public SendAccessibilityEvent(int reactTag, int eventType) { + public SendAccessibilityEvent(int surfaceId, int reactTag, int eventType) { + mSurfaceId = surfaceId; mReactTag = reactTag; mEventType = eventType; } @@ -27,7 +29,7 @@ public SendAccessibilityEvent(int reactTag, int eventType) { @Override public void execute(@NonNull MountingManager mountingManager) { try { - mountingManager.sendAccessibilityEvent(mReactTag, mEventType); + mountingManager.sendAccessibilityEvent(mSurfaceId, mReactTag, mEventType); } catch (RetryableMountingLayerException e) { // Accessibility events are similar to commands in that they're imperative // calls from JS, disconnected from the commit lifecycle, and therefore diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java index 51f84ebc9400e8..fa24d964f93111 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java @@ -724,8 +724,7 @@ public void clearJSResponder() { @ReactMethod public void dispatchViewManagerCommand( int reactTag, Dynamic commandId, @Nullable ReadableArray commandArgs) { - // TODO: this is a temporary approach to support ViewManagerCommands in Fabric until - // the dispatchViewManagerCommand() method is supported by Fabric JS API. + // Fabric dispatchCommands should go through the JSI API - this will crash in Fabric. @Nullable UIManager uiManager = UIManagerHelper.getUIManager(