NativeAnimatedDriver: synchronize animation lifecycle closer to Fabric or Paper lifecycle

Summary:
Switch between "Fabric" and "Non-Fabric" modes based on which types of native Views are being attached to animations. Don't allow non-Fabric to drive Fabric animations and vice-versa.

Changelog: [Internal]

Reviewed By: mdvacca

Differential Revision: D21985411

fbshipit-source-id: fb9bef1e38375b384430b4e0275e7b6d62eda7a4
This commit is contained in:
Joshua Gross
2020-06-11 20:46:48 -07:00
committed by Facebook GitHub Bot
parent 3346ac7f96
commit 472cf3f4ad
7 changed files with 447 additions and 118 deletions
@@ -10,16 +10,19 @@ package com.facebook.react.animated;
import android.util.SparseArray;
import androidx.annotation.Nullable;
import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.JSApplicationCausedNativeException;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.UIManager;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.uimanager.IllegalViewOperationException;
import com.facebook.react.uimanager.UIManagerHelper;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.events.Event;
import com.facebook.react.uimanager.events.EventDispatcher;
@@ -48,6 +51,9 @@ import java.util.Queue;
*/
/*package*/ class NativeAnimatedNodesManager implements EventDispatcherListener {
private static final String TAG = "NativeAnimatedNodesManager";
private static final int MAX_INCONSISTENT_FRAMES = 64;
private final SparseArray<AnimatedNode> mAnimatedNodes = new SparseArray<>();
private final SparseArray<AnimationDriver> mActiveAnimations = new SparseArray<>();
private final SparseArray<AnimatedNode> mUpdatedNodes = new SparseArray<>();
@@ -55,14 +61,19 @@ import java.util.Queue;
// there will be only one driver per mapping so all code code should be optimized around that.
private final Map<String, List<EventAnimationDriver>> mEventDrivers = new HashMap<>();
private final UIManagerModule.CustomEventNamesResolver mCustomEventNamesResolver;
private final UIManager mUIManager;
private final ReactApplicationContext mReactApplicationContext;
private int mAnimatedGraphBFSColor = 0;
private int mNumInconsistentFrames = 0;
// Used to avoid allocating a new array on every frame in `runUpdates` and `onEventDispatch`.
private final List<AnimatedNode> mRunUpdateNodeList = new LinkedList<>();
public NativeAnimatedNodesManager(UIManagerModule uiManager) {
mUIManager = uiManager;
mUIManager.<EventDispatcher>getEventDispatcher().addListener(this);
public NativeAnimatedNodesManager(ReactApplicationContext reactApplicationContext) {
mReactApplicationContext = reactApplicationContext;
UIManagerModule uiManager =
Assertions.assertNotNull(reactApplicationContext.getNativeModule(UIManagerModule.class));
uiManager.<EventDispatcher>getEventDispatcher().addListener(this);
// TODO T64216139 Remove dependency of UIManagerModule when the Constants are not in Native
// anymore
mCustomEventNamesResolver = uiManager.getDirectEventNamesResolver();
@@ -89,7 +100,7 @@ import java.util.Queue;
} else if ("value".equals(type)) {
node = new ValueAnimatedNode(config);
} else if ("props".equals(type)) {
node = new PropsAnimatedNode(config, this, mUIManager);
node = new PropsAnimatedNode(config, this);
} else if ("interpolation".equals(type)) {
node = new InterpolationAnimatedNode(config);
} else if ("addition".equals(type)) {
@@ -301,8 +312,21 @@ import java.util.Queue;
+ "of type "
+ PropsAnimatedNode.class.getName());
}
if (mReactApplicationContext == null) {
throw new IllegalStateException(
"Animated node could not be connected, no ReactApplicationContext: " + viewTag);
}
@Nullable
UIManager uiManager =
UIManagerHelper.getUIManagerForReactTag(mReactApplicationContext, viewTag);
if (uiManager == null) {
throw new IllegalStateException(
"Animated node could not be connected to UIManager: " + viewTag);
}
PropsAnimatedNode propsAnimatedNode = (PropsAnimatedNode) node;
propsAnimatedNode.connectToView(viewTag);
propsAnimatedNode.connectToView(viewTag, uiManager);
mUpdatedNodes.put(animatedNodeTag, node);
}
@@ -543,26 +567,43 @@ import java.util.Queue;
}
// Run main "update" loop
boolean errorsCaught = false;
while (!nodesQueue.isEmpty()) {
AnimatedNode nextNode = nodesQueue.poll();
nextNode.update();
if (nextNode instanceof PropsAnimatedNode) {
// Send property updates to native view manager
try {
try {
nextNode.update();
if (nextNode instanceof PropsAnimatedNode) {
// Send property updates to native view manager
((PropsAnimatedNode) nextNode).updateView();
} catch (IllegalViewOperationException e) {
// An exception is thrown if the view hasn't been created yet. This can happen because
// views are
// created in batches. If this particular view didn't make it into a batch yet, the view
// won't
// exist and an exception will be thrown when attempting to start an animation on it.
//
// Eat the exception rather than crashing. The impact is that we may drop one or more
// frames of the
// animation.
}
} catch (IllegalViewOperationException e) {
// An exception is thrown if the view hasn't been created yet. This can happen because
// views are
// created in batches. If this particular view didn't make it into a batch yet, the view
// won't
// exist and an exception will be thrown when attempting to start an animation on it.
//
// Eat the exception rather than crashing. The impact is that we may drop one or more
// frames of the
// animation.
FLog.e(TAG, "Native animation workaround, frame lost as result of race condition", e);
} catch (JSApplicationCausedNativeException e) {
// In Fabric there can be race conditions between the JS thread setting up or tearing down
// animated nodes, and Fabric executing them on the UI thread, leading to temporary
// inconsistent
// states. We require that the inconsistency last for N frames before throwing these
// exceptions.
if (!errorsCaught) {
errorsCaught = true;
mNumInconsistentFrames++;
}
if (mNumInconsistentFrames > MAX_INCONSISTENT_FRAMES) {
throw new IllegalStateException(e);
} else {
FLog.e(
ReactConstants.TAG,
"Native animation workaround, frame lost as result of race condition",
TAG,
"Swallowing exception due to potential race between JS and UI threads: inconsistent frame counter: "
+ mNumInconsistentFrames,
e);
}
}
@@ -586,13 +627,23 @@ import java.util.Queue;
// Verify that we've visited *all* active nodes. Throw otherwise as this would mean there is a
// cycle in animated node graph. We also take advantage of the fact that all active nodes are
// visited in the step above so that all the nodes properties `mActiveIncomingNodes` are set to
// zero
// zero.
// In Fabric there can be race conditions between the JS thread setting up or tearing down
// animated nodes, and Fabric executing them on the UI thread, leading to temporary inconsistent
// states. We require that the inconsistency last for 64 frames before throwing this exception.
if (activeNodesCount != updatedNodesCount) {
throw new IllegalStateException(
"Looks like animated nodes graph has cycles, there are "
+ activeNodesCount
+ " but toposort visited only "
+ updatedNodesCount);
if (!errorsCaught) {
mNumInconsistentFrames++;
}
if (mNumInconsistentFrames > MAX_INCONSISTENT_FRAMES) {
throw new IllegalStateException(
"Looks like animated nodes graph has cycles, there are "
+ activeNodesCount
+ " but toposort visited only "
+ updatedNodesCount);
}
} else if (!errorsCaught) {
mNumInconsistentFrames = 0;
}
}
}