Files
react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java
T
Ramanpreet Nara 42dcfab2a9 Stop requiring TurboModuleManager JSIModule on CatalystInstance cleanup
Summary:
In `CatalystInstanceImpl.destroy()`, we require the TurboModuleManager using the [following lines](https://fburl.com/diffusion/a4y6wbft):

```
final JSIModule turboModuleManager =
    ReactFeatureFlags.useTurboModules
        ? mJSIModuleRegistry.getModule(JSIModuleType.TurboModuleManager)
        : null;
```

For some strange reason, even though `ReactFeatureFlags.useTurboModules` is true, the TurboModuleManager isn't registered with mJSIModuleRegistry. I spent some time looking through the code, but I couldn't figure out why. These lines actually aren't necessary, so it's possible to fix the issue by simply working around it, which is what this diff does. We shouldn't have been double requiring the TurboModuleManager anyways, since `CatalystInstance.java` has a method to set the TurboModuleManager, which we call in `ReactInstanceManager.createReactContext`.

## Alternative approach
I could push this diff to the next cut, and instead land a diff that adds debug information to the native crash. At the cost of a week, it may help us figure out why we're seeing the crash. Thoughts? cc fkgozali

Reviewed By: fkgozali

Differential Revision: D17636604

fbshipit-source-id: ecfff593dc6eb4ec4d5e331348b308bc7ab37966
2019-09-27 18:20:28 -07:00

682 lines
24 KiB
Java

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* <p>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.bridge;
import static com.facebook.systrace.Systrace.TRACE_TAG_REACT_JAVA_BRIDGE;
import android.content.res.AssetManager;
import android.os.AsyncTask;
import android.util.Log;
import androidx.annotation.Nullable;
import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.facebook.jni.HybridData;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.bridge.queue.MessageQueueThread;
import com.facebook.react.bridge.queue.QueueThreadExceptionHandler;
import com.facebook.react.bridge.queue.ReactQueueConfiguration;
import com.facebook.react.bridge.queue.ReactQueueConfigurationImpl;
import com.facebook.react.bridge.queue.ReactQueueConfigurationSpec;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.turbomodule.core.CallInvokerHolderImpl;
import com.facebook.react.turbomodule.core.interfaces.TurboModule;
import com.facebook.react.turbomodule.core.interfaces.TurboModuleRegistry;
import com.facebook.systrace.Systrace;
import com.facebook.systrace.TraceListener;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
/**
* This provides an implementation of the public CatalystInstance instance. It is public because it
* is built by XReactInstanceManager which is in a different package.
*/
@DoNotStrip
public class CatalystInstanceImpl implements CatalystInstance {
static {
ReactBridge.staticInit();
}
private static final AtomicInteger sNextInstanceIdForTrace = new AtomicInteger(1);
public static class PendingJSCall {
public String mModule;
public String mMethod;
public @Nullable NativeArray mArguments;
public PendingJSCall(String module, String method, @Nullable NativeArray arguments) {
mModule = module;
mMethod = method;
mArguments = arguments;
}
void call(CatalystInstanceImpl catalystInstance) {
NativeArray arguments = mArguments != null ? mArguments : new WritableNativeArray();
catalystInstance.jniCallJSFunction(mModule, mMethod, arguments);
}
public String toString() {
return mModule
+ "."
+ mMethod
+ "("
+ (mArguments == null ? "" : mArguments.toString())
+ ")";
}
}
// Access from any thread
private final ReactQueueConfigurationImpl mReactQueueConfiguration;
private final CopyOnWriteArrayList<NotThreadSafeBridgeIdleDebugListener> mBridgeIdleListeners;
private final AtomicInteger mPendingJSCalls = new AtomicInteger(0);
private final String mJsPendingCallsTitleForTrace =
"pending_js_calls_instance" + sNextInstanceIdForTrace.getAndIncrement();
private volatile boolean mDestroyed = false;
private final TraceListener mTraceListener;
private final JavaScriptModuleRegistry mJSModuleRegistry;
private final JSBundleLoader mJSBundleLoader;
private final ArrayList<PendingJSCall> mJSCallsPendingInit = new ArrayList<PendingJSCall>();
private final Object mJSCallsPendingInitLock = new Object();
private final NativeModuleRegistry mNativeModuleRegistry;
private final JSIModuleRegistry mJSIModuleRegistry = new JSIModuleRegistry();
private final NativeModuleCallExceptionHandler mNativeModuleCallExceptionHandler;
private final MessageQueueThread mNativeModulesQueueThread;
private boolean mInitialized = false;
private volatile boolean mAcceptCalls = false;
private boolean mJSBundleHasLoaded;
private @Nullable String mSourceURL;
private JavaScriptContextHolder mJavaScriptContextHolder;
private @Nullable TurboModuleRegistry mTurboModuleRegistry = null;
private @Nullable JSIModule mTurboModuleManagerJSIModule = null;
// C++ parts
private final HybridData mHybridData;
private static native HybridData initHybrid();
public native CallInvokerHolderImpl getJSCallInvokerHolder();
public native CallInvokerHolderImpl getNativeCallInvokerHolder();
private CatalystInstanceImpl(
final ReactQueueConfigurationSpec reactQueueConfigurationSpec,
final JavaScriptExecutor jsExecutor,
final NativeModuleRegistry nativeModuleRegistry,
final JSBundleLoader jsBundleLoader,
NativeModuleCallExceptionHandler nativeModuleCallExceptionHandler) {
Log.d(ReactConstants.TAG, "Initializing React Xplat Bridge.");
Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "createCatalystInstanceImpl");
mHybridData = initHybrid();
mReactQueueConfiguration =
ReactQueueConfigurationImpl.create(
reactQueueConfigurationSpec, new NativeExceptionHandler());
mBridgeIdleListeners = new CopyOnWriteArrayList<>();
mNativeModuleRegistry = nativeModuleRegistry;
mJSModuleRegistry = new JavaScriptModuleRegistry();
mJSBundleLoader = jsBundleLoader;
mNativeModuleCallExceptionHandler = nativeModuleCallExceptionHandler;
mNativeModulesQueueThread = mReactQueueConfiguration.getNativeModulesQueueThread();
mTraceListener = new JSProfilerTraceListener(this);
Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
Log.d(ReactConstants.TAG, "Initializing React Xplat Bridge before initializeBridge");
Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "initializeCxxBridge");
initializeBridge(
new BridgeCallback(this),
jsExecutor,
mReactQueueConfiguration.getJSQueueThread(),
mNativeModulesQueueThread,
mNativeModuleRegistry.getJavaModules(this),
mNativeModuleRegistry.getCxxModules());
Log.d(ReactConstants.TAG, "Initializing React Xplat Bridge after initializeBridge");
Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
mJavaScriptContextHolder = new JavaScriptContextHolder(getJavaScriptContext());
}
private static class BridgeCallback implements ReactCallback {
// We do this so the callback doesn't keep the CatalystInstanceImpl alive.
// In this case, the callback is held in C++ code, so the GC can't see it
// and determine there's an inaccessible cycle.
private final WeakReference<CatalystInstanceImpl> mOuter;
BridgeCallback(CatalystInstanceImpl outer) {
mOuter = new WeakReference<>(outer);
}
@Override
public void onBatchComplete() {
CatalystInstanceImpl impl = mOuter.get();
if (impl != null) {
impl.mNativeModuleRegistry.onBatchComplete();
}
}
@Override
public void incrementPendingJSCalls() {
CatalystInstanceImpl impl = mOuter.get();
if (impl != null) {
impl.incrementPendingJSCalls();
}
}
@Override
public void decrementPendingJSCalls() {
CatalystInstanceImpl impl = mOuter.get();
if (impl != null) {
impl.decrementPendingJSCalls();
}
}
}
/**
* This method and the native below permits a CatalystInstance to extend the known Native modules.
* This registry contains only the new modules to load. The registry {@code mNativeModuleRegistry}
* updates internally to contain all the new modules, and generates the new registry for
* extracting just the new collections.
*/
@Override
public void extendNativeModules(NativeModuleRegistry modules) {
// Extend the Java-visible registry of modules
mNativeModuleRegistry.registerModules(modules);
Collection<JavaModuleWrapper> javaModules = modules.getJavaModules(this);
Collection<ModuleHolder> cxxModules = modules.getCxxModules();
// Extend the Cxx-visible registry of modules wrapped in appropriate interfaces
jniExtendNativeModules(javaModules, cxxModules);
}
private native void jniExtendNativeModules(
Collection<JavaModuleWrapper> javaModules, Collection<ModuleHolder> cxxModules);
private native void initializeBridge(
ReactCallback callback,
JavaScriptExecutor jsExecutor,
MessageQueueThread jsQueue,
MessageQueueThread moduleQueue,
Collection<JavaModuleWrapper> javaModules,
Collection<ModuleHolder> cxxModules);
@Override
public void setSourceURLs(String deviceURL, String remoteURL) {
mSourceURL = deviceURL;
jniSetSourceURL(remoteURL);
}
@Override
public void registerSegment(int segmentId, String path) {
jniRegisterSegment(segmentId, path);
}
@Override
public void loadScriptFromAssets(
AssetManager assetManager, String assetURL, boolean loadSynchronously) {
mSourceURL = assetURL;
jniLoadScriptFromAssets(assetManager, assetURL, loadSynchronously);
}
@Override
public void loadScriptFromFile(String fileName, String sourceURL, boolean loadSynchronously) {
mSourceURL = sourceURL;
jniLoadScriptFromFile(fileName, sourceURL, loadSynchronously);
}
private native void jniSetSourceURL(String sourceURL);
private native void jniRegisterSegment(int segmentId, String path);
private native void jniLoadScriptFromAssets(
AssetManager assetManager, String assetURL, boolean loadSynchronously);
private native void jniLoadScriptFromFile(
String fileName, String sourceURL, boolean loadSynchronously);
@Override
public void runJSBundle() {
Log.d(ReactConstants.TAG, "CatalystInstanceImpl.runJSBundle()");
Assertions.assertCondition(!mJSBundleHasLoaded, "JS bundle was already loaded!");
// incrementPendingJSCalls();
mJSBundleLoader.loadScript(CatalystInstanceImpl.this);
synchronized (mJSCallsPendingInitLock) {
// Loading the bundle is queued on the JS thread, but may not have
// run yet. It's safe to set this here, though, since any work it
// gates will be queued on the JS thread behind the load.
mAcceptCalls = true;
for (PendingJSCall function : mJSCallsPendingInit) {
function.call(this);
}
mJSCallsPendingInit.clear();
mJSBundleHasLoaded = true;
}
// This is registered after JS starts since it makes a JS call
Systrace.registerListener(mTraceListener);
}
@Override
public boolean hasRunJSBundle() {
synchronized (mJSCallsPendingInitLock) {
return mJSBundleHasLoaded && mAcceptCalls;
}
}
@Override
public @Nullable String getSourceURL() {
return mSourceURL;
}
private native void jniCallJSFunction(String module, String method, NativeArray arguments);
@Override
public void callFunction(final String module, final String method, final NativeArray arguments) {
callFunction(new PendingJSCall(module, method, arguments));
}
public void callFunction(PendingJSCall function) {
if (mDestroyed) {
final String call = function.toString();
FLog.w(ReactConstants.TAG, "Calling JS function after bridge has been destroyed: " + call);
return;
}
if (!mAcceptCalls) {
// Most of the time the instance is initialized and we don't need to acquire the lock
synchronized (mJSCallsPendingInitLock) {
if (!mAcceptCalls) {
mJSCallsPendingInit.add(function);
return;
}
}
}
function.call(this);
}
private native void jniCallJSCallback(int callbackID, NativeArray arguments);
@Override
public void invokeCallback(final int callbackID, final NativeArrayInterface arguments) {
if (mDestroyed) {
FLog.w(ReactConstants.TAG, "Invoking JS callback after bridge has been destroyed.");
return;
}
jniCallJSCallback(callbackID, (NativeArray) arguments);
}
/**
* Destroys this catalyst instance, waiting for any other threads in ReactQueueConfiguration
* (besides the UI thread) to finish running. Must be called from the UI thread so that we can
* fully shut down other threads.
*/
@Override
public void destroy() {
Log.d(ReactConstants.TAG, "CatalystInstanceImpl.destroy() start");
UiThreadUtil.assertOnUiThread();
if (mDestroyed) {
return;
}
// TODO: tell all APIs to shut down
ReactMarker.logMarker(ReactMarkerConstants.DESTROY_CATALYST_INSTANCE_START);
mDestroyed = true;
mNativeModulesQueueThread.runOnQueue(
new Runnable() {
@Override
public void run() {
mNativeModuleRegistry.notifyJSInstanceDestroy();
mJSIModuleRegistry.notifyJSInstanceDestroy();
boolean wasIdle = (mPendingJSCalls.getAndSet(0) == 0);
if (!mBridgeIdleListeners.isEmpty()) {
for (NotThreadSafeBridgeIdleDebugListener listener : mBridgeIdleListeners) {
if (!wasIdle) {
listener.onTransitionToBridgeIdle();
}
listener.onBridgeDestroyed();
}
}
getReactQueueConfiguration()
.getJSQueueThread()
.runOnQueue(
new Runnable() {
@Override
public void run() {
// We need to destroy the TurboModuleManager on the JS Thread
if (mTurboModuleManagerJSIModule != null) {
mTurboModuleManagerJSIModule.onCatalystInstanceDestroy();
}
getReactQueueConfiguration()
.getUIQueueThread()
.runOnQueue(
new Runnable() {
@Override
public void run() {
// AsyncTask.execute must be executed from the UI Thread
AsyncTask.execute(
new Runnable() {
@Override
public void run() {
// Kill non-UI threads from neutral third party
// potentially expensive, so don't run on UI thread
// contextHolder is used as a lock to guard against
// other users of the JS VM having the VM destroyed
// underneath them, so notify them before we reset
// Native
mJavaScriptContextHolder.clear();
mHybridData.resetNative();
getReactQueueConfiguration().destroy();
Log.d(
ReactConstants.TAG,
"CatalystInstanceImpl.destroy() end");
ReactMarker.logMarker(
ReactMarkerConstants.DESTROY_CATALYST_INSTANCE_END);
}
});
}
});
}
});
}
});
// This is a noop if the listener was not yet registered.
Systrace.unregisterListener(mTraceListener);
}
@Override
public boolean isDestroyed() {
return mDestroyed;
}
/** Initialize all the native modules */
@VisibleForTesting
@Override
public void initialize() {
Log.d(ReactConstants.TAG, "CatalystInstanceImpl.initialize()");
Assertions.assertCondition(
!mInitialized, "This catalyst instance has already been initialized");
// We assume that the instance manager blocks on running the JS bundle. If
// that changes, then we need to set mAcceptCalls just after posting the
// task that will run the js bundle.
Assertions.assertCondition(mAcceptCalls, "RunJSBundle hasn't completed.");
mInitialized = true;
mNativeModulesQueueThread.runOnQueue(
new Runnable() {
@Override
public void run() {
mNativeModuleRegistry.notifyJSInstanceInitialized();
}
});
}
@Override
public ReactQueueConfiguration getReactQueueConfiguration() {
return mReactQueueConfiguration;
}
@Override
public <T extends JavaScriptModule> T getJSModule(Class<T> jsInterface) {
return mJSModuleRegistry.getJavaScriptModule(this, jsInterface);
}
@Override
public <T extends NativeModule> boolean hasNativeModule(Class<T> nativeModuleInterface) {
String moduleName = getNameFromAnnotation(nativeModuleInterface);
return mTurboModuleRegistry != null && mTurboModuleRegistry.hasModule(moduleName)
? true
: mNativeModuleRegistry.hasModule(moduleName);
}
@Override
public <T extends NativeModule> T getNativeModule(Class<T> nativeModuleInterface) {
return (T) getNativeModule(getNameFromAnnotation(nativeModuleInterface));
}
@Override
public NativeModule getNativeModule(String moduleName) {
if (mTurboModuleRegistry != null) {
TurboModule turboModule = mTurboModuleRegistry.getModule(moduleName);
if (turboModule != null) {
return (NativeModule) turboModule;
}
}
return mNativeModuleRegistry.getModule(moduleName);
}
private <T extends NativeModule> String getNameFromAnnotation(Class<T> nativeModuleInterface) {
ReactModule annotation = nativeModuleInterface.getAnnotation(ReactModule.class);
if (annotation == null) {
throw new IllegalArgumentException(
"Could not find @ReactModule annotation in " + nativeModuleInterface.getCanonicalName());
}
return annotation.name();
}
// This is only used by com.facebook.react.modules.common.ModuleDataCleaner
@Override
public Collection<NativeModule> getNativeModules() {
Collection<NativeModule> nativeModules = new ArrayList<>();
nativeModules.addAll(mNativeModuleRegistry.getAllModules());
if (mTurboModuleRegistry != null) {
for (TurboModule turboModule : mTurboModuleRegistry.getModules()) {
nativeModules.add((NativeModule) turboModule);
}
}
return nativeModules;
}
private native void jniHandleMemoryPressure(int level);
@Override
public void handleMemoryPressure(int level) {
if (mDestroyed) {
return;
}
jniHandleMemoryPressure(level);
}
/**
* Adds a idle listener for this Catalyst instance. The listener will receive notifications
* whenever the bridge transitions from idle to busy and vice-versa, where the busy state is
* defined as there being some non-zero number of calls to JS that haven't resolved via a
* onBatchComplete call. The listener should be purely passive and not affect application logic.
*/
@Override
public void addBridgeIdleDebugListener(NotThreadSafeBridgeIdleDebugListener listener) {
mBridgeIdleListeners.add(listener);
}
/**
* Removes a NotThreadSafeBridgeIdleDebugListener previously added with {@link
* #addBridgeIdleDebugListener}
*/
@Override
public void removeBridgeIdleDebugListener(NotThreadSafeBridgeIdleDebugListener listener) {
mBridgeIdleListeners.remove(listener);
}
@Override
public native void setGlobalVariable(String propName, String jsonValue);
@Override
public JavaScriptContextHolder getJavaScriptContextHolder() {
return mJavaScriptContextHolder;
}
@Override
public void addJSIModules(List<JSIModuleSpec> jsiModules) {
mJSIModuleRegistry.registerModules(jsiModules);
}
@Override
public JSIModule getJSIModule(JSIModuleType moduleType) {
return mJSIModuleRegistry.getModule(moduleType);
}
private native long getJavaScriptContext();
private void incrementPendingJSCalls() {
int oldPendingCalls = mPendingJSCalls.getAndIncrement();
boolean wasIdle = oldPendingCalls == 0;
Systrace.traceCounter(
Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, mJsPendingCallsTitleForTrace, oldPendingCalls + 1);
if (wasIdle && !mBridgeIdleListeners.isEmpty()) {
mNativeModulesQueueThread.runOnQueue(
new Runnable() {
@Override
public void run() {
for (NotThreadSafeBridgeIdleDebugListener listener : mBridgeIdleListeners) {
listener.onTransitionToBridgeBusy();
}
}
});
}
}
public void setTurboModuleManager(JSIModule module) {
mTurboModuleRegistry = (TurboModuleRegistry) module;
mTurboModuleManagerJSIModule = module;
}
private void decrementPendingJSCalls() {
int newPendingCalls = mPendingJSCalls.decrementAndGet();
// TODO(9604406): handle case of web workers injecting messages to main thread
// Assertions.assertCondition(newPendingCalls >= 0);
boolean isNowIdle = newPendingCalls == 0;
Systrace.traceCounter(
Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, mJsPendingCallsTitleForTrace, newPendingCalls);
if (isNowIdle && !mBridgeIdleListeners.isEmpty()) {
mNativeModulesQueueThread.runOnQueue(
new Runnable() {
@Override
public void run() {
for (NotThreadSafeBridgeIdleDebugListener listener : mBridgeIdleListeners) {
listener.onTransitionToBridgeIdle();
}
}
});
}
}
private void onNativeException(Exception e) {
mNativeModuleCallExceptionHandler.handleException(e);
mReactQueueConfiguration
.getUIQueueThread()
.runOnQueue(
new Runnable() {
@Override
public void run() {
destroy();
}
});
}
private class NativeExceptionHandler implements QueueThreadExceptionHandler {
@Override
public void handleException(Exception e) {
// Any Exception caught here is because of something in JS. Even if it's a bug in the
// framework/native code, it was triggered by JS and theoretically since we were able
// to set up the bridge, JS could change its logic, reload, and not trigger that crash.
onNativeException(e);
}
}
private static class JSProfilerTraceListener implements TraceListener {
// We do this so the callback doesn't keep the CatalystInstanceImpl alive.
// In this case, Systrace will keep the registered listener around forever
// if the CatalystInstanceImpl is not explicitly destroyed. These instances
// can still leak, but they are at least small.
private final WeakReference<CatalystInstanceImpl> mOuter;
public JSProfilerTraceListener(CatalystInstanceImpl outer) {
mOuter = new WeakReference<CatalystInstanceImpl>(outer);
}
@Override
public void onTraceStarted() {
CatalystInstanceImpl impl = mOuter.get();
if (impl != null) {
impl.getJSModule(com.facebook.react.bridge.Systrace.class).setEnabled(true);
}
}
@Override
public void onTraceStopped() {
CatalystInstanceImpl impl = mOuter.get();
if (impl != null) {
impl.getJSModule(com.facebook.react.bridge.Systrace.class).setEnabled(false);
}
}
}
public static class Builder {
private @Nullable ReactQueueConfigurationSpec mReactQueueConfigurationSpec;
private @Nullable JSBundleLoader mJSBundleLoader;
private @Nullable NativeModuleRegistry mRegistry;
private @Nullable JavaScriptExecutor mJSExecutor;
private @Nullable NativeModuleCallExceptionHandler mNativeModuleCallExceptionHandler;
public Builder setReactQueueConfigurationSpec(
ReactQueueConfigurationSpec ReactQueueConfigurationSpec) {
mReactQueueConfigurationSpec = ReactQueueConfigurationSpec;
return this;
}
public Builder setRegistry(NativeModuleRegistry registry) {
mRegistry = registry;
return this;
}
public Builder setJSBundleLoader(JSBundleLoader jsBundleLoader) {
mJSBundleLoader = jsBundleLoader;
return this;
}
public Builder setJSExecutor(JavaScriptExecutor jsExecutor) {
mJSExecutor = jsExecutor;
return this;
}
public Builder setNativeModuleCallExceptionHandler(NativeModuleCallExceptionHandler handler) {
mNativeModuleCallExceptionHandler = handler;
return this;
}
public CatalystInstanceImpl build() {
return new CatalystInstanceImpl(
Assertions.assertNotNull(mReactQueueConfigurationSpec),
Assertions.assertNotNull(mJSExecutor),
Assertions.assertNotNull(mRegistry),
Assertions.assertNotNull(mJSBundleLoader),
Assertions.assertNotNull(mNativeModuleCallExceptionHandler));
}
}
}