/* * 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.uimanager; import android.os.SystemClock; import android.view.View; import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import com.facebook.common.logging.FLog; import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.GuardedRunnable; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactNoCrashSoftException; import com.facebook.react.bridge.ReactSoftExceptionLogger; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.RetryableMountingLayerException; import com.facebook.react.bridge.SoftAssertions; import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.common.ReactConstants; import com.facebook.react.modules.core.ReactChoreographer; import com.facebook.react.uimanager.debug.NotThreadSafeViewHierarchyUpdateDebugListener; import com.facebook.systrace.Systrace; import com.facebook.systrace.SystraceMessage; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; /** * This class acts as a buffer for command executed on {@link NativeViewHierarchyManager}. It expose * similar methods as mentioned classes but instead of executing commands immediately it enqueues * those operations in a queue that is then flushed from {@link UIManagerModule} once JS batch of ui * operations is finished. This is to make sure that we execute all the JS operation coming from a * single batch a single loop of the main (UI) android looper. * *
TODO(7135923): Pooling of operation objects TODO(5694019): Consider a better data structure
* for operations queue to save on allocations
*/
public class UIViewOperationQueue {
public static final int DEFAULT_MIN_TIME_LEFT_IN_FRAME_FOR_NONBATCHED_OPERATION_MS = 8;
private static final String TAG = UIViewOperationQueue.class.getSimpleName();
private final int[] mMeasureBuffer = new int[4];
/** A mutation or animation operation on the view hierarchy. */
public interface UIOperation {
void execute();
}
/** A spec for an operation on the native View hierarchy. */
private abstract class ViewOperation implements UIOperation {
public int mTag;
public ViewOperation(int tag) {
mTag = tag;
}
}
private final class RemoveRootViewOperation extends ViewOperation {
public RemoveRootViewOperation(int tag) {
super(tag);
}
@Override
public void execute() {
mNativeViewHierarchyManager.removeRootView(mTag);
}
}
private final class UpdatePropertiesOperation extends ViewOperation {
private final ReactStylesDiffMap mProps;
private UpdatePropertiesOperation(int tag, ReactStylesDiffMap props) {
super(tag);
mProps = props;
}
@Override
public void execute() {
mNativeViewHierarchyManager.updateProperties(mTag, mProps);
}
}
private final class EmitOnLayoutEventOperation extends ViewOperation {
private final int mScreenX;
private final int mScreenY;
private final int mScreenWidth;
private final int mScreenHeight;
public EmitOnLayoutEventOperation(
int tag, int screenX, int screenY, int screenWidth, int screenHeight) {
super(tag);
mScreenX = screenX;
mScreenY = screenY;
mScreenWidth = screenWidth;
mScreenHeight = screenHeight;
}
@Override
public void execute() {
UIManagerModule uiManager = mReactApplicationContext.getNativeModule(UIManagerModule.class);
if (uiManager != null) {
uiManager
.getEventDispatcher()
.dispatchEvent(
OnLayoutEvent.obtain(
-1 /* SurfaceId not used in classic renderer */,
mTag,
mScreenX,
mScreenY,
mScreenWidth,
mScreenHeight));
}
}
}
private final class UpdateInstanceHandleOperation extends ViewOperation {
private final long mInstanceHandle;
private UpdateInstanceHandleOperation(int tag, long instanceHandle) {
super(tag);
mInstanceHandle = instanceHandle;
}
@Override
public void execute() {
mNativeViewHierarchyManager.updateInstanceHandle(mTag, mInstanceHandle);
}
}
/**
* Operation for updating native view's position and size. The operation is not created directly
* by a {@link UIManagerModule} call from JS. Instead it gets inflated using computed position and
* size values by CSSNodeDEPRECATED hierarchy.
*/
private final class UpdateLayoutOperation extends ViewOperation {
private final int mParentTag, mX, mY, mWidth, mHeight;
public UpdateLayoutOperation(int parentTag, int tag, int x, int y, int width, int height) {
super(tag);
mParentTag = parentTag;
mX = x;
mY = y;
mWidth = width;
mHeight = height;
Systrace.startAsyncFlow(Systrace.TRACE_TAG_REACT_VIEW, "updateLayout", mTag);
}
@Override
public void execute() {
Systrace.endAsyncFlow(Systrace.TRACE_TAG_REACT_VIEW, "updateLayout", mTag);
mNativeViewHierarchyManager.updateLayout(mParentTag, mTag, mX, mY, mWidth, mHeight);
}
}
private final class CreateViewOperation extends ViewOperation {
private final ThemedReactContext mThemedContext;
private final String mClassName;
private final @Nullable ReactStylesDiffMap mInitialProps;
public CreateViewOperation(
ThemedReactContext themedContext,
int tag,
String className,
@Nullable ReactStylesDiffMap initialProps) {
super(tag);
mThemedContext = themedContext;
mClassName = className;
mInitialProps = initialProps;
Systrace.startAsyncFlow(Systrace.TRACE_TAG_REACT_VIEW, "createView", mTag);
}
@Override
public void execute() {
Systrace.endAsyncFlow(Systrace.TRACE_TAG_REACT_VIEW, "createView", mTag);
mNativeViewHierarchyManager.createView(mThemedContext, mTag, mClassName, mInitialProps);
}
}
private final class ManageChildrenOperation extends ViewOperation {
private final @Nullable int[] mIndicesToRemove;
private final @Nullable ViewAtIndex[] mViewsToAdd;
private final @Nullable int[] mTagsToDelete;
public ManageChildrenOperation(
int tag,
@Nullable int[] indicesToRemove,
@Nullable ViewAtIndex[] viewsToAdd,
@Nullable int[] tagsToDelete) {
super(tag);
mIndicesToRemove = indicesToRemove;
mViewsToAdd = viewsToAdd;
mTagsToDelete = tagsToDelete;
}
@Override
public void execute() {
mNativeViewHierarchyManager.manageChildren(
mTag, mIndicesToRemove, mViewsToAdd, mTagsToDelete);
}
}
private final class SetChildrenOperation extends ViewOperation {
private final ReadableArray mChildrenTags;
public SetChildrenOperation(int tag, ReadableArray childrenTags) {
super(tag);
mChildrenTags = childrenTags;
}
@Override
public void execute() {
mNativeViewHierarchyManager.setChildren(mTag, mChildrenTags);
}
}
private final class UpdateViewExtraData extends ViewOperation {
private final Object mExtraData;
public UpdateViewExtraData(int tag, Object extraData) {
super(tag);
mExtraData = extraData;
}
@Override
public void execute() {
mNativeViewHierarchyManager.updateViewExtraData(mTag, mExtraData);
}
}
private final class ChangeJSResponderOperation extends ViewOperation {
private final int mInitialTag;
private final boolean mBlockNativeResponder;
private final boolean mClearResponder;
public ChangeJSResponderOperation(
int tag, int initialTag, boolean clearResponder, boolean blockNativeResponder) {
super(tag);
mInitialTag = initialTag;
mClearResponder = clearResponder;
mBlockNativeResponder = blockNativeResponder;
}
@Override
public void execute() {
if (!mClearResponder) {
mNativeViewHierarchyManager.setJSResponder(mTag, mInitialTag, mBlockNativeResponder);
} else {
mNativeViewHierarchyManager.clearJSResponder();
}
}
}
/**
* This is a common interface for View Command operations. Once we delete the deprecated {@link
* DispatchCommandOperation}, we can delete this interface too. It provides a set of common
* operations to simplify generic operations on all types of ViewCommands.
*/
private interface DispatchCommandViewOperation {
/**
* Like the execute function, but throws real exceptions instead of logging soft errors and
* returning silently.
*/
void executeWithExceptions();
/** Increment retry counter. */
void incrementRetries();
/** Get retry counter. */
int getRetries();
}
@Deprecated
private final class DispatchCommandOperation extends ViewOperation
implements DispatchCommandViewOperation {
private final int mCommand;
private final @Nullable ReadableArray mArgs;
private int numRetries = 0;
public DispatchCommandOperation(int tag, int command, @Nullable ReadableArray args) {
super(tag);
mCommand = command;
mArgs = args;
}
@Override
public void execute() {
try {
mNativeViewHierarchyManager.dispatchCommand(mTag, mCommand, mArgs);
} catch (Throwable e) {
ReactSoftExceptionLogger.logSoftException(
TAG, new RuntimeException("Error dispatching View Command", e));
}
}
@Override
public void executeWithExceptions() {
mNativeViewHierarchyManager.dispatchCommand(mTag, mCommand, mArgs);
}
@Override
@UiThread
public void incrementRetries() {
numRetries++;
}
@Override
@UiThread
public int getRetries() {
return numRetries;
}
}
private final class DispatchStringCommandOperation extends ViewOperation
implements DispatchCommandViewOperation {
private final String mCommand;
private final @Nullable ReadableArray mArgs;
private int numRetries = 0;
public DispatchStringCommandOperation(int tag, String command, @Nullable ReadableArray args) {
super(tag);
mCommand = command;
mArgs = args;
}
@Override
public void execute() {
try {
mNativeViewHierarchyManager.dispatchCommand(mTag, mCommand, mArgs);
} catch (Throwable e) {
ReactSoftExceptionLogger.logSoftException(
TAG, new RuntimeException("Error dispatching View Command", e));
}
}
@Override
@UiThread
public void executeWithExceptions() {
mNativeViewHierarchyManager.dispatchCommand(mTag, mCommand, mArgs);
}
@Override
@UiThread
public void incrementRetries() {
numRetries++;
}
@Override
public int getRetries() {
return numRetries;
}
}
private final class ShowPopupMenuOperation extends ViewOperation {
private final ReadableArray mItems;
private final Callback mError;
private final Callback mSuccess;
public ShowPopupMenuOperation(int tag, ReadableArray items, Callback error, Callback success) {
super(tag);
mItems = items;
mError = error;
mSuccess = success;
}
@Override
public void execute() {
mNativeViewHierarchyManager.showPopupMenu(mTag, mItems, mSuccess, mError);
}
}
private final class DismissPopupMenuOperation implements UIOperation {
@Override
public void execute() {
mNativeViewHierarchyManager.dismissPopupMenu();
}
}
/** A spec for animation operations (add/remove) */
private abstract static class AnimationOperation implements UIViewOperationQueue.UIOperation {
protected final int mAnimationID;
public AnimationOperation(int animationID) {
mAnimationID = animationID;
}
}
private class SetLayoutAnimationEnabledOperation implements UIOperation {
private final boolean mEnabled;
private SetLayoutAnimationEnabledOperation(final boolean enabled) {
mEnabled = enabled;
}
@Override
public void execute() {
mNativeViewHierarchyManager.setLayoutAnimationEnabled(mEnabled);
}
}
private class ConfigureLayoutAnimationOperation implements UIOperation {
private final ReadableMap mConfig;
private final Callback mAnimationComplete;
private ConfigureLayoutAnimationOperation(
final ReadableMap config, final Callback animationComplete) {
mConfig = config;
mAnimationComplete = animationComplete;
}
@Override
public void execute() {
mNativeViewHierarchyManager.configureLayoutAnimation(mConfig, mAnimationComplete);
}
}
private final class MeasureOperation implements UIOperation {
private final int mReactTag;
private final Callback mCallback;
private MeasureOperation(final int reactTag, final Callback callback) {
super();
mReactTag = reactTag;
mCallback = callback;
}
@Override
public void execute() {
try {
mNativeViewHierarchyManager.measure(mReactTag, mMeasureBuffer);
} catch (NoSuchNativeViewException e) {
// Invoke with no args to signal failure and to allow JS to clean up the callback
// handle.
mCallback.invoke();
return;
}
float x = PixelUtil.toDIPFromPixel(mMeasureBuffer[0]);
float y = PixelUtil.toDIPFromPixel(mMeasureBuffer[1]);
float width = PixelUtil.toDIPFromPixel(mMeasureBuffer[2]);
float height = PixelUtil.toDIPFromPixel(mMeasureBuffer[3]);
mCallback.invoke(0, 0, width, height, x, y);
}
}
private final class MeasureInWindowOperation implements UIOperation {
private final int mReactTag;
private final Callback mCallback;
private MeasureInWindowOperation(final int reactTag, final Callback callback) {
super();
mReactTag = reactTag;
mCallback = callback;
}
@Override
public void execute() {
try {
mNativeViewHierarchyManager.measureInWindow(mReactTag, mMeasureBuffer);
} catch (NoSuchNativeViewException e) {
// Invoke with no args to signal failure and to allow JS to clean up the callback
// handle.
mCallback.invoke();
return;
}
float x = PixelUtil.toDIPFromPixel(mMeasureBuffer[0]);
float y = PixelUtil.toDIPFromPixel(mMeasureBuffer[1]);
float width = PixelUtil.toDIPFromPixel(mMeasureBuffer[2]);
float height = PixelUtil.toDIPFromPixel(mMeasureBuffer[3]);
mCallback.invoke(x, y, width, height);
}
}
private final class FindTargetForTouchOperation implements UIOperation {
private final int mReactTag;
private final float mTargetX;
private final float mTargetY;
private final Callback mCallback;
private FindTargetForTouchOperation(
final int reactTag, final float targetX, final float targetY, final Callback callback) {
super();
mReactTag = reactTag;
mTargetX = targetX;
mTargetY = targetY;
mCallback = callback;
}
@Override
public void execute() {
try {
mNativeViewHierarchyManager.measure(mReactTag, mMeasureBuffer);
} catch (IllegalViewOperationException e) {
mCallback.invoke();
return;
}
// Because React coordinates are relative to root container, and measure() operates
// on screen coordinates, we need to offset values using root container location.
final float containerX = (float) mMeasureBuffer[0];
final float containerY = (float) mMeasureBuffer[1];
final int touchTargetReactTag =
mNativeViewHierarchyManager.findTargetTagForTouch(mReactTag, mTargetX, mTargetY);
try {
mNativeViewHierarchyManager.measure(touchTargetReactTag, mMeasureBuffer);
} catch (IllegalViewOperationException e) {
mCallback.invoke();
return;
}
float x = PixelUtil.toDIPFromPixel(mMeasureBuffer[0] - containerX);
float y = PixelUtil.toDIPFromPixel(mMeasureBuffer[1] - containerY);
float width = PixelUtil.toDIPFromPixel(mMeasureBuffer[2]);
float height = PixelUtil.toDIPFromPixel(mMeasureBuffer[3]);
mCallback.invoke(touchTargetReactTag, x, y, width, height);
}
}
private final class LayoutUpdateFinishedOperation implements UIOperation {
private final ReactShadowNode mNode;
private final UIImplementation.LayoutUpdateListener mListener;
private LayoutUpdateFinishedOperation(
ReactShadowNode node, UIImplementation.LayoutUpdateListener listener) {
mNode = node;
mListener = listener;
}
@Override
public void execute() {
mListener.onLayoutUpdated(mNode);
}
}
private class UIBlockOperation implements UIOperation {
private final UIBlock mBlock;
public UIBlockOperation(UIBlock block) {
mBlock = block;
}
@Override
public void execute() {
mBlock.execute(mNativeViewHierarchyManager);
}
}
private final class SendAccessibilityEvent extends ViewOperation {
private final int mEventType;
private SendAccessibilityEvent(int tag, int eventType) {
super(tag);
mEventType = eventType;
}
@Override
public void execute() {
mNativeViewHierarchyManager.sendAccessibilityEvent(mTag, mEventType);
}
}
private final NativeViewHierarchyManager mNativeViewHierarchyManager;
private final Object mDispatchRunnablesLock = new Object();
private final Object mNonBatchedOperationsLock = new Object();
private final DispatchUIFrameCallback mDispatchUIFrameCallback;
private final ReactApplicationContext mReactApplicationContext;
private ArrayList ViewRootImpl#scheduleTraversals (which is called from invalidate, requestLayout, etc) calls
* Looper#postSyncBarrier which keeps any UI thread looper messages from being processed until
* that barrier is removed during the next traversal. That means, depending on when we get updates
* from JS and what else is happening on the UI thread, we can sometimes try to post this runnable
* after ViewRootImpl has posted a barrier.
*
* Using a Choreographer callback (which runs immediately before traversals), we guarantee we
* run before the next traversal.
*/
private class DispatchUIFrameCallback extends GuardedFrameCallback {
private static final int FRAME_TIME_MS = 16;
private final int mMinTimeLeftInFrameForNonBatchedOperationMs;
private DispatchUIFrameCallback(
ReactContext reactContext, int minTimeLeftInFrameForNonBatchedOperationMs) {
super(reactContext);
mMinTimeLeftInFrameForNonBatchedOperationMs = minTimeLeftInFrameForNonBatchedOperationMs;
}
@Override
public void doFrameGuarded(long frameTimeNanos) {
if (mIsInIllegalUIState) {
FLog.w(
ReactConstants.TAG,
"Not flushing pending UI operations because of previously thrown Exception");
return;
}
Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "dispatchNonBatchedUIOperations");
try {
dispatchPendingNonBatchedOperations(frameTimeNanos);
} finally {
Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
}
flushPendingBatches();
ReactChoreographer.getInstance()
.postFrameCallback(ReactChoreographer.CallbackType.DISPATCH_UI, this);
}
private void dispatchPendingNonBatchedOperations(long frameTimeNanos) {
while (true) {
long timeLeftInFrame = FRAME_TIME_MS - ((System.nanoTime() - frameTimeNanos) / 1000000);
if (timeLeftInFrame < mMinTimeLeftInFrameForNonBatchedOperationMs) {
break;
}
UIOperation nextOperation;
synchronized (mNonBatchedOperationsLock) {
if (mNonBatchedOperations.isEmpty()) {
break;
}
nextOperation = mNonBatchedOperations.pollFirst();
}
try {
long nonBatchedExecutionStartTime = SystemClock.uptimeMillis();
nextOperation.execute();
mNonBatchedExecutionTotalTime +=
SystemClock.uptimeMillis() - nonBatchedExecutionStartTime;
} catch (Exception e) {
mIsInIllegalUIState = true;
throw e;
}
}
}
}
}