Files
react-native/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManagerBuilder.java
T
Lulu Wu b6f7689d70 Destroy React Native instance after catching a fatal js error
Summary:
As title, destroy React Native instance after catching a fatal js error to avoid incoming calls into JS.

Changelog:
[Android][Changed] - Rename NativeModuleCallExceptionHandler to JSExceptionHandler for broader usage

Reviewed By: fkgozali

Differential Revision: D37379340

fbshipit-source-id: 465a30bc824a264b45df3e8b0b24edd61c4b571d
2022-06-28 15:24:13 -07:00

405 lines
16 KiB
Java

/*
* Copyright (c) Meta Platforms, Inc. and 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;
import static com.facebook.react.ReactInstanceManager.initializeSoLoaderIfNecessary;
import static com.facebook.react.modules.systeminfo.AndroidInfoHelpers.getFriendlyDeviceName;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import androidx.annotation.Nullable;
import com.facebook.hermes.reactexecutor.HermesExecutor;
import com.facebook.hermes.reactexecutor.HermesExecutorFactory;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.JSBundleLoader;
import com.facebook.react.bridge.JSExceptionHandler;
import com.facebook.react.bridge.JSIModulePackage;
import com.facebook.react.bridge.JavaScriptExecutorFactory;
import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener;
import com.facebook.react.common.LifecycleState;
import com.facebook.react.common.SurfaceDelegateFactory;
import com.facebook.react.devsupport.DefaultDevSupportManagerFactory;
import com.facebook.react.devsupport.DevSupportManagerFactory;
import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener;
import com.facebook.react.devsupport.interfaces.DevSupportManager;
import com.facebook.react.devsupport.interfaces.RedBoxHandler;
import com.facebook.react.jscexecutor.JSCExecutor;
import com.facebook.react.jscexecutor.JSCExecutorFactory;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.packagerconnection.RequestHandler;
import com.facebook.react.uimanager.UIImplementationProvider;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/** Builder class for {@link ReactInstanceManager} */
public class ReactInstanceManagerBuilder {
private final List<ReactPackage> mPackages = new ArrayList<>();
private @Nullable String mJSBundleAssetUrl;
private @Nullable JSBundleLoader mJSBundleLoader;
private @Nullable String mJSMainModulePath;
private @Nullable NotThreadSafeBridgeIdleDebugListener mBridgeIdleDebugListener;
private @Nullable Application mApplication;
private boolean mUseDeveloperSupport;
private @Nullable DevSupportManagerFactory mDevSupportManagerFactory;
private boolean mRequireActivity;
private @Nullable LifecycleState mInitialLifecycleState;
private @Nullable UIImplementationProvider mUIImplementationProvider;
private @Nullable JSExceptionHandler mJSExceptionHandler;
private @Nullable Activity mCurrentActivity;
private @Nullable DefaultHardwareBackBtnHandler mDefaultHardwareBackBtnHandler;
private @Nullable RedBoxHandler mRedBoxHandler;
private boolean mLazyViewManagersEnabled;
private @Nullable DevBundleDownloadListener mDevBundleDownloadListener;
private @Nullable JavaScriptExecutorFactory mJavaScriptExecutorFactory;
private int mMinNumShakes = 1;
private int mMinTimeLeftInFrameForNonBatchedOperationMs = -1;
private @Nullable JSIModulePackage mJSIModulesPackage;
private @Nullable Map<String, RequestHandler> mCustomPackagerCommandHandlers;
private @Nullable ReactPackageTurboModuleManagerDelegate.Builder mTMMDelegateBuilder;
private @Nullable SurfaceDelegateFactory mSurfaceDelegateFactory;
private JSInterpreter jsInterpreter = JSInterpreter.OLD_LOGIC;
/* package protected */ ReactInstanceManagerBuilder() {}
/** Sets a provider of {@link UIImplementation}. Uses default provider if null is passed. */
public ReactInstanceManagerBuilder setUIImplementationProvider(
@Nullable UIImplementationProvider uiImplementationProvider) {
mUIImplementationProvider = uiImplementationProvider;
return this;
}
public ReactInstanceManagerBuilder setJSIModulesPackage(
@Nullable JSIModulePackage jsiModulePackage) {
mJSIModulesPackage = jsiModulePackage;
return this;
}
/** Factory for desired implementation of JavaScriptExecutor. */
public ReactInstanceManagerBuilder setJavaScriptExecutorFactory(
@Nullable JavaScriptExecutorFactory javaScriptExecutorFactory) {
mJavaScriptExecutorFactory = javaScriptExecutorFactory;
return this;
}
/**
* Name of the JS bundle file to be loaded from application's raw assets. Example: {@code
* "index.android.js"}
*/
public ReactInstanceManagerBuilder setBundleAssetName(String bundleAssetName) {
mJSBundleAssetUrl = (bundleAssetName == null ? null : "assets://" + bundleAssetName);
mJSBundleLoader = null;
return this;
}
/**
* Path to the JS bundle file to be loaded from the file system.
*
* <p>Example: {@code "assets://index.android.js" or "/sdcard/main.jsbundle"}
*/
public ReactInstanceManagerBuilder setJSBundleFile(String jsBundleFile) {
if (jsBundleFile.startsWith("assets://")) {
mJSBundleAssetUrl = jsBundleFile;
mJSBundleLoader = null;
return this;
}
return setJSBundleLoader(JSBundleLoader.createFileLoader(jsBundleFile));
}
/**
* Bundle loader to use when setting up JS environment. This supersedes prior invocations of
* {@link setJSBundleFile} and {@link setBundleAssetName}.
*
* <p>Example: {@code JSBundleLoader.createFileLoader(application, bundleFile)}
*/
public ReactInstanceManagerBuilder setJSBundleLoader(JSBundleLoader jsBundleLoader) {
mJSBundleLoader = jsBundleLoader;
mJSBundleAssetUrl = null;
return this;
}
/**
* Sets the jsEngine as JSC or HERMES as per the setJsEngineAsHermes call Uses the enum {@link
* JSInterpreter}
*
* @param jsInterpreter
*/
private void setJSEngine(JSInterpreter jsInterpreter) {
this.jsInterpreter = jsInterpreter;
}
/**
* Utility setter to set the required JSEngine as HERMES or JSC Defaults to OLD_LOGIC if not
* called by the host app
*
* @param hermesEnabled hermesEnabled = true sets the JS Engine as HERMES and JSC otherwise
*/
public ReactInstanceManagerBuilder setJsEngineAsHermes(boolean hermesEnabled) {
if (hermesEnabled) {
setJSEngine(JSInterpreter.HERMES);
} else {
setJSEngine(JSInterpreter.JSC);
}
return this;
}
/**
* Path to your app's main module on Metro. This is used when reloading JS during development. All
* paths are relative to the root folder the packager is serving files from. Examples: {@code
* "index.android"} or {@code "subdirectory/index.android"}
*/
public ReactInstanceManagerBuilder setJSMainModulePath(String jsMainModulePath) {
mJSMainModulePath = jsMainModulePath;
return this;
}
public ReactInstanceManagerBuilder addPackage(ReactPackage reactPackage) {
mPackages.add(reactPackage);
return this;
}
public ReactInstanceManagerBuilder addPackages(List<ReactPackage> reactPackages) {
mPackages.addAll(reactPackages);
return this;
}
public ReactInstanceManagerBuilder setBridgeIdleDebugListener(
NotThreadSafeBridgeIdleDebugListener bridgeIdleDebugListener) {
mBridgeIdleDebugListener = bridgeIdleDebugListener;
return this;
}
/** Required. This must be your {@code Application} instance. */
public ReactInstanceManagerBuilder setApplication(Application application) {
mApplication = application;
return this;
}
public ReactInstanceManagerBuilder setCurrentActivity(Activity activity) {
mCurrentActivity = activity;
return this;
}
public ReactInstanceManagerBuilder setDefaultHardwareBackBtnHandler(
DefaultHardwareBackBtnHandler defaultHardwareBackBtnHandler) {
mDefaultHardwareBackBtnHandler = defaultHardwareBackBtnHandler;
return this;
}
/**
* When {@code true}, developer options such as JS reloading and debugging are enabled. Note you
* still have to call {@link #showDevOptionsDialog} to show the dev menu, e.g. when the device
* Menu button is pressed.
*/
public ReactInstanceManagerBuilder setUseDeveloperSupport(boolean useDeveloperSupport) {
mUseDeveloperSupport = useDeveloperSupport;
return this;
}
/**
* Set the custom {@link DevSupportManagerFactory}. If not set, will use {@link
* DefaultDevSupportManagerFactory}.
*/
public ReactInstanceManagerBuilder setDevSupportManagerFactory(
final DevSupportManagerFactory devSupportManagerFactory) {
mDevSupportManagerFactory = devSupportManagerFactory;
return this;
}
/**
* When {@code false}, indicates that correct usage of React Native will NOT involve an Activity.
* For the vast majority of Android apps in the ecosystem, this will not need to change. Unless
* you really know what you're doing, you should probably not change this!
*/
public ReactInstanceManagerBuilder setRequireActivity(boolean requireActivity) {
mRequireActivity = requireActivity;
return this;
}
/**
* When the {@link SurfaceDelegateFactory} is provided, it will be used for native modules to get
* a {@link SurfaceDelegate} to interact with the platform specific surface that they that needs
* to be rendered in. For mobile platform this is default to be null so that these modules will
* need to provide a default surface delegate. One example of such native module is LogBoxModule,
* which is rendered in mobile platform with LogBoxDialog, while in VR platform with custom layer
* provided by runtime.
*/
public ReactInstanceManagerBuilder setSurfaceDelegateFactory(
@Nullable final SurfaceDelegateFactory surfaceDelegateFactory) {
mSurfaceDelegateFactory = surfaceDelegateFactory;
return this;
}
/**
* Sets the initial lifecycle state of the host. For example, if the host is already resumed at
* creation time, we wouldn't expect an onResume call until we get an onPause call.
*/
public ReactInstanceManagerBuilder setInitialLifecycleState(
LifecycleState initialLifecycleState) {
mInitialLifecycleState = initialLifecycleState;
return this;
}
/**
* Set the exception handler for all native module calls. If not set, the default {@link
* DevSupportManager} will be used, which shows a redbox in dev mode and rethrows (crashes the
* app) in prod mode.
*/
public ReactInstanceManagerBuilder setJSExceptionHandler(JSExceptionHandler handler) {
mJSExceptionHandler = handler;
return this;
}
public ReactInstanceManagerBuilder setRedBoxHandler(@Nullable RedBoxHandler redBoxHandler) {
mRedBoxHandler = redBoxHandler;
return this;
}
public ReactInstanceManagerBuilder setLazyViewManagersEnabled(boolean lazyViewManagersEnabled) {
mLazyViewManagersEnabled = lazyViewManagersEnabled;
return this;
}
public ReactInstanceManagerBuilder setDevBundleDownloadListener(
@Nullable DevBundleDownloadListener listener) {
mDevBundleDownloadListener = listener;
return this;
}
public ReactInstanceManagerBuilder setMinNumShakes(int minNumShakes) {
mMinNumShakes = minNumShakes;
return this;
}
public ReactInstanceManagerBuilder setMinTimeLeftInFrameForNonBatchedOperationMs(
int minTimeLeftInFrameForNonBatchedOperationMs) {
mMinTimeLeftInFrameForNonBatchedOperationMs = minTimeLeftInFrameForNonBatchedOperationMs;
return this;
}
public ReactInstanceManagerBuilder setCustomPackagerCommandHandlers(
Map<String, RequestHandler> customPackagerCommandHandlers) {
mCustomPackagerCommandHandlers = customPackagerCommandHandlers;
return this;
}
public ReactInstanceManagerBuilder setReactPackageTurboModuleManagerDelegateBuilder(
@Nullable ReactPackageTurboModuleManagerDelegate.Builder builder) {
mTMMDelegateBuilder = builder;
return this;
}
/**
* Instantiates a new {@link ReactInstanceManager}. Before calling {@code build}, the following
* must be called:
*
* <ul>
* <li>{@link #setApplication}
* <li>{@link #setCurrentActivity} if the activity has already resumed
* <li>{@link #setDefaultHardwareBackBtnHandler} if the activity has already resumed
* <li>{@link #setJSBundleFile} or {@link #setJSMainModulePath}
* </ul>
*/
public ReactInstanceManager build() {
Assertions.assertNotNull(
mApplication, "Application property has not been set with this builder");
if (mInitialLifecycleState == LifecycleState.RESUMED) {
Assertions.assertNotNull(
mCurrentActivity, "Activity needs to be set if initial lifecycle state is resumed");
}
Assertions.assertCondition(
mUseDeveloperSupport || mJSBundleAssetUrl != null || mJSBundleLoader != null,
"JS Bundle File or Asset URL has to be provided when dev support is disabled");
Assertions.assertCondition(
mJSMainModulePath != null || mJSBundleAssetUrl != null || mJSBundleLoader != null,
"Either MainModulePath or JS Bundle File needs to be provided");
if (mUIImplementationProvider == null) {
// create default UIImplementationProvider if the provided one is null.
mUIImplementationProvider = new UIImplementationProvider();
}
// We use the name of the device and the app for debugging & metrics
//noinspection ConstantConditions
String appName = mApplication.getPackageName();
String deviceName = getFriendlyDeviceName();
return new ReactInstanceManager(
mApplication,
mCurrentActivity,
mDefaultHardwareBackBtnHandler,
mJavaScriptExecutorFactory == null
? getDefaultJSExecutorFactory(appName, deviceName, mApplication.getApplicationContext())
: mJavaScriptExecutorFactory,
(mJSBundleLoader == null && mJSBundleAssetUrl != null)
? JSBundleLoader.createAssetLoader(
mApplication, mJSBundleAssetUrl, false /*Asynchronous*/)
: mJSBundleLoader,
mJSMainModulePath,
mPackages,
mUseDeveloperSupport,
mDevSupportManagerFactory == null
? new DefaultDevSupportManagerFactory()
: mDevSupportManagerFactory,
mRequireActivity,
mBridgeIdleDebugListener,
Assertions.assertNotNull(mInitialLifecycleState, "Initial lifecycle state was not set"),
mUIImplementationProvider,
mJSExceptionHandler,
mRedBoxHandler,
mLazyViewManagersEnabled,
mDevBundleDownloadListener,
mMinNumShakes,
mMinTimeLeftInFrameForNonBatchedOperationMs,
mJSIModulesPackage,
mCustomPackagerCommandHandlers,
mTMMDelegateBuilder,
mSurfaceDelegateFactory);
}
private JavaScriptExecutorFactory getDefaultJSExecutorFactory(
String appName, String deviceName, Context applicationContext) {
// Relying solely on try catch block and loading jsc even when
// project is using hermes can lead to launch-time crashes especially in
// monorepo architectures and hybrid apps using both native android
// and react native.
// So we can use the value of enableHermes received by the constructor
// to decide which library to load at launch
// if nothing is specified, use old loading method
// else load the required engine
if (jsInterpreter == JSInterpreter.OLD_LOGIC) {
try {
// If JSC is included, use it as normal
initializeSoLoaderIfNecessary(applicationContext);
JSCExecutor.loadLibrary();
return new JSCExecutorFactory(appName, deviceName);
} catch (UnsatisfiedLinkError jscE) {
if (jscE.getMessage().contains("__cxa_bad_typeid")) {
throw jscE;
}
HermesExecutor.loadLibrary();
return new HermesExecutorFactory();
}
} else if (jsInterpreter == JSInterpreter.HERMES) {
HermesExecutor.loadLibrary();
return new HermesExecutorFactory();
} else {
JSCExecutor.loadLibrary();
return new JSCExecutorFactory(appName, deviceName);
}
}
}