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
This commit is contained in:
Joshua Gross
2021-01-21 23:47:34 -08:00
committed by Facebook GitHub Bot
parent 82e9cb1b4e
commit 29eb632f1c
11 changed files with 1196 additions and 881 deletions
@@ -84,7 +84,7 @@ public interface UIManager extends JSIModule, PerformanceCounter {
* 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
@@ -58,6 +58,7 @@ import com.facebook.react.fabric.events.EventBeatManager;
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.Collection;
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<Integer, ThemedReactContext> mReactContextForRootTag =
new ConcurrentHashMap<>();
@NonNull private final EventBeatManager mEventBeatManager;
private boolean mInDispatch = false;
@@ -183,13 +179,11 @@ public class FabricUIManager implements UIManager, LifecycleEventListener {
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 class FabricUIManager implements UIManager, LifecycleEventListener {
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 class FabricUIManager implements UIManager, LifecycleEventListener {
@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 @@ public class FabricUIManager implements UIManager, LifecycleEventListener {
@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 @@ public class FabricUIManager implements UIManager, LifecycleEventListener {
@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 @@ public class FabricUIManager implements UIManager, LifecycleEventListener {
@DoNotStrip
@SuppressWarnings("unused")
private long measure(
int rootTag,
int surfaceId,
String componentName,
ReadableMap localData,
ReadableMap props,
@@ -409,16 +384,16 @@ public class FabricUIManager implements UIManager, LifecycleEventListener {
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 @@ public class FabricUIManager implements UIManager, LifecycleEventListener {
}
/**
* @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 @@ public class FabricUIManager implements UIManager, LifecycleEventListener {
}
}
/**
* 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 @@ public class FabricUIManager implements UIManager, LifecycleEventListener {
// 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 @@ public class FabricUIManager implements UIManager, LifecycleEventListener {
tryDispatchMountItems();
}
mReDispatchCounter = 0;
return didDispatchItems;
}
@Nullable
@@ -694,35 +671,6 @@ public class FabricUIManager implements UIManager, LifecycleEventListener {
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 @@ public class FabricUIManager implements UIManager, LifecycleEventListener {
+ 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 @@ public class FabricUIManager implements UIManager, LifecycleEventListener {
}
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 @@ public class FabricUIManager implements UIManager, LifecycleEventListener {
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 @@ public class FabricUIManager implements UIManager, LifecycleEventListener {
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 @@ public class FabricUIManager implements UIManager, LifecycleEventListener {
break;
}
if (isSurfaceActiveForExecution(
preMountItemToDispatch.getRootTag(), "dispatchPreMountItems")) {
preMountItemToDispatch.execute(mMountingManager);
}
preMountItemToDispatch.execute(mMountingManager);
}
} finally {
mInDispatch = false;
@@ -912,7 +854,7 @@ public class FabricUIManager implements UIManager, LifecycleEventListener {
@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 class FabricUIManager implements UIManager, LifecycleEventListener {
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 class FabricUIManager implements UIManager, LifecycleEventListener {
}
mBinding.setConstraints(
rootTag,
surfaceId,
getMinSize(widthMeasureSpec),
getMaxSize(widthMeasureSpec),
getMinSize(heightMeasureSpec),
@@ -976,21 +920,48 @@ public class FabricUIManager implements UIManager, LifecycleEventListener {
@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 @@ public class FabricUIManager implements UIManager, LifecycleEventListener {
@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 class FabricUIManager implements UIManager, LifecycleEventListener {
throw new IllegalArgumentException(
"sendAccessibilityEventFromJS: invalid eventType " + eventTypeJS);
}
addMountItem(new SendAccessibilityEvent(reactTag, eventType));
addMountItem(new SendAccessibilityEvent(surfaceId, reactTag, eventType));
}
/**
@@ -1032,12 +1006,16 @@ public class FabricUIManager implements UIManager, LifecycleEventListener {
*/
@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 class FabricUIManager implements UIManager, LifecycleEventListener {
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();
@@ -1062,7 +1062,7 @@ void Binding::schedulerDidDispatchCommand(
static auto dispatchCommand =
jni::findClassStatic(Binding::UIManagerJavaDescriptor)
->getMethod<void(jint, jstring, ReadableArray::javaobject)>(
->getMethod<void(jint, jint, jstring, ReadableArray::javaobject)>(
"dispatchCommand");
local_ref<JString> 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<void(jint, jstring)>("sendAccessibilityEventFromJS");
->getMethod<void(jint, jint, jstring)>(
"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<void(jint, jint, jboolean)>("setJSResponder");
->getMethod<void(jint, jint, jint, jboolean)>("setJSResponder");
setJSResponder(
localJavaUIManager,
shadowView.surfaceId,
shadowView.tag,
initialShadowView.tag,
(jboolean)blockNativeResponder);
File diff suppressed because it is too large Load Diff
@@ -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<Integer, ViewState> mTagToViewState =
new ConcurrentHashMap<>(); // any thread
private JSResponderHandler mJSResponderHandler;
private ViewManagerRegistry mViewManagerRegistry;
private RootViewManager mRootViewManager;
// This is null *until* StopSurface is called.
private Set<Integer> 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, " <ViewGroup tag=" + parentTag + " class=" + parent.getClass().toString() + ">");
for (int i = 0; i < parent.getChildCount(); i++) {
FLog.e(
TAG,
" <View idx="
+ i
+ " tag="
+ parent.getChildAt(i).getId()
+ " class="
+ parent.getChildAt(i).getClass().toString()
+ ">");
}
FLog.e(TAG, " </ViewGroup tag=" + parentTag + ">");
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,
"<ViewParent tag=" + ancestorId + " class=" + ancestor.getClass().toString() + ">");
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).
*
* <p>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.
*
* <p>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.
*
* <p>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<ViewGroup> 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 <ViewGroup> type
private static @NonNull ViewGroupManager<ViewGroup> getViewGroupManager(
@NonNull ViewState viewState) {
if (viewState.mViewManager == null) {
throw new IllegalStateException("Unable to find ViewManager for view: " + viewState);
}
return (ViewGroupManager<ViewGroup>) 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;
}
}
}
@@ -14,12 +14,14 @@ import com.facebook.react.fabric.mounting.MountingManager;
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 class DispatchIntCommandMountItem extends DispatchCommandMountItem {
@Override
public void execute(@NonNull MountingManager mountingManager) {
mountingManager.receiveCommand(mReactTag, mCommandId, mCommandArgs);
mountingManager.receiveCommand(mSurfaceId, mReactTag, mCommandId, mCommandArgs);
}
@Override
@@ -14,12 +14,14 @@ import com.facebook.react.fabric.mounting.MountingManager;
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 class DispatchStringCommandMountItem extends DispatchCommandMountItem {
@Override
public void execute(@NonNull MountingManager mountingManager) {
mountingManager.receiveCommand(mReactTag, mCommandId, mCommandArgs);
mountingManager.receiveCommand(mSurfaceId, mReactTag, mCommandId, mCommandArgs);
}
@Override
@@ -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.ReactMarkerConstants;
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 @@ public class IntBufferBatchMountItem implements MountItem {
@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 class IntBufferBatchMountItem implements MountItem {
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 class IntBufferBatchMountItem implements MountItem {
// 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 class IntBufferBatchMountItem implements MountItem {
}
public int getRootTag() {
return mRootTag;
return mSurfaceId;
}
public boolean shouldSchedule() {
@@ -17,30 +17,26 @@ import com.facebook.common.logging.FLog;
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 class PreAllocateViewMountItem implements MountItem {
}
public int getRootTag() {
return mRootTag;
return mSurfaceId;
}
@Override
@@ -56,15 +52,9 @@ public class PreAllocateViewMountItem implements MountItem {
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 class PreAllocateViewMountItem implements MountItem {
.append(mReactTag)
.append("] - component: ")
.append(mComponent)
.append(" rootTag: ")
.append(mRootTag)
.append(" surfaceId: ")
.append(mSurfaceId)
.append(" isLayoutable: ")
.append(mIsLayoutable);
@@ -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 class SendAccessibilityEvent implements MountItem {
@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
@@ -724,8 +724,7 @@ public class UIManagerModule extends ReactContextBaseJavaModule
@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(