Clean up the interface to the bridge
Summary: Change the following classes into interfaces, with a separate Impl file: CatalystInstance, ReactInstanceManager, CatalystQueueConfiguration, MessageQueueThread. This is done to help isolate the interface between React Native and applications which use it. This will also help some intrusive development work on a branch such as porting parts of the bridge to common C++ code, without affecting app reliability while this work is ongoing. public Reviewed By: astreet Differential Revision: D2651277 fb-gh-sync-id: f04dc04a6e68df7acbc2bbf8b2529287d7b5b2ae
This commit is contained in:
parent
facf8a56d2
commit
9a61628f13
|
@ -16,44 +16,16 @@ import java.util.List;
|
|||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.CatalystInstance;
|
||||
import com.facebook.react.bridge.JSBundleLoader;
|
||||
import com.facebook.react.bridge.JSCJavaScriptExecutor;
|
||||
import com.facebook.react.bridge.JavaJSExecutor;
|
||||
import com.facebook.react.bridge.JavaScriptExecutor;
|
||||
import com.facebook.react.bridge.JavaScriptModule;
|
||||
import com.facebook.react.bridge.JavaScriptModulesConfig;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.NativeModuleRegistry;
|
||||
import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener;
|
||||
import com.facebook.react.bridge.ProxyJavaScriptExecutor;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.UiThreadUtil;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.bridge.WritableNativeMap;
|
||||
import com.facebook.react.bridge.queue.CatalystQueueConfigurationSpec;
|
||||
import com.facebook.react.common.ReactConstants;
|
||||
import com.facebook.react.common.annotations.VisibleForTesting;
|
||||
import com.facebook.react.devsupport.DevServerHelper;
|
||||
import com.facebook.react.devsupport.DevSupportManager;
|
||||
import com.facebook.react.devsupport.ReactInstanceDevCommandsHandler;
|
||||
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
import com.facebook.react.uimanager.AppRegistry;
|
||||
import com.facebook.react.uimanager.ReactNative;
|
||||
import com.facebook.react.uimanager.UIManagerModule;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
|
||||
/**
|
||||
* This class is managing instances of {@link CatalystInstance}. It expose a way to configure
|
||||
|
@ -70,169 +42,11 @@ import com.facebook.soloader.SoLoader;
|
|||
* owning activity's lifecycle events to the instance manager (see {@link #onPause},
|
||||
* {@link #onDestroy} and {@link #onResume}).
|
||||
*
|
||||
* To instantiate an instance of this class use {@link #builder}.
|
||||
* Ideally, this would be an interface, but because of the API used by earlier versions, it has to
|
||||
* have a static method, and so cannot (in Java < 8), be one.
|
||||
*/
|
||||
public class ReactInstanceManager {
|
||||
|
||||
/* should only be accessed from main thread (UI thread) */
|
||||
private final List<ReactRootView> mAttachedRootViews = new ArrayList<>();
|
||||
private LifecycleState mLifecycleState;
|
||||
private boolean mIsContextInitAsyncTaskRunning;
|
||||
private @Nullable ReactContextInitParams mPendingReactContextInitParams;
|
||||
|
||||
/* accessed from any thread */
|
||||
private @Nullable String mJSBundleFile; /* path to JS bundle on file system */
|
||||
private final @Nullable String mJSMainModuleName; /* path to JS bundle root on packager server */
|
||||
private final List<ReactPackage> mPackages;
|
||||
private final DevSupportManager mDevSupportManager;
|
||||
private final boolean mUseDeveloperSupport;
|
||||
private final @Nullable NotThreadSafeBridgeIdleDebugListener mBridgeIdleDebugListener;
|
||||
private @Nullable volatile ReactContext mCurrentReactContext;
|
||||
private final Context mApplicationContext;
|
||||
private @Nullable DefaultHardwareBackBtnHandler mDefaultBackButtonImpl;
|
||||
private String mSourceUrl;
|
||||
private @Nullable Activity mCurrentActivity;
|
||||
private volatile boolean mHasStartedCreatingInitialContext = false;
|
||||
|
||||
private final ReactInstanceDevCommandsHandler mDevInterface =
|
||||
new ReactInstanceDevCommandsHandler() {
|
||||
|
||||
@Override
|
||||
public void onReloadWithJSDebugger(JavaJSExecutor jsExecutor) {
|
||||
ReactInstanceManager.this.onReloadWithJSDebugger(jsExecutor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onJSBundleLoadedFromServer() {
|
||||
ReactInstanceManager.this.onJSBundleLoadedFromServer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toggleElementInspector() {
|
||||
ReactInstanceManager.this.toggleElementInspector();
|
||||
}
|
||||
};
|
||||
|
||||
private final DefaultHardwareBackBtnHandler mBackBtnHandler =
|
||||
new DefaultHardwareBackBtnHandler() {
|
||||
@Override
|
||||
public void invokeDefaultOnBackPressed() {
|
||||
ReactInstanceManager.this.invokeDefaultOnBackPressed();
|
||||
}
|
||||
};
|
||||
|
||||
private class ReactContextInitParams {
|
||||
private final JavaScriptExecutor mJsExecutor;
|
||||
private final JSBundleLoader mJsBundleLoader;
|
||||
|
||||
public ReactContextInitParams(
|
||||
JavaScriptExecutor jsExecutor,
|
||||
JSBundleLoader jsBundleLoader) {
|
||||
mJsExecutor = Assertions.assertNotNull(jsExecutor);
|
||||
mJsBundleLoader = Assertions.assertNotNull(jsBundleLoader);
|
||||
}
|
||||
|
||||
public JavaScriptExecutor getJsExecutor() {
|
||||
return mJsExecutor;
|
||||
}
|
||||
|
||||
public JSBundleLoader getJsBundleLoader() {
|
||||
return mJsBundleLoader;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Task class responsible for (re)creating react context in the background. These tasks can only
|
||||
* be executing one at time, see {@link #recreateReactContextInBackground()}.
|
||||
*/
|
||||
private final class ReactContextInitAsyncTask extends
|
||||
AsyncTask<ReactContextInitParams, Void, ReactApplicationContext> {
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
if (mCurrentReactContext != null) {
|
||||
tearDownReactContext(mCurrentReactContext);
|
||||
mCurrentReactContext = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ReactApplicationContext doInBackground(ReactContextInitParams... params) {
|
||||
Assertions.assertCondition(params != null && params.length > 0 && params[0] != null);
|
||||
return createReactContext(params[0].getJsExecutor(), params[0].getJsBundleLoader());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(ReactApplicationContext reactContext) {
|
||||
try {
|
||||
setupReactContext(reactContext);
|
||||
} finally {
|
||||
mIsContextInitAsyncTaskRunning = false;
|
||||
}
|
||||
|
||||
// Handle enqueued request to re-initialize react context.
|
||||
if (mPendingReactContextInitParams != null) {
|
||||
recreateReactContextInBackground(
|
||||
mPendingReactContextInitParams.getJsExecutor(),
|
||||
mPendingReactContextInitParams.getJsBundleLoader());
|
||||
mPendingReactContextInitParams = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ReactInstanceManager(
|
||||
Context applicationContext,
|
||||
@Nullable String jsBundleFile,
|
||||
@Nullable String jsMainModuleName,
|
||||
List<ReactPackage> packages,
|
||||
boolean useDeveloperSupport,
|
||||
@Nullable NotThreadSafeBridgeIdleDebugListener bridgeIdleDebugListener,
|
||||
LifecycleState initialLifecycleState) {
|
||||
initializeSoLoaderIfNecessary(applicationContext);
|
||||
|
||||
mApplicationContext = applicationContext;
|
||||
mJSBundleFile = jsBundleFile;
|
||||
mJSMainModuleName = jsMainModuleName;
|
||||
mPackages = packages;
|
||||
mUseDeveloperSupport = useDeveloperSupport;
|
||||
// We need to instantiate DevSupportManager regardless to the useDeveloperSupport option,
|
||||
// although will prevent dev support manager from displaying any options or dialogs by
|
||||
// checking useDeveloperSupport option before calling setDevSupportEnabled on this manager
|
||||
// TODO(6803830): Don't instantiate devsupport manager when useDeveloperSupport is false
|
||||
mDevSupportManager = new DevSupportManager(
|
||||
applicationContext,
|
||||
mDevInterface,
|
||||
mJSMainModuleName,
|
||||
useDeveloperSupport);
|
||||
mBridgeIdleDebugListener = bridgeIdleDebugListener;
|
||||
mLifecycleState = initialLifecycleState;
|
||||
}
|
||||
|
||||
public DevSupportManager getDevSupportManager() {
|
||||
return mDevSupportManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a builder that is capable of creating an instance of {@link ReactInstanceManager}.
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
private static void initializeSoLoaderIfNecessary(Context applicationContext) {
|
||||
// Call SoLoader.initialize here, this is required for apps that does not use exopackage and
|
||||
// does not use SoLoader for loading other native code except from the one used by React Native
|
||||
// This way we don't need to require others to have additional initialization code and to
|
||||
// subclass android.app.Application.
|
||||
|
||||
// Method SoLoader.init is idempotent, so if you wish to use native exopackage, just call
|
||||
// SoLoader.init with appropriate args before initializing ReactInstanceManager
|
||||
SoLoader.init(applicationContext, /* native exopackage */ false);
|
||||
}
|
||||
|
||||
public void setJSBundleFile(String jsBundleFile) {
|
||||
mJSBundleFile = jsBundleFile;
|
||||
}
|
||||
public abstract class ReactInstanceManager {
|
||||
public abstract DevSupportManager getDevSupportManager();
|
||||
|
||||
/**
|
||||
* Trigger react context initialization asynchronously in a background async task. This enables
|
||||
|
@ -243,129 +57,20 @@ public class ReactInstanceManager {
|
|||
*
|
||||
* Called from UI thread.
|
||||
*/
|
||||
public void createReactContextInBackground() {
|
||||
Assertions.assertCondition(
|
||||
!mHasStartedCreatingInitialContext,
|
||||
"createReactContextInBackground should only be called when creating the react " +
|
||||
"application for the first time. When reloading JS, e.g. from a new file, explicitly" +
|
||||
"use recreateReactContextInBackground");
|
||||
|
||||
mHasStartedCreatingInitialContext = true;
|
||||
recreateReactContextInBackgroundInner();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recreate the react application and context. This should be called if configuration has
|
||||
* changed or the developer has requested the app to be reloaded. It should only be called after
|
||||
* an initial call to createReactContextInBackground.
|
||||
*
|
||||
* Called from UI thread.
|
||||
*/
|
||||
public void recreateReactContextInBackground() {
|
||||
Assertions.assertCondition(
|
||||
mHasStartedCreatingInitialContext,
|
||||
"recreateReactContextInBackground should only be called after the initial " +
|
||||
"createReactContextInBackground call.");
|
||||
recreateReactContextInBackgroundInner();
|
||||
}
|
||||
|
||||
private void recreateReactContextInBackgroundInner() {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
|
||||
if (mUseDeveloperSupport && mJSMainModuleName != null) {
|
||||
if (mDevSupportManager.hasUpToDateJSBundleInCache()) {
|
||||
// If there is a up-to-date bundle downloaded from server, always use that
|
||||
onJSBundleLoadedFromServer();
|
||||
} else if (mJSBundleFile == null) {
|
||||
mDevSupportManager.handleReloadJS();
|
||||
} else {
|
||||
mDevSupportManager.isPackagerRunning(
|
||||
new DevServerHelper.PackagerStatusCallback() {
|
||||
@Override
|
||||
public void onPackagerStatusFetched(final boolean packagerIsRunning) {
|
||||
UiThreadUtil.runOnUiThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (packagerIsRunning) {
|
||||
mDevSupportManager.handleReloadJS();
|
||||
} else {
|
||||
recreateReactContextInBackgroundFromBundleFile();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
recreateReactContextInBackgroundFromBundleFile();
|
||||
}
|
||||
|
||||
private void recreateReactContextInBackgroundFromBundleFile() {
|
||||
recreateReactContextInBackground(
|
||||
new JSCJavaScriptExecutor(),
|
||||
JSBundleLoader.createFileLoader(mApplicationContext, mJSBundleFile));
|
||||
}
|
||||
public abstract void createReactContextInBackground();
|
||||
|
||||
/**
|
||||
* @return whether createReactContextInBackground has been called. Will return false after
|
||||
* onDestroy until a new initial context has been created.
|
||||
*/
|
||||
public boolean hasStartedCreatingInitialContext() {
|
||||
return mHasStartedCreatingInitialContext;
|
||||
}
|
||||
public abstract boolean hasStartedCreatingInitialContext();
|
||||
|
||||
/**
|
||||
* This method will give JS the opportunity to consume the back button event. If JS does not
|
||||
* consume the event, mDefaultBackButtonImpl will be invoked at the end of the round trip to JS.
|
||||
*/
|
||||
public void onBackPressed() {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
ReactContext reactContext = mCurrentReactContext;
|
||||
if (mCurrentReactContext == null) {
|
||||
// Invoke without round trip to JS.
|
||||
FLog.w(ReactConstants.TAG, "Instance detached from instance manager");
|
||||
invokeDefaultOnBackPressed();
|
||||
} else {
|
||||
DeviceEventManagerModule deviceEventManagerModule =
|
||||
Assertions.assertNotNull(reactContext).getNativeModule(DeviceEventManagerModule.class);
|
||||
deviceEventManagerModule.emitHardwareBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
private void invokeDefaultOnBackPressed() {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
if (mDefaultBackButtonImpl != null) {
|
||||
mDefaultBackButtonImpl.invokeDefaultOnBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
private void toggleElementInspector() {
|
||||
if (mCurrentReactContext != null) {
|
||||
mCurrentReactContext
|
||||
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||
.emit("toggleElementInspector", null);
|
||||
}
|
||||
}
|
||||
|
||||
public void onPause() {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
|
||||
mLifecycleState = LifecycleState.BEFORE_RESUME;
|
||||
|
||||
mDefaultBackButtonImpl = null;
|
||||
if (mUseDeveloperSupport) {
|
||||
mDevSupportManager.setDevSupportEnabled(false);
|
||||
}
|
||||
|
||||
mCurrentActivity = null;
|
||||
if (mCurrentReactContext != null) {
|
||||
mCurrentReactContext.onPause();
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void onBackPressed();
|
||||
public abstract void onPause();
|
||||
/**
|
||||
* Use this method when the activity resumes to enable invoking the back button directly from JS.
|
||||
*
|
||||
|
@ -377,53 +82,17 @@ public class ReactInstanceManager {
|
|||
* @param defaultBackButtonImpl a {@link DefaultHardwareBackBtnHandler} from an Activity that owns
|
||||
* this instance of {@link ReactInstanceManager}.
|
||||
*/
|
||||
public void onResume(Activity activity, DefaultHardwareBackBtnHandler defaultBackButtonImpl) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
|
||||
mLifecycleState = LifecycleState.RESUMED;
|
||||
|
||||
mDefaultBackButtonImpl = defaultBackButtonImpl;
|
||||
if (mUseDeveloperSupport) {
|
||||
mDevSupportManager.setDevSupportEnabled(true);
|
||||
}
|
||||
|
||||
mCurrentActivity = activity;
|
||||
if (mCurrentReactContext != null) {
|
||||
mCurrentReactContext.onResume(activity);
|
||||
}
|
||||
}
|
||||
|
||||
public void onDestroy() {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
|
||||
if (mUseDeveloperSupport) {
|
||||
mDevSupportManager.setDevSupportEnabled(false);
|
||||
}
|
||||
|
||||
if (mCurrentReactContext != null) {
|
||||
mCurrentReactContext.onDestroy();
|
||||
mCurrentReactContext = null;
|
||||
mHasStartedCreatingInitialContext = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (mCurrentReactContext != null) {
|
||||
mCurrentReactContext.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
public void showDevOptionsDialog() {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
mDevSupportManager.showDevOptionsDialog();
|
||||
}
|
||||
public abstract void onResume(
|
||||
Activity activity,
|
||||
DefaultHardwareBackBtnHandler defaultBackButtonImpl);
|
||||
public abstract void onDestroy();
|
||||
public abstract void onActivityResult(int requestCode, int resultCode, Intent data);
|
||||
public abstract void showDevOptionsDialog();
|
||||
|
||||
/**
|
||||
* Get the URL where the last bundle was loaded from.
|
||||
*/
|
||||
public String getSourceUrl() {
|
||||
return Assertions.assertNotNull(mSourceUrl);
|
||||
}
|
||||
public abstract String getSourceUrl();
|
||||
|
||||
/**
|
||||
* Attach given {@param rootView} to a catalyst instance manager and start JS application using
|
||||
|
@ -433,218 +102,46 @@ public class ReactInstanceManager {
|
|||
* This view will then be tracked by this manager and in case of catalyst instance restart it will
|
||||
* be re-attached.
|
||||
*/
|
||||
/* package */ void attachMeasuredRootView(ReactRootView rootView) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
mAttachedRootViews.add(rootView);
|
||||
|
||||
// If react context is being created in the background, JS application will be started
|
||||
// automatically when creation completes, as root view is part of the attached root view list.
|
||||
if (!mIsContextInitAsyncTaskRunning && mCurrentReactContext != null) {
|
||||
attachMeasuredRootViewToInstance(rootView, mCurrentReactContext.getCatalystInstance());
|
||||
}
|
||||
}
|
||||
public abstract void attachMeasuredRootView(ReactRootView rootView);
|
||||
|
||||
/**
|
||||
* Detach given {@param rootView} from current catalyst instance. It's safe to call this method
|
||||
* multiple times on the same {@param rootView} - in that case view will be detached with the
|
||||
* first call.
|
||||
*/
|
||||
/* package */ void detachRootView(ReactRootView rootView) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
if (mAttachedRootViews.remove(rootView)) {
|
||||
if (mCurrentReactContext != null && mCurrentReactContext.hasActiveCatalystInstance()) {
|
||||
detachViewFromInstance(rootView, mCurrentReactContext.getCatalystInstance());
|
||||
}
|
||||
}
|
||||
}
|
||||
public abstract void detachRootView(ReactRootView rootView);
|
||||
|
||||
/**
|
||||
* Uses configured {@link ReactPackage} instances to create all view managers
|
||||
*/
|
||||
/* package */ List<ViewManager> createAllViewManagers(
|
||||
ReactApplicationContext catalystApplicationContext) {
|
||||
List<ViewManager> allViewManagers = new ArrayList<>();
|
||||
for (ReactPackage reactPackage : mPackages) {
|
||||
allViewManagers.addAll(reactPackage.createViewManagers(catalystApplicationContext));
|
||||
}
|
||||
return allViewManagers;
|
||||
}
|
||||
public abstract List<ViewManager> createAllViewManagers(
|
||||
ReactApplicationContext catalystApplicationContext);
|
||||
|
||||
@VisibleForTesting
|
||||
public @Nullable ReactContext getCurrentReactContext() {
|
||||
return mCurrentReactContext;
|
||||
}
|
||||
|
||||
private void onReloadWithJSDebugger(JavaJSExecutor jsExecutor) {
|
||||
recreateReactContextInBackground(
|
||||
new ProxyJavaScriptExecutor(jsExecutor),
|
||||
JSBundleLoader.createRemoteDebuggerBundleLoader(
|
||||
mDevSupportManager.getJSBundleURLForRemoteDebugging()));
|
||||
}
|
||||
|
||||
private void onJSBundleLoadedFromServer() {
|
||||
recreateReactContextInBackground(
|
||||
new JSCJavaScriptExecutor(),
|
||||
JSBundleLoader.createCachedBundleFromNetworkLoader(
|
||||
mDevSupportManager.getSourceUrl(),
|
||||
mDevSupportManager.getDownloadedJSBundleFile()));
|
||||
}
|
||||
|
||||
private void recreateReactContextInBackground(
|
||||
JavaScriptExecutor jsExecutor,
|
||||
JSBundleLoader jsBundleLoader) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
|
||||
ReactContextInitParams initParams = new ReactContextInitParams(jsExecutor, jsBundleLoader);
|
||||
if (!mIsContextInitAsyncTaskRunning) {
|
||||
// No background task to create react context is currently running, create and execute one.
|
||||
ReactContextInitAsyncTask initTask = new ReactContextInitAsyncTask();
|
||||
initTask.execute(initParams);
|
||||
mIsContextInitAsyncTaskRunning = true;
|
||||
} else {
|
||||
// Background task is currently running, queue up most recent init params to recreate context
|
||||
// once task completes.
|
||||
mPendingReactContextInitParams = initParams;
|
||||
}
|
||||
}
|
||||
|
||||
private void setupReactContext(ReactApplicationContext reactContext) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
Assertions.assertCondition(mCurrentReactContext == null);
|
||||
mCurrentReactContext = Assertions.assertNotNull(reactContext);
|
||||
CatalystInstance catalystInstance =
|
||||
Assertions.assertNotNull(reactContext.getCatalystInstance());
|
||||
|
||||
catalystInstance.initialize();
|
||||
mDevSupportManager.onNewReactContextCreated(reactContext);
|
||||
moveReactContextToCurrentLifecycleState(reactContext);
|
||||
|
||||
for (ReactRootView rootView : mAttachedRootViews) {
|
||||
attachMeasuredRootViewToInstance(rootView, catalystInstance);
|
||||
}
|
||||
}
|
||||
|
||||
private void attachMeasuredRootViewToInstance(
|
||||
ReactRootView rootView,
|
||||
CatalystInstance catalystInstance) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
|
||||
// Reset view content as it's going to be populated by the application content from JS
|
||||
rootView.removeAllViews();
|
||||
rootView.setId(View.NO_ID);
|
||||
|
||||
UIManagerModule uiManagerModule = catalystInstance.getNativeModule(UIManagerModule.class);
|
||||
int rootTag = uiManagerModule.addMeasuredRootView(rootView);
|
||||
@Nullable Bundle launchOptions = rootView.getLaunchOptions();
|
||||
WritableMap initialProps = launchOptions != null
|
||||
? Arguments.fromBundle(launchOptions)
|
||||
: Arguments.createMap();
|
||||
String jsAppModuleName = rootView.getJSModuleName();
|
||||
|
||||
WritableNativeMap appParams = new WritableNativeMap();
|
||||
appParams.putDouble("rootTag", rootTag);
|
||||
appParams.putMap("initialProps", initialProps);
|
||||
catalystInstance.getJSModule(AppRegistry.class).runApplication(jsAppModuleName, appParams);
|
||||
}
|
||||
|
||||
private void detachViewFromInstance(
|
||||
ReactRootView rootView,
|
||||
CatalystInstance catalystInstance) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
catalystInstance.getJSModule(ReactNative.class)
|
||||
.unmountComponentAtNodeAndRemoveContainer(rootView.getId());
|
||||
}
|
||||
|
||||
private void tearDownReactContext(ReactContext reactContext) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
if (mLifecycleState == LifecycleState.RESUMED) {
|
||||
reactContext.onPause();
|
||||
}
|
||||
for (ReactRootView rootView : mAttachedRootViews) {
|
||||
detachViewFromInstance(rootView, reactContext.getCatalystInstance());
|
||||
}
|
||||
reactContext.onDestroy();
|
||||
mDevSupportManager.onReactInstanceDestroyed(reactContext);
|
||||
}
|
||||
public abstract @Nullable ReactContext getCurrentReactContext();
|
||||
|
||||
/**
|
||||
* @return instance of {@link ReactContext} configured a {@link CatalystInstance} set
|
||||
* Creates a builder that is capable of creating an instance of {@link ReactInstanceManagerImpl}.
|
||||
*/
|
||||
private ReactApplicationContext createReactContext(
|
||||
JavaScriptExecutor jsExecutor,
|
||||
JSBundleLoader jsBundleLoader) {
|
||||
FLog.i(ReactConstants.TAG, "Creating react context.");
|
||||
mSourceUrl = jsBundleLoader.getSourceUrl();
|
||||
NativeModuleRegistry.Builder nativeRegistryBuilder = new NativeModuleRegistry.Builder();
|
||||
JavaScriptModulesConfig.Builder jsModulesBuilder = new JavaScriptModulesConfig.Builder();
|
||||
|
||||
ReactApplicationContext reactContext = new ReactApplicationContext(mApplicationContext);
|
||||
if (mUseDeveloperSupport) {
|
||||
reactContext.setNativeModuleCallExceptionHandler(mDevSupportManager);
|
||||
}
|
||||
|
||||
CoreModulesPackage coreModulesPackage =
|
||||
new CoreModulesPackage(this, mBackBtnHandler);
|
||||
processPackage(coreModulesPackage, reactContext, nativeRegistryBuilder, jsModulesBuilder);
|
||||
|
||||
// TODO(6818138): Solve use-case of native/js modules overriding
|
||||
for (ReactPackage reactPackage : mPackages) {
|
||||
processPackage(reactPackage, reactContext, nativeRegistryBuilder, jsModulesBuilder);
|
||||
}
|
||||
|
||||
CatalystInstance.Builder catalystInstanceBuilder = new CatalystInstance.Builder()
|
||||
.setCatalystQueueConfigurationSpec(CatalystQueueConfigurationSpec.createDefault())
|
||||
.setJSExecutor(jsExecutor)
|
||||
.setRegistry(nativeRegistryBuilder.build())
|
||||
.setJSModulesConfig(jsModulesBuilder.build())
|
||||
.setJSBundleLoader(jsBundleLoader)
|
||||
.setNativeModuleCallExceptionHandler(mDevSupportManager);
|
||||
|
||||
CatalystInstance catalystInstance = catalystInstanceBuilder.build();
|
||||
if (mBridgeIdleDebugListener != null) {
|
||||
catalystInstance.addBridgeIdleDebugListener(mBridgeIdleDebugListener);
|
||||
}
|
||||
|
||||
reactContext.initializeWithInstance(catalystInstance);
|
||||
catalystInstance.runJSBundle();
|
||||
|
||||
return reactContext;
|
||||
}
|
||||
|
||||
private void processPackage(
|
||||
ReactPackage reactPackage,
|
||||
ReactApplicationContext reactContext,
|
||||
NativeModuleRegistry.Builder nativeRegistryBuilder,
|
||||
JavaScriptModulesConfig.Builder jsModulesBuilder) {
|
||||
for (NativeModule nativeModule : reactPackage.createNativeModules(reactContext)) {
|
||||
nativeRegistryBuilder.add(nativeModule);
|
||||
}
|
||||
for (Class<? extends JavaScriptModule> jsModuleClass : reactPackage.createJSModules()) {
|
||||
jsModulesBuilder.add(jsModuleClass);
|
||||
}
|
||||
}
|
||||
|
||||
private void moveReactContextToCurrentLifecycleState(ReactApplicationContext reactContext) {
|
||||
if (mLifecycleState == LifecycleState.RESUMED) {
|
||||
reactContext.onResume(mCurrentActivity);
|
||||
}
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder class for {@link ReactInstanceManager}
|
||||
* Builder class for {@link ReactInstanceManagerImpl}
|
||||
*/
|
||||
public static class Builder {
|
||||
|
||||
private final List<ReactPackage> mPackages = new ArrayList<>();
|
||||
protected final List<ReactPackage> mPackages = new ArrayList<>();
|
||||
|
||||
private @Nullable String mJSBundleFile;
|
||||
private @Nullable String mJSMainModuleName;
|
||||
private @Nullable NotThreadSafeBridgeIdleDebugListener mBridgeIdleDebugListener;
|
||||
private @Nullable Application mApplication;
|
||||
private boolean mUseDeveloperSupport;
|
||||
private @Nullable LifecycleState mInitialLifecycleState;
|
||||
protected @Nullable String mJSBundleFile;
|
||||
protected @Nullable String mJSMainModuleName;
|
||||
protected @Nullable NotThreadSafeBridgeIdleDebugListener mBridgeIdleDebugListener;
|
||||
protected @Nullable Application mApplication;
|
||||
protected boolean mUseDeveloperSupport;
|
||||
protected @Nullable LifecycleState mInitialLifecycleState;
|
||||
|
||||
private Builder() {
|
||||
protected Builder() {
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -718,7 +215,7 @@ public class ReactInstanceManager {
|
|||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new {@link ReactInstanceManager}.
|
||||
* Instantiates a new {@link ReactInstanceManagerImpl}.
|
||||
* Before calling {@code build}, the following must be called:
|
||||
* <ul>
|
||||
* <li> {@link #setApplication}
|
||||
|
@ -734,7 +231,7 @@ public class ReactInstanceManager {
|
|||
mJSMainModuleName != null || mJSBundleFile != null,
|
||||
"Either MainModuleName or JS Bundle File needs to be provided");
|
||||
|
||||
return new ReactInstanceManager(
|
||||
return new ReactInstanceManagerImpl(
|
||||
Assertions.assertNotNull(
|
||||
mApplication,
|
||||
"Application property has not been set with this builder"),
|
||||
|
|
|
@ -0,0 +1,635 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
package com.facebook.react;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.CatalystInstance;
|
||||
import com.facebook.react.bridge.CatalystInstanceImpl;
|
||||
import com.facebook.react.bridge.JSBundleLoader;
|
||||
import com.facebook.react.bridge.JSCJavaScriptExecutor;
|
||||
import com.facebook.react.bridge.JavaJSExecutor;
|
||||
import com.facebook.react.bridge.JavaScriptExecutor;
|
||||
import com.facebook.react.bridge.JavaScriptModule;
|
||||
import com.facebook.react.bridge.JavaScriptModulesConfig;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.NativeModuleRegistry;
|
||||
import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener;
|
||||
import com.facebook.react.bridge.ProxyJavaScriptExecutor;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.UiThreadUtil;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.bridge.WritableNativeMap;
|
||||
import com.facebook.react.bridge.queue.CatalystQueueConfigurationSpec;
|
||||
import com.facebook.react.common.ReactConstants;
|
||||
import com.facebook.react.common.annotations.VisibleForTesting;
|
||||
import com.facebook.react.devsupport.DevServerHelper;
|
||||
import com.facebook.react.devsupport.DevSupportManager;
|
||||
import com.facebook.react.devsupport.ReactInstanceDevCommandsHandler;
|
||||
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
import com.facebook.react.uimanager.AppRegistry;
|
||||
import com.facebook.react.uimanager.ReactNative;
|
||||
import com.facebook.react.uimanager.UIManagerModule;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
|
||||
/**
|
||||
* This class is managing instances of {@link CatalystInstance}. It expose a way to configure
|
||||
* catalyst instance using {@link ReactPackage} and keeps track of the lifecycle of that
|
||||
* instance. It also sets up connection between the instance and developers support functionality
|
||||
* of the framework.
|
||||
*
|
||||
* An instance of this manager is required to start JS application in {@link ReactRootView} (see
|
||||
* {@link ReactRootView#startReactApplication} for more info).
|
||||
*
|
||||
* The lifecycle of the instance of {@link ReactInstanceManagerImpl} should be bound to the activity
|
||||
* that owns the {@link ReactRootView} that is used to render react application using this
|
||||
* instance manager (see {@link ReactRootView#startReactApplication}). It's required to pass
|
||||
* owning activity's lifecycle events to the instance manager (see {@link #onPause},
|
||||
* {@link #onDestroy} and {@link #onResume}).
|
||||
*
|
||||
* To instantiate an instance of this class use {@link #builder}.
|
||||
*/
|
||||
/* package */ class ReactInstanceManagerImpl extends ReactInstanceManager {
|
||||
|
||||
/* should only be accessed from main thread (UI thread) */
|
||||
private final List<ReactRootView> mAttachedRootViews = new ArrayList<>();
|
||||
private LifecycleState mLifecycleState;
|
||||
private boolean mIsContextInitAsyncTaskRunning;
|
||||
private @Nullable ReactContextInitParams mPendingReactContextInitParams;
|
||||
|
||||
/* accessed from any thread */
|
||||
private @Nullable String mJSBundleFile; /* path to JS bundle on file system */
|
||||
private final @Nullable String mJSMainModuleName; /* path to JS bundle root on packager server */
|
||||
private final List<ReactPackage> mPackages;
|
||||
private final DevSupportManager mDevSupportManager;
|
||||
private final boolean mUseDeveloperSupport;
|
||||
private final @Nullable NotThreadSafeBridgeIdleDebugListener mBridgeIdleDebugListener;
|
||||
private @Nullable volatile ReactContext mCurrentReactContext;
|
||||
private final Context mApplicationContext;
|
||||
private @Nullable DefaultHardwareBackBtnHandler mDefaultBackButtonImpl;
|
||||
private String mSourceUrl;
|
||||
private @Nullable Activity mCurrentActivity;
|
||||
private volatile boolean mHasStartedCreatingInitialContext = false;
|
||||
|
||||
private final ReactInstanceDevCommandsHandler mDevInterface =
|
||||
new ReactInstanceDevCommandsHandler() {
|
||||
|
||||
@Override
|
||||
public void onReloadWithJSDebugger(JavaJSExecutor jsExecutor) {
|
||||
ReactInstanceManagerImpl.this.onReloadWithJSDebugger(jsExecutor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onJSBundleLoadedFromServer() {
|
||||
ReactInstanceManagerImpl.this.onJSBundleLoadedFromServer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toggleElementInspector() {
|
||||
ReactInstanceManagerImpl.this.toggleElementInspector();
|
||||
}
|
||||
};
|
||||
|
||||
private final DefaultHardwareBackBtnHandler mBackBtnHandler =
|
||||
new DefaultHardwareBackBtnHandler() {
|
||||
@Override
|
||||
public void invokeDefaultOnBackPressed() {
|
||||
ReactInstanceManagerImpl.this.invokeDefaultOnBackPressed();
|
||||
}
|
||||
};
|
||||
|
||||
private class ReactContextInitParams {
|
||||
private final JavaScriptExecutor mJsExecutor;
|
||||
private final JSBundleLoader mJsBundleLoader;
|
||||
|
||||
public ReactContextInitParams(
|
||||
JavaScriptExecutor jsExecutor,
|
||||
JSBundleLoader jsBundleLoader) {
|
||||
mJsExecutor = Assertions.assertNotNull(jsExecutor);
|
||||
mJsBundleLoader = Assertions.assertNotNull(jsBundleLoader);
|
||||
}
|
||||
|
||||
public JavaScriptExecutor getJsExecutor() {
|
||||
return mJsExecutor;
|
||||
}
|
||||
|
||||
public JSBundleLoader getJsBundleLoader() {
|
||||
return mJsBundleLoader;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Task class responsible for (re)creating react context in the background. These tasks can only
|
||||
* be executing one at time, see {@link #recreateReactContextInBackground()}.
|
||||
*/
|
||||
private final class ReactContextInitAsyncTask extends
|
||||
AsyncTask<ReactContextInitParams, Void, ReactApplicationContext> {
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
if (mCurrentReactContext != null) {
|
||||
tearDownReactContext(mCurrentReactContext);
|
||||
mCurrentReactContext = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ReactApplicationContext doInBackground(ReactContextInitParams... params) {
|
||||
Assertions.assertCondition(params != null && params.length > 0 && params[0] != null);
|
||||
return createReactContext(params[0].getJsExecutor(), params[0].getJsBundleLoader());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(ReactApplicationContext reactContext) {
|
||||
try {
|
||||
setupReactContext(reactContext);
|
||||
} finally {
|
||||
mIsContextInitAsyncTaskRunning = false;
|
||||
}
|
||||
|
||||
// Handle enqueued request to re-initialize react context.
|
||||
if (mPendingReactContextInitParams != null) {
|
||||
recreateReactContextInBackground(
|
||||
mPendingReactContextInitParams.getJsExecutor(),
|
||||
mPendingReactContextInitParams.getJsBundleLoader());
|
||||
mPendingReactContextInitParams = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* package */ ReactInstanceManagerImpl(
|
||||
Context applicationContext,
|
||||
@Nullable String jsBundleFile,
|
||||
@Nullable String jsMainModuleName,
|
||||
List<ReactPackage> packages,
|
||||
boolean useDeveloperSupport,
|
||||
@Nullable NotThreadSafeBridgeIdleDebugListener bridgeIdleDebugListener,
|
||||
LifecycleState initialLifecycleState) {
|
||||
initializeSoLoaderIfNecessary(applicationContext);
|
||||
|
||||
mApplicationContext = applicationContext;
|
||||
mJSBundleFile = jsBundleFile;
|
||||
mJSMainModuleName = jsMainModuleName;
|
||||
mPackages = packages;
|
||||
mUseDeveloperSupport = useDeveloperSupport;
|
||||
// We need to instantiate DevSupportManager regardless to the useDeveloperSupport option,
|
||||
// although will prevent dev support manager from displaying any options or dialogs by
|
||||
// checking useDeveloperSupport option before calling setDevSupportEnabled on this manager
|
||||
// TODO(6803830): Don't instantiate devsupport manager when useDeveloperSupport is false
|
||||
mDevSupportManager = new DevSupportManager(
|
||||
applicationContext,
|
||||
mDevInterface,
|
||||
mJSMainModuleName,
|
||||
useDeveloperSupport);
|
||||
mBridgeIdleDebugListener = bridgeIdleDebugListener;
|
||||
mLifecycleState = initialLifecycleState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DevSupportManager getDevSupportManager() {
|
||||
return mDevSupportManager;
|
||||
}
|
||||
|
||||
private static void initializeSoLoaderIfNecessary(Context applicationContext) {
|
||||
// Call SoLoader.initialize here, this is required for apps that does not use exopackage and
|
||||
// does not use SoLoader for loading other native code except from the one used by React Native
|
||||
// This way we don't need to require others to have additional initialization code and to
|
||||
// subclass android.app.Application.
|
||||
|
||||
// Method SoLoader.init is idempotent, so if you wish to use native exopackage, just call
|
||||
// SoLoader.init with appropriate args before initializing ReactInstanceManagerImpl
|
||||
SoLoader.init(applicationContext, /* native exopackage */ false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger react context initialization asynchronously in a background async task. This enables
|
||||
* applications to pre-load the application JS, and execute global code before
|
||||
* {@link ReactRootView} is available and measured. This should only be called the first time the
|
||||
* application is set up, which is enforced to keep developers from accidentally creating their
|
||||
* application multiple times without realizing it.
|
||||
*
|
||||
* Called from UI thread.
|
||||
*/
|
||||
@Override
|
||||
public void createReactContextInBackground() {
|
||||
Assertions.assertCondition(
|
||||
!mHasStartedCreatingInitialContext,
|
||||
"createReactContextInBackground should only be called when creating the react " +
|
||||
"application for the first time. When reloading JS, e.g. from a new file, explicitly" +
|
||||
"use recreateReactContextInBackground");
|
||||
|
||||
mHasStartedCreatingInitialContext = true;
|
||||
recreateReactContextInBackgroundInner();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recreate the react application and context. This should be called if configuration has
|
||||
* changed or the developer has requested the app to be reloaded. It should only be called after
|
||||
* an initial call to createReactContextInBackground.
|
||||
*
|
||||
* Called from UI thread.
|
||||
*/
|
||||
public void recreateReactContextInBackground() {
|
||||
Assertions.assertCondition(
|
||||
mHasStartedCreatingInitialContext,
|
||||
"recreateReactContextInBackground should only be called after the initial " +
|
||||
"createReactContextInBackground call.");
|
||||
recreateReactContextInBackgroundInner();
|
||||
}
|
||||
|
||||
private void recreateReactContextInBackgroundInner() {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
|
||||
if (mUseDeveloperSupport && mJSMainModuleName != null) {
|
||||
if (mDevSupportManager.hasUpToDateJSBundleInCache()) {
|
||||
// If there is a up-to-date bundle downloaded from server, always use that
|
||||
onJSBundleLoadedFromServer();
|
||||
} else if (mJSBundleFile == null) {
|
||||
mDevSupportManager.handleReloadJS();
|
||||
} else {
|
||||
mDevSupportManager.isPackagerRunning(
|
||||
new DevServerHelper.PackagerStatusCallback() {
|
||||
@Override
|
||||
public void onPackagerStatusFetched(final boolean packagerIsRunning) {
|
||||
UiThreadUtil.runOnUiThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (packagerIsRunning) {
|
||||
mDevSupportManager.handleReloadJS();
|
||||
} else {
|
||||
recreateReactContextInBackgroundFromBundleFile();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
recreateReactContextInBackgroundFromBundleFile();
|
||||
}
|
||||
|
||||
private void recreateReactContextInBackgroundFromBundleFile() {
|
||||
recreateReactContextInBackground(
|
||||
new JSCJavaScriptExecutor(),
|
||||
JSBundleLoader.createFileLoader(mApplicationContext, mJSBundleFile));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether createReactContextInBackground has been called. Will return false after
|
||||
* onDestroy until a new initial context has been created.
|
||||
*/
|
||||
public boolean hasStartedCreatingInitialContext() {
|
||||
return mHasStartedCreatingInitialContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will give JS the opportunity to consume the back button event. If JS does not
|
||||
* consume the event, mDefaultBackButtonImpl will be invoked at the end of the round trip to JS.
|
||||
*/
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
ReactContext reactContext = mCurrentReactContext;
|
||||
if (mCurrentReactContext == null) {
|
||||
// Invoke without round trip to JS.
|
||||
FLog.w(ReactConstants.TAG, "Instance detached from instance manager");
|
||||
invokeDefaultOnBackPressed();
|
||||
} else {
|
||||
DeviceEventManagerModule deviceEventManagerModule =
|
||||
Assertions.assertNotNull(reactContext).getNativeModule(DeviceEventManagerModule.class);
|
||||
deviceEventManagerModule.emitHardwareBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
private void invokeDefaultOnBackPressed() {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
if (mDefaultBackButtonImpl != null) {
|
||||
mDefaultBackButtonImpl.invokeDefaultOnBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
private void toggleElementInspector() {
|
||||
if (mCurrentReactContext != null) {
|
||||
mCurrentReactContext
|
||||
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||
.emit("toggleElementInspector", null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
|
||||
mLifecycleState = LifecycleState.BEFORE_RESUME;
|
||||
|
||||
mDefaultBackButtonImpl = null;
|
||||
if (mUseDeveloperSupport) {
|
||||
mDevSupportManager.setDevSupportEnabled(false);
|
||||
}
|
||||
|
||||
mCurrentActivity = null;
|
||||
if (mCurrentReactContext != null) {
|
||||
mCurrentReactContext.onPause();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this method when the activity resumes to enable invoking the back button directly from JS.
|
||||
*
|
||||
* This method retains an instance to provided mDefaultBackButtonImpl. Thus it's
|
||||
* important to pass from the activity instance that owns this particular instance of {@link
|
||||
* ReactInstanceManagerImpl}, so that once this instance receive {@link #onDestroy} event it will
|
||||
* clear the reference to that defaultBackButtonImpl.
|
||||
*
|
||||
* @param defaultBackButtonImpl a {@link DefaultHardwareBackBtnHandler} from an Activity that owns
|
||||
* this instance of {@link ReactInstanceManagerImpl}.
|
||||
*/
|
||||
@Override
|
||||
public void onResume(Activity activity, DefaultHardwareBackBtnHandler defaultBackButtonImpl) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
|
||||
mLifecycleState = LifecycleState.RESUMED;
|
||||
|
||||
mDefaultBackButtonImpl = defaultBackButtonImpl;
|
||||
if (mUseDeveloperSupport) {
|
||||
mDevSupportManager.setDevSupportEnabled(true);
|
||||
}
|
||||
|
||||
mCurrentActivity = activity;
|
||||
if (mCurrentReactContext != null) {
|
||||
mCurrentReactContext.onResume(activity);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
|
||||
if (mUseDeveloperSupport) {
|
||||
mDevSupportManager.setDevSupportEnabled(false);
|
||||
}
|
||||
|
||||
if (mCurrentReactContext != null) {
|
||||
mCurrentReactContext.onDestroy();
|
||||
mCurrentReactContext = null;
|
||||
mHasStartedCreatingInitialContext = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (mCurrentReactContext != null) {
|
||||
mCurrentReactContext.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showDevOptionsDialog() {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
mDevSupportManager.showDevOptionsDialog();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL where the last bundle was loaded from.
|
||||
*/
|
||||
@Override
|
||||
public String getSourceUrl() {
|
||||
return Assertions.assertNotNull(mSourceUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach given {@param rootView} to a catalyst instance manager and start JS application using
|
||||
* JS module provided by {@link ReactRootView#getJSModuleName}. If the react context is currently
|
||||
* being (re)-created, or if react context has not been created yet, the JS application associated
|
||||
* with the provided root view will be started asynchronously, i.e this method won't block.
|
||||
* This view will then be tracked by this manager and in case of catalyst instance restart it will
|
||||
* be re-attached.
|
||||
*/
|
||||
@Override
|
||||
public void attachMeasuredRootView(ReactRootView rootView) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
mAttachedRootViews.add(rootView);
|
||||
|
||||
// If react context is being created in the background, JS application will be started
|
||||
// automatically when creation completes, as root view is part of the attached root view list.
|
||||
if (!mIsContextInitAsyncTaskRunning && mCurrentReactContext != null) {
|
||||
attachMeasuredRootViewToInstance(rootView, mCurrentReactContext.getCatalystInstance());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach given {@param rootView} from current catalyst instance. It's safe to call this method
|
||||
* multiple times on the same {@param rootView} - in that case view will be detached with the
|
||||
* first call.
|
||||
*/
|
||||
@Override
|
||||
public void detachRootView(ReactRootView rootView) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
if (mAttachedRootViews.remove(rootView)) {
|
||||
if (mCurrentReactContext != null && mCurrentReactContext.hasActiveCatalystInstance()) {
|
||||
detachViewFromInstance(rootView, mCurrentReactContext.getCatalystInstance());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses configured {@link ReactPackage} instances to create all view managers
|
||||
*/
|
||||
@Override
|
||||
public List<ViewManager> createAllViewManagers(
|
||||
ReactApplicationContext catalystApplicationContext) {
|
||||
List<ViewManager> allViewManagers = new ArrayList<>();
|
||||
for (ReactPackage reactPackage : mPackages) {
|
||||
allViewManagers.addAll(reactPackage.createViewManagers(catalystApplicationContext));
|
||||
}
|
||||
return allViewManagers;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@Override
|
||||
public @Nullable ReactContext getCurrentReactContext() {
|
||||
return mCurrentReactContext;
|
||||
}
|
||||
|
||||
private void onReloadWithJSDebugger(JavaJSExecutor jsExecutor) {
|
||||
recreateReactContextInBackground(
|
||||
new ProxyJavaScriptExecutor(jsExecutor),
|
||||
JSBundleLoader.createRemoteDebuggerBundleLoader(
|
||||
mDevSupportManager.getJSBundleURLForRemoteDebugging()));
|
||||
}
|
||||
|
||||
private void onJSBundleLoadedFromServer() {
|
||||
recreateReactContextInBackground(
|
||||
new JSCJavaScriptExecutor(),
|
||||
JSBundleLoader.createCachedBundleFromNetworkLoader(
|
||||
mDevSupportManager.getSourceUrl(),
|
||||
mDevSupportManager.getDownloadedJSBundleFile()));
|
||||
}
|
||||
|
||||
private void recreateReactContextInBackground(
|
||||
JavaScriptExecutor jsExecutor,
|
||||
JSBundleLoader jsBundleLoader) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
|
||||
ReactContextInitParams initParams = new ReactContextInitParams(jsExecutor, jsBundleLoader);
|
||||
if (!mIsContextInitAsyncTaskRunning) {
|
||||
// No background task to create react context is currently running, create and execute one.
|
||||
ReactContextInitAsyncTask initTask = new ReactContextInitAsyncTask();
|
||||
initTask.execute(initParams);
|
||||
mIsContextInitAsyncTaskRunning = true;
|
||||
} else {
|
||||
// Background task is currently running, queue up most recent init params to recreate context
|
||||
// once task completes.
|
||||
mPendingReactContextInitParams = initParams;
|
||||
}
|
||||
}
|
||||
|
||||
private void setupReactContext(ReactApplicationContext reactContext) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
Assertions.assertCondition(mCurrentReactContext == null);
|
||||
mCurrentReactContext = Assertions.assertNotNull(reactContext);
|
||||
CatalystInstance catalystInstance =
|
||||
Assertions.assertNotNull(reactContext.getCatalystInstance());
|
||||
|
||||
catalystInstance.initialize();
|
||||
mDevSupportManager.onNewReactContextCreated(reactContext);
|
||||
moveReactContextToCurrentLifecycleState(reactContext);
|
||||
|
||||
for (ReactRootView rootView : mAttachedRootViews) {
|
||||
attachMeasuredRootViewToInstance(rootView, catalystInstance);
|
||||
}
|
||||
}
|
||||
|
||||
private void attachMeasuredRootViewToInstance(
|
||||
ReactRootView rootView,
|
||||
CatalystInstance catalystInstance) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
|
||||
// Reset view content as it's going to be populated by the application content from JS
|
||||
rootView.removeAllViews();
|
||||
rootView.setId(View.NO_ID);
|
||||
|
||||
UIManagerModule uiManagerModule = catalystInstance.getNativeModule(UIManagerModule.class);
|
||||
int rootTag = uiManagerModule.addMeasuredRootView(rootView);
|
||||
@Nullable Bundle launchOptions = rootView.getLaunchOptions();
|
||||
WritableMap initialProps = launchOptions != null
|
||||
? Arguments.fromBundle(launchOptions)
|
||||
: Arguments.createMap();
|
||||
String jsAppModuleName = rootView.getJSModuleName();
|
||||
|
||||
WritableNativeMap appParams = new WritableNativeMap();
|
||||
appParams.putDouble("rootTag", rootTag);
|
||||
appParams.putMap("initialProps", initialProps);
|
||||
catalystInstance.getJSModule(AppRegistry.class).runApplication(jsAppModuleName, appParams);
|
||||
}
|
||||
|
||||
private void detachViewFromInstance(
|
||||
ReactRootView rootView,
|
||||
CatalystInstance catalystInstance) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
catalystInstance.getJSModule(ReactNative.class)
|
||||
.unmountComponentAtNodeAndRemoveContainer(rootView.getId());
|
||||
}
|
||||
|
||||
private void tearDownReactContext(ReactContext reactContext) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
if (mLifecycleState == LifecycleState.RESUMED) {
|
||||
reactContext.onPause();
|
||||
}
|
||||
for (ReactRootView rootView : mAttachedRootViews) {
|
||||
detachViewFromInstance(rootView, reactContext.getCatalystInstance());
|
||||
}
|
||||
reactContext.onDestroy();
|
||||
mDevSupportManager.onReactInstanceDestroyed(reactContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return instance of {@link ReactContext} configured a {@link CatalystInstance} set
|
||||
*/
|
||||
private ReactApplicationContext createReactContext(
|
||||
JavaScriptExecutor jsExecutor,
|
||||
JSBundleLoader jsBundleLoader) {
|
||||
FLog.i(ReactConstants.TAG, "Creating react context.");
|
||||
mSourceUrl = jsBundleLoader.getSourceUrl();
|
||||
NativeModuleRegistry.Builder nativeRegistryBuilder = new NativeModuleRegistry.Builder();
|
||||
JavaScriptModulesConfig.Builder jsModulesBuilder = new JavaScriptModulesConfig.Builder();
|
||||
|
||||
ReactApplicationContext reactContext = new ReactApplicationContext(mApplicationContext);
|
||||
if (mUseDeveloperSupport) {
|
||||
reactContext.setNativeModuleCallExceptionHandler(mDevSupportManager);
|
||||
}
|
||||
|
||||
CoreModulesPackage coreModulesPackage =
|
||||
new CoreModulesPackage(this, mBackBtnHandler);
|
||||
processPackage(coreModulesPackage, reactContext, nativeRegistryBuilder, jsModulesBuilder);
|
||||
|
||||
// TODO(6818138): Solve use-case of native/js modules overriding
|
||||
for (ReactPackage reactPackage : mPackages) {
|
||||
processPackage(reactPackage, reactContext, nativeRegistryBuilder, jsModulesBuilder);
|
||||
}
|
||||
|
||||
CatalystInstanceImpl.Builder catalystInstanceBuilder = new CatalystInstanceImpl.Builder()
|
||||
.setCatalystQueueConfigurationSpec(CatalystQueueConfigurationSpec.createDefault())
|
||||
.setJSExecutor(jsExecutor)
|
||||
.setRegistry(nativeRegistryBuilder.build())
|
||||
.setJSModulesConfig(jsModulesBuilder.build())
|
||||
.setJSBundleLoader(jsBundleLoader)
|
||||
.setNativeModuleCallExceptionHandler(mDevSupportManager);
|
||||
|
||||
CatalystInstance catalystInstance = catalystInstanceBuilder.build();
|
||||
if (mBridgeIdleDebugListener != null) {
|
||||
catalystInstance.addBridgeIdleDebugListener(mBridgeIdleDebugListener);
|
||||
}
|
||||
|
||||
reactContext.initializeWithInstance(catalystInstance);
|
||||
catalystInstance.runJSBundle();
|
||||
|
||||
return reactContext;
|
||||
}
|
||||
|
||||
private void processPackage(
|
||||
ReactPackage reactPackage,
|
||||
ReactApplicationContext reactContext,
|
||||
NativeModuleRegistry.Builder nativeRegistryBuilder,
|
||||
JavaScriptModulesConfig.Builder jsModulesBuilder) {
|
||||
for (NativeModule nativeModule : reactPackage.createNativeModules(reactContext)) {
|
||||
nativeRegistryBuilder.add(nativeModule);
|
||||
}
|
||||
for (Class<? extends JavaScriptModule> jsModuleClass : reactPackage.createJSModules()) {
|
||||
jsModulesBuilder.add(jsModuleClass);
|
||||
}
|
||||
}
|
||||
|
||||
private void moveReactContextToCurrentLifecycleState(ReactApplicationContext reactContext) {
|
||||
if (mLifecycleState == LifecycleState.RESUMED) {
|
||||
reactContext.onResume(mCurrentActivity);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,27 +11,11 @@ package com.facebook.react.bridge;
|
|||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.react.bridge.queue.CatalystQueueConfiguration;
|
||||
import com.facebook.react.bridge.queue.CatalystQueueConfigurationSpec;
|
||||
import com.facebook.react.bridge.queue.QueueThreadExceptionHandler;
|
||||
import com.facebook.proguard.annotations.DoNotStrip;
|
||||
import com.facebook.react.common.ReactConstants;
|
||||
import com.facebook.react.common.annotations.VisibleForTesting;
|
||||
import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.systrace.Systrace;
|
||||
import com.facebook.systrace.TraceListener;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonFactory;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
|
||||
/**
|
||||
* A higher level API on top of the asynchronous JSC bridge. This provides an
|
||||
|
@ -39,254 +23,31 @@ import com.fasterxml.jackson.core.JsonGenerator;
|
|||
* Java APIs be invokable from JavaScript as well.
|
||||
*/
|
||||
@DoNotStrip
|
||||
public class CatalystInstance {
|
||||
|
||||
private static final int BRIDGE_SETUP_TIMEOUT_MS = 30000;
|
||||
private static final int LOAD_JS_BUNDLE_TIMEOUT_MS = 30000;
|
||||
|
||||
private static final AtomicInteger sNextInstanceIdForTrace = new AtomicInteger(1);
|
||||
|
||||
// Access from any thread
|
||||
private final CatalystQueueConfiguration mCatalystQueueConfiguration;
|
||||
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;
|
||||
|
||||
// Access from native modules thread
|
||||
private final NativeModuleRegistry mJavaRegistry;
|
||||
private final NativeModuleCallExceptionHandler mNativeModuleCallExceptionHandler;
|
||||
private boolean mInitialized = false;
|
||||
|
||||
// Access from JS thread
|
||||
private @Nullable ReactBridge mBridge;
|
||||
private boolean mJSBundleHasLoaded;
|
||||
|
||||
private CatalystInstance(
|
||||
final CatalystQueueConfigurationSpec catalystQueueConfigurationSpec,
|
||||
final JavaScriptExecutor jsExecutor,
|
||||
final NativeModuleRegistry registry,
|
||||
final JavaScriptModulesConfig jsModulesConfig,
|
||||
final JSBundleLoader jsBundleLoader,
|
||||
NativeModuleCallExceptionHandler nativeModuleCallExceptionHandler) {
|
||||
mCatalystQueueConfiguration = CatalystQueueConfiguration.create(
|
||||
catalystQueueConfigurationSpec,
|
||||
new NativeExceptionHandler());
|
||||
mBridgeIdleListeners = new CopyOnWriteArrayList<>();
|
||||
mJavaRegistry = registry;
|
||||
mJSModuleRegistry = new JavaScriptModuleRegistry(CatalystInstance.this, jsModulesConfig);
|
||||
mJSBundleLoader = jsBundleLoader;
|
||||
mNativeModuleCallExceptionHandler = nativeModuleCallExceptionHandler;
|
||||
mTraceListener = new JSProfilerTraceListener();
|
||||
|
||||
final CountDownLatch initLatch = new CountDownLatch(1);
|
||||
mCatalystQueueConfiguration.getJSQueueThread().runOnQueue(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
initializeBridge(jsExecutor, jsModulesConfig);
|
||||
initLatch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
Assertions.assertCondition(
|
||||
initLatch.await(BRIDGE_SETUP_TIMEOUT_MS, TimeUnit.MILLISECONDS),
|
||||
"Timed out waiting for bridge to initialize!");
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeBridge(
|
||||
JavaScriptExecutor jsExecutor,
|
||||
JavaScriptModulesConfig jsModulesConfig) {
|
||||
mCatalystQueueConfiguration.getJSQueueThread().assertIsOnThread();
|
||||
Assertions.assertCondition(mBridge == null, "initializeBridge should be called once");
|
||||
|
||||
mBridge = new ReactBridge(
|
||||
jsExecutor,
|
||||
new NativeModulesReactCallback(),
|
||||
mCatalystQueueConfiguration.getNativeModulesQueueThread());
|
||||
mBridge.setGlobalVariable(
|
||||
"__fbBatchedBridgeConfig",
|
||||
buildModulesConfigJSONProperty(mJavaRegistry, jsModulesConfig));
|
||||
|
||||
Systrace.registerListener(mTraceListener);
|
||||
}
|
||||
|
||||
public void runJSBundle() {
|
||||
Systrace.beginSection(
|
||||
Systrace.TRACE_TAG_REACT_JAVA_BRIDGE,
|
||||
"CatalystInstance_runJSBundle");
|
||||
|
||||
try {
|
||||
final CountDownLatch initLatch = new CountDownLatch(1);
|
||||
mCatalystQueueConfiguration.getJSQueueThread().runOnQueue(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Assertions.assertCondition(!mJSBundleHasLoaded, "JS bundle was already loaded!");
|
||||
mJSBundleHasLoaded = true;
|
||||
|
||||
incrementPendingJSCalls();
|
||||
|
||||
try {
|
||||
mJSBundleLoader.loadScript(mBridge);
|
||||
} catch (JSExecutionException e) {
|
||||
mNativeModuleCallExceptionHandler.handleException(e);
|
||||
}
|
||||
|
||||
initLatch.countDown();
|
||||
}
|
||||
});
|
||||
Assertions.assertCondition(
|
||||
initLatch.await(LOAD_JS_BUNDLE_TIMEOUT_MS, TimeUnit.MILLISECONDS),
|
||||
"Timed out loading JS!");
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
|
||||
}
|
||||
}
|
||||
|
||||
/* package */ void callFunction(
|
||||
final int moduleId,
|
||||
final int methodId,
|
||||
final NativeArray arguments,
|
||||
final String tracingName) {
|
||||
if (mDestroyed) {
|
||||
FLog.w(ReactConstants.TAG, "Calling JS function after bridge has been destroyed.");
|
||||
return;
|
||||
}
|
||||
|
||||
incrementPendingJSCalls();
|
||||
|
||||
mCatalystQueueConfiguration.getJSQueueThread().runOnQueue(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mCatalystQueueConfiguration.getJSQueueThread().assertIsOnThread();
|
||||
|
||||
if (mDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, tracingName);
|
||||
try {
|
||||
Assertions.assertNotNull(mBridge).callFunction(moduleId, methodId, arguments);
|
||||
} finally {
|
||||
Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public interface CatalystInstance {
|
||||
void runJSBundle();
|
||||
// This is called from java code, so it won't be stripped anyway, but proguard will rename it,
|
||||
// which this prevents.
|
||||
@DoNotStrip
|
||||
/* package */ void invokeCallback(final int callbackID, final NativeArray arguments) {
|
||||
if (mDestroyed) {
|
||||
FLog.w(ReactConstants.TAG, "Invoking JS callback after bridge has been destroyed.");
|
||||
return;
|
||||
}
|
||||
|
||||
incrementPendingJSCalls();
|
||||
|
||||
mCatalystQueueConfiguration.getJSQueueThread().runOnQueue(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mCatalystQueueConfiguration.getJSQueueThread().assertIsOnThread();
|
||||
|
||||
if (mDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "<callback>");
|
||||
try {
|
||||
Assertions.assertNotNull(mBridge).invokeCallback(callbackID, arguments);
|
||||
} finally {
|
||||
Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void invokeCallback(final int callbackID, final NativeArray arguments);
|
||||
/**
|
||||
* Destroys this catalyst instance, waiting for any other threads in CatalystQueueConfiguration
|
||||
* (besides the UI thread) to finish running. Must be called from the UI thread so that we can
|
||||
* fully shut down other threads.
|
||||
*/
|
||||
/* package */ void destroy() {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
|
||||
if (mDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: tell all APIs to shut down
|
||||
mDestroyed = true;
|
||||
mJavaRegistry.notifyCatalystInstanceDestroy();
|
||||
mCatalystQueueConfiguration.destroy();
|
||||
boolean wasIdle = (mPendingJSCalls.getAndSet(0) == 0);
|
||||
if (!wasIdle && !mBridgeIdleListeners.isEmpty()) {
|
||||
for (NotThreadSafeBridgeIdleDebugListener listener : mBridgeIdleListeners) {
|
||||
listener.onTransitionToBridgeIdle();
|
||||
}
|
||||
}
|
||||
|
||||
if (mBridge != null) {
|
||||
Systrace.unregisterListener(mTraceListener);
|
||||
}
|
||||
|
||||
// We can access the Bridge from any thread now because we know either we are on the JS thread
|
||||
// or the JS thread has finished via CatalystQueueConfiguration#destroy()
|
||||
Assertions.assertNotNull(mBridge).dispose();
|
||||
}
|
||||
|
||||
public boolean isDestroyed() {
|
||||
return mDestroyed;
|
||||
}
|
||||
void destroy();
|
||||
boolean isDestroyed();
|
||||
|
||||
/**
|
||||
* Initialize all the native modules
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public void initialize() {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
Assertions.assertCondition(
|
||||
!mInitialized,
|
||||
"This catalyst instance has already been initialized");
|
||||
mInitialized = true;
|
||||
mJavaRegistry.notifyCatalystInstanceInitialized();
|
||||
}
|
||||
void initialize();
|
||||
|
||||
public CatalystQueueConfiguration getCatalystQueueConfiguration() {
|
||||
return mCatalystQueueConfiguration;
|
||||
}
|
||||
CatalystQueueConfiguration getCatalystQueueConfiguration();
|
||||
|
||||
@VisibleForTesting
|
||||
public @Nullable
|
||||
ReactBridge getBridge() {
|
||||
return mBridge;
|
||||
}
|
||||
|
||||
public <T extends JavaScriptModule> T getJSModule(Class<T> jsInterface) {
|
||||
return Assertions.assertNotNull(mJSModuleRegistry).getJavaScriptModule(jsInterface);
|
||||
}
|
||||
|
||||
public <T extends NativeModule> T getNativeModule(Class<T> nativeModuleInterface) {
|
||||
return mJavaRegistry.getModule(nativeModuleInterface);
|
||||
}
|
||||
|
||||
public Collection<NativeModule> getNativeModules() {
|
||||
return mJavaRegistry.getAllModules();
|
||||
}
|
||||
<T extends JavaScriptModule> T getJSModule(Class<T> jsInterface);
|
||||
<T extends NativeModule> T getNativeModule(Class<T> nativeModuleInterface);
|
||||
Collection<NativeModule> getNativeModules();
|
||||
|
||||
/**
|
||||
* Adds a idle listener for this Catalyst instance. The listener will receive notifications
|
||||
|
@ -294,204 +55,15 @@ public class CatalystInstance {
|
|||
* defined as there being some non-zero number of calls to JS that haven't resolved via a
|
||||
* onBatchCompleted call. The listener should be purely passive and not affect application logic.
|
||||
*/
|
||||
public void addBridgeIdleDebugListener(NotThreadSafeBridgeIdleDebugListener listener) {
|
||||
mBridgeIdleListeners.add(listener);
|
||||
}
|
||||
void addBridgeIdleDebugListener(NotThreadSafeBridgeIdleDebugListener listener);
|
||||
|
||||
/**
|
||||
* Removes a NotThreadSafeBridgeIdleDebugListener previously added with
|
||||
* {@link #addBridgeIdleDebugListener}
|
||||
*/
|
||||
public void removeBridgeIdleDebugListener(NotThreadSafeBridgeIdleDebugListener listener) {
|
||||
mBridgeIdleListeners.remove(listener);
|
||||
}
|
||||
void removeBridgeIdleDebugListener(NotThreadSafeBridgeIdleDebugListener listener);
|
||||
|
||||
public boolean supportsProfiling() {
|
||||
if (mBridge == null) {
|
||||
return false;
|
||||
}
|
||||
return mBridge.supportsProfiling();
|
||||
}
|
||||
|
||||
public void startProfiler(String title) {
|
||||
if (mBridge == null) {
|
||||
return;
|
||||
}
|
||||
mBridge.startProfiler(title);
|
||||
}
|
||||
|
||||
public void stopProfiler(String title, String filename) {
|
||||
if (mBridge == null) {
|
||||
return;
|
||||
}
|
||||
mBridge.stopProfiler(title, filename);
|
||||
}
|
||||
|
||||
private String buildModulesConfigJSONProperty(
|
||||
NativeModuleRegistry nativeModuleRegistry,
|
||||
JavaScriptModulesConfig jsModulesConfig) {
|
||||
// TODO(5300733): Serialize config using single json generator
|
||||
JsonFactory jsonFactory = new JsonFactory();
|
||||
StringWriter writer = new StringWriter();
|
||||
try {
|
||||
JsonGenerator jg = jsonFactory.createGenerator(writer);
|
||||
jg.writeStartObject();
|
||||
jg.writeFieldName("remoteModuleConfig");
|
||||
jg.writeRawValue(nativeModuleRegistry.moduleDescriptions());
|
||||
jg.writeFieldName("localModulesConfig");
|
||||
jg.writeRawValue(jsModulesConfig.moduleDescriptions());
|
||||
jg.writeEndObject();
|
||||
jg.close();
|
||||
} catch (IOException ioe) {
|
||||
throw new RuntimeException("Unable to serialize JavaScript module declaration", ioe);
|
||||
}
|
||||
return writer.getBuffer().toString();
|
||||
}
|
||||
|
||||
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()) {
|
||||
for (NotThreadSafeBridgeIdleDebugListener listener : mBridgeIdleListeners) {
|
||||
listener.onTransitionToBridgeBusy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void decrementPendingJSCalls() {
|
||||
int newPendingCalls = mPendingJSCalls.decrementAndGet();
|
||||
Assertions.assertCondition(newPendingCalls >= 0);
|
||||
boolean isNowIdle = newPendingCalls == 0;
|
||||
Systrace.traceCounter(
|
||||
Systrace.TRACE_TAG_REACT_JAVA_BRIDGE,
|
||||
mJsPendingCallsTitleForTrace,
|
||||
newPendingCalls);
|
||||
|
||||
if (isNowIdle && !mBridgeIdleListeners.isEmpty()) {
|
||||
for (NotThreadSafeBridgeIdleDebugListener listener : mBridgeIdleListeners) {
|
||||
listener.onTransitionToBridgeIdle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class NativeModulesReactCallback implements ReactCallback {
|
||||
|
||||
@Override
|
||||
public void call(int moduleId, int methodId, ReadableNativeArray parameters) {
|
||||
mCatalystQueueConfiguration.getNativeModulesQueueThread().assertIsOnThread();
|
||||
|
||||
// Suppress any callbacks if destroyed - will only lead to sadness.
|
||||
if (mDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
mJavaRegistry.call(CatalystInstance.this, moduleId, methodId, parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBatchComplete() {
|
||||
mCatalystQueueConfiguration.getNativeModulesQueueThread().assertIsOnThread();
|
||||
|
||||
// The bridge may have been destroyed due to an exception during the batch. In that case
|
||||
// native modules could be in a bad state so we don't want to call anything on them. We
|
||||
// still want to trigger the debug listener since it allows instrumentation tests to end and
|
||||
// check their assertions without waiting for a timeout.
|
||||
if (!mDestroyed) {
|
||||
Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "onBatchComplete");
|
||||
try {
|
||||
mJavaRegistry.onBatchComplete();
|
||||
} finally {
|
||||
Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
|
||||
}
|
||||
}
|
||||
|
||||
decrementPendingJSCalls();
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
mNativeModuleCallExceptionHandler.handleException(e);
|
||||
mCatalystQueueConfiguration.getUIQueueThread().runOnQueue(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private class JSProfilerTraceListener implements TraceListener {
|
||||
@Override
|
||||
public void onTraceStarted() {
|
||||
getJSModule(BridgeProfiling.class).setEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTraceStopped() {
|
||||
getJSModule(BridgeProfiling.class).setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private @Nullable CatalystQueueConfigurationSpec mCatalystQueueConfigurationSpec;
|
||||
private @Nullable JSBundleLoader mJSBundleLoader;
|
||||
private @Nullable NativeModuleRegistry mRegistry;
|
||||
private @Nullable JavaScriptModulesConfig mJSModulesConfig;
|
||||
private @Nullable JavaScriptExecutor mJSExecutor;
|
||||
private @Nullable NativeModuleCallExceptionHandler mNativeModuleCallExceptionHandler;
|
||||
|
||||
public Builder setCatalystQueueConfigurationSpec(
|
||||
CatalystQueueConfigurationSpec catalystQueueConfigurationSpec) {
|
||||
mCatalystQueueConfigurationSpec = catalystQueueConfigurationSpec;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRegistry(NativeModuleRegistry registry) {
|
||||
mRegistry = registry;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setJSModulesConfig(JavaScriptModulesConfig jsModulesConfig) {
|
||||
mJSModulesConfig = jsModulesConfig;
|
||||
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 CatalystInstance build() {
|
||||
return new CatalystInstance(
|
||||
Assertions.assertNotNull(mCatalystQueueConfigurationSpec),
|
||||
Assertions.assertNotNull(mJSExecutor),
|
||||
Assertions.assertNotNull(mRegistry),
|
||||
Assertions.assertNotNull(mJSModulesConfig),
|
||||
Assertions.assertNotNull(mJSBundleLoader),
|
||||
Assertions.assertNotNull(mNativeModuleCallExceptionHandler));
|
||||
}
|
||||
}
|
||||
boolean supportsProfiling();
|
||||
void startProfiler(String title);
|
||||
void stopProfiler(String title, String filename);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,510 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
package com.facebook.react.bridge;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.react.bridge.queue.CatalystQueueConfiguration;
|
||||
import com.facebook.react.bridge.queue.CatalystQueueConfigurationImpl;
|
||||
import com.facebook.react.bridge.queue.CatalystQueueConfigurationSpec;
|
||||
import com.facebook.react.bridge.queue.QueueThreadExceptionHandler;
|
||||
import com.facebook.proguard.annotations.DoNotStrip;
|
||||
import com.facebook.react.common.ReactConstants;
|
||||
import com.facebook.react.common.annotations.VisibleForTesting;
|
||||
import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.systrace.Systrace;
|
||||
import com.facebook.systrace.TraceListener;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonFactory;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
|
||||
/**
|
||||
* This provides an implementation of the public CatalystInstance instance. It is public because
|
||||
* it is built by ReactInstanceManager which is in a different package.
|
||||
*/
|
||||
@DoNotStrip
|
||||
public class CatalystInstanceImpl implements CatalystInstance {
|
||||
|
||||
private static final int BRIDGE_SETUP_TIMEOUT_MS = 30000;
|
||||
private static final int LOAD_JS_BUNDLE_TIMEOUT_MS = 30000;
|
||||
|
||||
private static final AtomicInteger sNextInstanceIdForTrace = new AtomicInteger(1);
|
||||
|
||||
// Access from any thread
|
||||
private final CatalystQueueConfigurationImpl mCatalystQueueConfiguration;
|
||||
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;
|
||||
|
||||
// Access from native modules thread
|
||||
private final NativeModuleRegistry mJavaRegistry;
|
||||
private final NativeModuleCallExceptionHandler mNativeModuleCallExceptionHandler;
|
||||
private boolean mInitialized = false;
|
||||
|
||||
// Access from JS thread
|
||||
private @Nullable ReactBridge mBridge;
|
||||
private boolean mJSBundleHasLoaded;
|
||||
|
||||
private CatalystInstanceImpl(
|
||||
final CatalystQueueConfigurationSpec catalystQueueConfigurationSpec,
|
||||
final JavaScriptExecutor jsExecutor,
|
||||
final NativeModuleRegistry registry,
|
||||
final JavaScriptModulesConfig jsModulesConfig,
|
||||
final JSBundleLoader jsBundleLoader,
|
||||
NativeModuleCallExceptionHandler nativeModuleCallExceptionHandler) {
|
||||
mCatalystQueueConfiguration = CatalystQueueConfigurationImpl.create(
|
||||
catalystQueueConfigurationSpec,
|
||||
new NativeExceptionHandler());
|
||||
mBridgeIdleListeners = new CopyOnWriteArrayList<>();
|
||||
mJavaRegistry = registry;
|
||||
mJSModuleRegistry = new JavaScriptModuleRegistry(CatalystInstanceImpl.this, jsModulesConfig);
|
||||
mJSBundleLoader = jsBundleLoader;
|
||||
mNativeModuleCallExceptionHandler = nativeModuleCallExceptionHandler;
|
||||
mTraceListener = new JSProfilerTraceListener();
|
||||
|
||||
final CountDownLatch initLatch = new CountDownLatch(1);
|
||||
mCatalystQueueConfiguration.getJSQueueThread().runOnQueue(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
initializeBridge(jsExecutor, jsModulesConfig);
|
||||
initLatch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
Assertions.assertCondition(
|
||||
initLatch.await(BRIDGE_SETUP_TIMEOUT_MS, TimeUnit.MILLISECONDS),
|
||||
"Timed out waiting for bridge to initialize!");
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeBridge(
|
||||
JavaScriptExecutor jsExecutor,
|
||||
JavaScriptModulesConfig jsModulesConfig) {
|
||||
mCatalystQueueConfiguration.getJSQueueThread().assertIsOnThread();
|
||||
Assertions.assertCondition(mBridge == null, "initializeBridge should be called once");
|
||||
|
||||
mBridge = new ReactBridge(
|
||||
jsExecutor,
|
||||
new NativeModulesReactCallback(),
|
||||
mCatalystQueueConfiguration.getNativeModulesQueueThread());
|
||||
mBridge.setGlobalVariable(
|
||||
"__fbBatchedBridgeConfig",
|
||||
buildModulesConfigJSONProperty(mJavaRegistry, jsModulesConfig));
|
||||
Systrace.registerListener(mTraceListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void runJSBundle() {
|
||||
Systrace.beginSection(
|
||||
Systrace.TRACE_TAG_REACT_JAVA_BRIDGE,
|
||||
"CatalystInstance_runJSBundle");
|
||||
|
||||
try {
|
||||
final CountDownLatch initLatch = new CountDownLatch(1);
|
||||
mCatalystQueueConfiguration.getJSQueueThread().runOnQueue(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Assertions.assertCondition(!mJSBundleHasLoaded, "JS bundle was already loaded!");
|
||||
mJSBundleHasLoaded = true;
|
||||
|
||||
incrementPendingJSCalls();
|
||||
|
||||
try {
|
||||
mJSBundleLoader.loadScript(mBridge);
|
||||
} catch (JSExecutionException e) {
|
||||
mNativeModuleCallExceptionHandler.handleException(e);
|
||||
}
|
||||
|
||||
initLatch.countDown();
|
||||
}
|
||||
});
|
||||
Assertions.assertCondition(
|
||||
initLatch.await(LOAD_JS_BUNDLE_TIMEOUT_MS, TimeUnit.MILLISECONDS),
|
||||
"Timed out loading JS!");
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
|
||||
}
|
||||
}
|
||||
|
||||
/* package */ void callFunction(
|
||||
final int moduleId,
|
||||
final int methodId,
|
||||
final NativeArray arguments,
|
||||
final String tracingName) {
|
||||
if (mDestroyed) {
|
||||
FLog.w(ReactConstants.TAG, "Calling JS function after bridge has been destroyed.");
|
||||
return;
|
||||
}
|
||||
|
||||
incrementPendingJSCalls();
|
||||
|
||||
mCatalystQueueConfiguration.getJSQueueThread().runOnQueue(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mCatalystQueueConfiguration.getJSQueueThread().assertIsOnThread();
|
||||
|
||||
if (mDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, tracingName);
|
||||
try {
|
||||
Assertions.assertNotNull(mBridge).callFunction(moduleId, methodId, arguments);
|
||||
} finally {
|
||||
Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// This is called from java code, so it won't be stripped anyway, but proguard will rename it,
|
||||
// which this prevents.
|
||||
@DoNotStrip
|
||||
@Override
|
||||
public void invokeCallback(final int callbackID, final NativeArray arguments) {
|
||||
if (mDestroyed) {
|
||||
FLog.w(ReactConstants.TAG, "Invoking JS callback after bridge has been destroyed.");
|
||||
return;
|
||||
}
|
||||
|
||||
incrementPendingJSCalls();
|
||||
|
||||
mCatalystQueueConfiguration.getJSQueueThread().runOnQueue(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mCatalystQueueConfiguration.getJSQueueThread().assertIsOnThread();
|
||||
|
||||
if (mDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "<callback>");
|
||||
try {
|
||||
Assertions.assertNotNull(mBridge).invokeCallback(callbackID, arguments);
|
||||
} finally {
|
||||
Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys this catalyst instance, waiting for any other threads in CatalystQueueConfiguration
|
||||
* (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() {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
|
||||
if (mDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: tell all APIs to shut down
|
||||
mDestroyed = true;
|
||||
mJavaRegistry.notifyCatalystInstanceDestroy();
|
||||
mCatalystQueueConfiguration.destroy();
|
||||
boolean wasIdle = (mPendingJSCalls.getAndSet(0) == 0);
|
||||
if (!wasIdle && !mBridgeIdleListeners.isEmpty()) {
|
||||
for (NotThreadSafeBridgeIdleDebugListener listener : mBridgeIdleListeners) {
|
||||
listener.onTransitionToBridgeIdle();
|
||||
}
|
||||
}
|
||||
|
||||
if (mBridge != null) {
|
||||
Systrace.unregisterListener(mTraceListener);
|
||||
}
|
||||
|
||||
// We can access the Bridge from any thread now because we know either we are on the JS thread
|
||||
// or the JS thread has finished via CatalystQueueConfiguration#destroy()
|
||||
Assertions.assertNotNull(mBridge).dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
return mDestroyed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all the native modules
|
||||
*/
|
||||
@VisibleForTesting
|
||||
@Override
|
||||
public void initialize() {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
Assertions.assertCondition(
|
||||
!mInitialized,
|
||||
"This catalyst instance has already been initialized");
|
||||
mInitialized = true;
|
||||
mJavaRegistry.notifyCatalystInstanceInitialized();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CatalystQueueConfiguration getCatalystQueueConfiguration() {
|
||||
return mCatalystQueueConfiguration;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public @Nullable
|
||||
ReactBridge getBridge() {
|
||||
return mBridge;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends JavaScriptModule> T getJSModule(Class<T> jsInterface) {
|
||||
return Assertions.assertNotNull(mJSModuleRegistry).getJavaScriptModule(jsInterface);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends NativeModule> T getNativeModule(Class<T> nativeModuleInterface) {
|
||||
return mJavaRegistry.getModule(nativeModuleInterface);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<NativeModule> getNativeModules() {
|
||||
return mJavaRegistry.getAllModules();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* onBatchCompleted 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 boolean supportsProfiling() {
|
||||
if (mBridge == null) {
|
||||
return false;
|
||||
}
|
||||
return mBridge.supportsProfiling();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startProfiler(String title) {
|
||||
if (mBridge == null) {
|
||||
return;
|
||||
}
|
||||
mBridge.startProfiler(title);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopProfiler(String title, String filename) {
|
||||
if (mBridge == null) {
|
||||
return;
|
||||
}
|
||||
mBridge.stopProfiler(title, filename);
|
||||
}
|
||||
|
||||
private String buildModulesConfigJSONProperty(
|
||||
NativeModuleRegistry nativeModuleRegistry,
|
||||
JavaScriptModulesConfig jsModulesConfig) {
|
||||
// TODO(5300733): Serialize config using single json generator
|
||||
JsonFactory jsonFactory = new JsonFactory();
|
||||
StringWriter writer = new StringWriter();
|
||||
try {
|
||||
JsonGenerator jg = jsonFactory.createGenerator(writer);
|
||||
jg.writeStartObject();
|
||||
jg.writeFieldName("remoteModuleConfig");
|
||||
jg.writeRawValue(nativeModuleRegistry.moduleDescriptions());
|
||||
jg.writeFieldName("localModulesConfig");
|
||||
jg.writeRawValue(jsModulesConfig.moduleDescriptions());
|
||||
jg.writeEndObject();
|
||||
jg.close();
|
||||
} catch (IOException ioe) {
|
||||
throw new RuntimeException("Unable to serialize JavaScript module declaration", ioe);
|
||||
}
|
||||
return writer.getBuffer().toString();
|
||||
}
|
||||
|
||||
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()) {
|
||||
for (NotThreadSafeBridgeIdleDebugListener listener : mBridgeIdleListeners) {
|
||||
listener.onTransitionToBridgeBusy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void decrementPendingJSCalls() {
|
||||
int newPendingCalls = mPendingJSCalls.decrementAndGet();
|
||||
Assertions.assertCondition(newPendingCalls >= 0);
|
||||
boolean isNowIdle = newPendingCalls == 0;
|
||||
Systrace.traceCounter(
|
||||
Systrace.TRACE_TAG_REACT_JAVA_BRIDGE,
|
||||
mJsPendingCallsTitleForTrace,
|
||||
newPendingCalls);
|
||||
|
||||
if (isNowIdle && !mBridgeIdleListeners.isEmpty()) {
|
||||
for (NotThreadSafeBridgeIdleDebugListener listener : mBridgeIdleListeners) {
|
||||
listener.onTransitionToBridgeIdle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class NativeModulesReactCallback implements ReactCallback {
|
||||
|
||||
@Override
|
||||
public void call(int moduleId, int methodId, ReadableNativeArray parameters) {
|
||||
mCatalystQueueConfiguration.getNativeModulesQueueThread().assertIsOnThread();
|
||||
|
||||
// Suppress any callbacks if destroyed - will only lead to sadness.
|
||||
if (mDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
mJavaRegistry.call(CatalystInstanceImpl.this, moduleId, methodId, parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBatchComplete() {
|
||||
mCatalystQueueConfiguration.getNativeModulesQueueThread().assertIsOnThread();
|
||||
|
||||
// The bridge may have been destroyed due to an exception during the batch. In that case
|
||||
// native modules could be in a bad state so we don't want to call anything on them. We
|
||||
// still want to trigger the debug listener since it allows instrumentation tests to end and
|
||||
// check their assertions without waiting for a timeout.
|
||||
if (!mDestroyed) {
|
||||
Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "onBatchComplete");
|
||||
try {
|
||||
mJavaRegistry.onBatchComplete();
|
||||
} finally {
|
||||
Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
|
||||
}
|
||||
}
|
||||
|
||||
decrementPendingJSCalls();
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
mNativeModuleCallExceptionHandler.handleException(e);
|
||||
mCatalystQueueConfiguration.getUIQueueThread().runOnQueue(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private class JSProfilerTraceListener implements TraceListener {
|
||||
@Override
|
||||
public void onTraceStarted() {
|
||||
getJSModule(BridgeProfiling.class).setEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTraceStopped() {
|
||||
getJSModule(BridgeProfiling.class).setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private @Nullable CatalystQueueConfigurationSpec mCatalystQueueConfigurationSpec;
|
||||
private @Nullable JSBundleLoader mJSBundleLoader;
|
||||
private @Nullable NativeModuleRegistry mRegistry;
|
||||
private @Nullable JavaScriptModulesConfig mJSModulesConfig;
|
||||
private @Nullable JavaScriptExecutor mJSExecutor;
|
||||
private @Nullable NativeModuleCallExceptionHandler mNativeModuleCallExceptionHandler;
|
||||
|
||||
public Builder setCatalystQueueConfigurationSpec(
|
||||
CatalystQueueConfigurationSpec catalystQueueConfigurationSpec) {
|
||||
mCatalystQueueConfigurationSpec = catalystQueueConfigurationSpec;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRegistry(NativeModuleRegistry registry) {
|
||||
mRegistry = registry;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setJSModulesConfig(JavaScriptModulesConfig jsModulesConfig) {
|
||||
mJSModulesConfig = jsModulesConfig;
|
||||
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(mCatalystQueueConfigurationSpec),
|
||||
Assertions.assertNotNull(mJSExecutor),
|
||||
Assertions.assertNotNull(mRegistry),
|
||||
Assertions.assertNotNull(mJSModulesConfig),
|
||||
Assertions.assertNotNull(mJSBundleLoader),
|
||||
Assertions.assertNotNull(mNativeModuleCallExceptionHandler));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -30,7 +30,7 @@ import com.facebook.infer.annotation.Assertions;
|
|||
private final HashMap<Class<? extends JavaScriptModule>, JavaScriptModule> mModuleInstances;
|
||||
|
||||
public JavaScriptModuleRegistry(
|
||||
CatalystInstance instance,
|
||||
CatalystInstanceImpl instance,
|
||||
JavaScriptModulesConfig config) {
|
||||
mModuleInstances = new HashMap<>();
|
||||
for (JavaScriptModuleRegistration registration : config.getModuleDefinitions()) {
|
||||
|
@ -52,11 +52,11 @@ import com.facebook.infer.annotation.Assertions;
|
|||
|
||||
private static class JavaScriptModuleInvocationHandler implements InvocationHandler {
|
||||
|
||||
private final CatalystInstance mCatalystInstance;
|
||||
private final CatalystInstanceImpl mCatalystInstance;
|
||||
private final JavaScriptModuleRegistration mModuleRegistration;
|
||||
|
||||
public JavaScriptModuleInvocationHandler(
|
||||
CatalystInstance catalystInstance,
|
||||
CatalystInstanceImpl catalystInstance,
|
||||
JavaScriptModuleRegistration moduleRegistration) {
|
||||
mCatalystInstance = catalystInstance;
|
||||
mModuleRegistration = moduleRegistration;
|
||||
|
|
|
@ -9,12 +9,6 @@
|
|||
|
||||
package com.facebook.react.bridge.queue;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import android.os.Looper;
|
||||
|
||||
import com.facebook.react.common.MapBuilder;
|
||||
|
||||
/**
|
||||
* Specifies which {@link MessageQueueThread}s must be used to run the various contexts of
|
||||
* execution within catalyst (Main UI thread, native modules, and JS). Some of these queues *may* be
|
||||
|
@ -24,67 +18,8 @@ import com.facebook.react.common.MapBuilder;
|
|||
* Native Modules Queue Thread: The thread and Looper that native modules are invoked on.
|
||||
* JS Queue Thread: The thread and Looper that JS is executed on.
|
||||
*/
|
||||
public class CatalystQueueConfiguration {
|
||||
|
||||
private final MessageQueueThread mUIQueueThread;
|
||||
private final MessageQueueThread mNativeModulesQueueThread;
|
||||
private final MessageQueueThread mJSQueueThread;
|
||||
|
||||
private CatalystQueueConfiguration(
|
||||
MessageQueueThread uiQueueThread,
|
||||
MessageQueueThread nativeModulesQueueThread,
|
||||
MessageQueueThread jsQueueThread) {
|
||||
mUIQueueThread = uiQueueThread;
|
||||
mNativeModulesQueueThread = nativeModulesQueueThread;
|
||||
mJSQueueThread = jsQueueThread;
|
||||
}
|
||||
|
||||
public MessageQueueThread getUIQueueThread() {
|
||||
return mUIQueueThread;
|
||||
}
|
||||
|
||||
public MessageQueueThread getNativeModulesQueueThread() {
|
||||
return mNativeModulesQueueThread;
|
||||
}
|
||||
|
||||
public MessageQueueThread getJSQueueThread() {
|
||||
return mJSQueueThread;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when the corresponding {@link com.facebook.react.bridge.CatalystInstance}
|
||||
* is destroyed so that we shut down the proper queue threads.
|
||||
*/
|
||||
public void destroy() {
|
||||
if (mNativeModulesQueueThread.getLooper() != Looper.getMainLooper()) {
|
||||
mNativeModulesQueueThread.quitSynchronous();
|
||||
}
|
||||
if (mJSQueueThread.getLooper() != Looper.getMainLooper()) {
|
||||
mJSQueueThread.quitSynchronous();
|
||||
}
|
||||
}
|
||||
|
||||
public static CatalystQueueConfiguration create(
|
||||
CatalystQueueConfigurationSpec spec,
|
||||
QueueThreadExceptionHandler exceptionHandler) {
|
||||
Map<MessageQueueThreadSpec, MessageQueueThread> specsToThreads = MapBuilder.newHashMap();
|
||||
|
||||
MessageQueueThreadSpec uiThreadSpec = MessageQueueThreadSpec.mainThreadSpec();
|
||||
MessageQueueThread uiThread = MessageQueueThread.create( uiThreadSpec, exceptionHandler);
|
||||
specsToThreads.put(uiThreadSpec, uiThread);
|
||||
|
||||
MessageQueueThread jsThread = specsToThreads.get(spec.getJSQueueThreadSpec());
|
||||
if (jsThread == null) {
|
||||
jsThread = MessageQueueThread.create(spec.getJSQueueThreadSpec(), exceptionHandler);
|
||||
}
|
||||
|
||||
MessageQueueThread nativeModulesThread =
|
||||
specsToThreads.get(spec.getNativeModulesQueueThreadSpec());
|
||||
if (nativeModulesThread == null) {
|
||||
nativeModulesThread =
|
||||
MessageQueueThread.create(spec.getNativeModulesQueueThreadSpec(), exceptionHandler);
|
||||
}
|
||||
|
||||
return new CatalystQueueConfiguration(uiThread, nativeModulesThread, jsThread);
|
||||
}
|
||||
public interface CatalystQueueConfiguration {
|
||||
MessageQueueThread getUIQueueThread();
|
||||
MessageQueueThread getNativeModulesQueueThread();
|
||||
MessageQueueThread getJSQueueThread();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
package com.facebook.react.bridge.queue;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import android.os.Looper;
|
||||
|
||||
import com.facebook.react.common.MapBuilder;
|
||||
|
||||
public class CatalystQueueConfigurationImpl implements CatalystQueueConfiguration {
|
||||
|
||||
private final MessageQueueThreadImpl mUIQueueThread;
|
||||
private final MessageQueueThreadImpl mNativeModulesQueueThread;
|
||||
private final MessageQueueThreadImpl mJSQueueThread;
|
||||
|
||||
private CatalystQueueConfigurationImpl(
|
||||
MessageQueueThreadImpl uiQueueThread,
|
||||
MessageQueueThreadImpl nativeModulesQueueThread,
|
||||
MessageQueueThreadImpl jsQueueThread) {
|
||||
mUIQueueThread = uiQueueThread;
|
||||
mNativeModulesQueueThread = nativeModulesQueueThread;
|
||||
mJSQueueThread = jsQueueThread;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageQueueThread getUIQueueThread() {
|
||||
return mUIQueueThread;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageQueueThread getNativeModulesQueueThread() {
|
||||
return mNativeModulesQueueThread;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageQueueThread getJSQueueThread() {
|
||||
return mJSQueueThread;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when the corresponding {@link com.facebook.react.bridge.CatalystInstance}
|
||||
* is destroyed so that we shut down the proper queue threads.
|
||||
*/
|
||||
public void destroy() {
|
||||
if (mNativeModulesQueueThread.getLooper() != Looper.getMainLooper()) {
|
||||
mNativeModulesQueueThread.quitSynchronous();
|
||||
}
|
||||
if (mJSQueueThread.getLooper() != Looper.getMainLooper()) {
|
||||
mJSQueueThread.quitSynchronous();
|
||||
}
|
||||
}
|
||||
|
||||
public static CatalystQueueConfigurationImpl create(
|
||||
CatalystQueueConfigurationSpec spec,
|
||||
QueueThreadExceptionHandler exceptionHandler) {
|
||||
Map<MessageQueueThreadSpec, MessageQueueThreadImpl> specsToThreads = MapBuilder.newHashMap();
|
||||
|
||||
MessageQueueThreadSpec uiThreadSpec = MessageQueueThreadSpec.mainThreadSpec();
|
||||
MessageQueueThreadImpl uiThread =
|
||||
MessageQueueThreadImpl.create( uiThreadSpec, exceptionHandler);
|
||||
specsToThreads.put(uiThreadSpec, uiThread);
|
||||
|
||||
MessageQueueThreadImpl jsThread = specsToThreads.get(spec.getJSQueueThreadSpec());
|
||||
if (jsThread == null) {
|
||||
jsThread = MessageQueueThreadImpl.create(spec.getJSQueueThreadSpec(), exceptionHandler);
|
||||
}
|
||||
|
||||
MessageQueueThreadImpl nativeModulesThread =
|
||||
specsToThreads.get(spec.getNativeModulesQueueThreadSpec());
|
||||
if (nativeModulesThread == null) {
|
||||
nativeModulesThread =
|
||||
MessageQueueThreadImpl.create(spec.getNativeModulesQueueThreadSpec(), exceptionHandler);
|
||||
}
|
||||
|
||||
return new CatalystQueueConfigurationImpl(uiThread, nativeModulesThread, jsThread);
|
||||
}
|
||||
}
|
|
@ -9,136 +9,28 @@
|
|||
|
||||
package com.facebook.react.bridge.queue;
|
||||
|
||||
import android.os.Looper;
|
||||
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.proguard.annotations.DoNotStrip;
|
||||
import com.facebook.react.bridge.AssertionException;
|
||||
import com.facebook.react.bridge.SoftAssertions;
|
||||
import com.facebook.react.common.ReactConstants;
|
||||
import com.facebook.react.common.futures.SimpleSettableFuture;
|
||||
|
||||
/**
|
||||
* Encapsulates a Thread that has a {@link Looper} running on it that can accept Runnables.
|
||||
* Encapsulates a Thread that can accept Runnables.
|
||||
*/
|
||||
@DoNotStrip
|
||||
public class MessageQueueThread {
|
||||
|
||||
private final String mName;
|
||||
private final Looper mLooper;
|
||||
private final MessageQueueThreadHandler mHandler;
|
||||
private final String mAssertionErrorMessage;
|
||||
private volatile boolean mIsFinished = false;
|
||||
|
||||
private MessageQueueThread(
|
||||
String name,
|
||||
Looper looper,
|
||||
QueueThreadExceptionHandler exceptionHandler) {
|
||||
mName = name;
|
||||
mLooper = looper;
|
||||
mHandler = new MessageQueueThreadHandler(looper, exceptionHandler);
|
||||
mAssertionErrorMessage = "Expected to be called from the '" + getName() + "' thread!";
|
||||
}
|
||||
|
||||
public interface MessageQueueThread {
|
||||
/**
|
||||
* Runs the given Runnable on this Thread. It will be submitted to the end of the event queue even
|
||||
* if it is being submitted from the same queue Thread.
|
||||
*/
|
||||
@DoNotStrip
|
||||
public void runOnQueue(Runnable runnable) {
|
||||
if (mIsFinished) {
|
||||
FLog.w(
|
||||
ReactConstants.TAG,
|
||||
"Tried to enqueue runnable on already finished thread: '" + getName() +
|
||||
"... dropping Runnable.");
|
||||
}
|
||||
mHandler.post(runnable);
|
||||
}
|
||||
void runOnQueue(Runnable runnable);
|
||||
|
||||
/**
|
||||
* @return whether the current Thread is also the Thread associated with this MessageQueueThread.
|
||||
*/
|
||||
public boolean isOnThread() {
|
||||
return mLooper.getThread() == Thread.currentThread();
|
||||
}
|
||||
boolean isOnThread();
|
||||
|
||||
/**
|
||||
* Asserts {@link #isOnThread()}, throwing a {@link AssertionException} (NOT an
|
||||
* {@link AssertionError}) if the assertion fails.
|
||||
*/
|
||||
public void assertIsOnThread() {
|
||||
SoftAssertions.assertCondition(isOnThread(), mAssertionErrorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Quits this queue's Looper. If that Looper was running on a different Thread than the current
|
||||
* Thread, also waits for the last message being processed to finish and the Thread to die.
|
||||
*/
|
||||
public void quitSynchronous() {
|
||||
mIsFinished = true;
|
||||
mLooper.quit();
|
||||
if (mLooper.getThread() != Thread.currentThread()) {
|
||||
try {
|
||||
mLooper.getThread().join();
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException("Got interrupted waiting to join thread " + mName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Looper getLooper() {
|
||||
return mLooper;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return mName;
|
||||
}
|
||||
|
||||
public static MessageQueueThread create(
|
||||
MessageQueueThreadSpec spec,
|
||||
QueueThreadExceptionHandler exceptionHandler) {
|
||||
switch (spec.getThreadType()) {
|
||||
case MAIN_UI:
|
||||
return createForMainThread(spec.getName(), exceptionHandler);
|
||||
case NEW_BACKGROUND:
|
||||
return startNewBackgroundThread(spec.getName(), exceptionHandler);
|
||||
default:
|
||||
throw new RuntimeException("Unknown thread type: " + spec.getThreadType());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a MessageQueueThread corresponding to Android's main UI thread.
|
||||
*/
|
||||
private static MessageQueueThread createForMainThread(
|
||||
String name,
|
||||
QueueThreadExceptionHandler exceptionHandler) {
|
||||
Looper mainLooper = Looper.getMainLooper();
|
||||
return new MessageQueueThread(name, mainLooper, exceptionHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and starts a new MessageQueueThread encapsulating a new Thread with a new Looper
|
||||
* running on it. Give it a name for easier debugging. When this method exits, the new
|
||||
* MessageQueueThread is ready to receive events.
|
||||
*/
|
||||
private static MessageQueueThread startNewBackgroundThread(
|
||||
String name,
|
||||
QueueThreadExceptionHandler exceptionHandler) {
|
||||
final SimpleSettableFuture<Looper> simpleSettableFuture = new SimpleSettableFuture<>();
|
||||
Thread bgThread = new Thread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Looper.prepare();
|
||||
|
||||
simpleSettableFuture.set(Looper.myLooper());
|
||||
|
||||
Looper.loop();
|
||||
}
|
||||
}, "mqt_" + name);
|
||||
bgThread.start();
|
||||
|
||||
return new MessageQueueThread(name, simpleSettableFuture.get(5000), exceptionHandler);
|
||||
}
|
||||
void assertIsOnThread();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
package com.facebook.react.bridge.queue;
|
||||
|
||||
import android.os.Looper;
|
||||
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.proguard.annotations.DoNotStrip;
|
||||
import com.facebook.react.bridge.AssertionException;
|
||||
import com.facebook.react.bridge.SoftAssertions;
|
||||
import com.facebook.react.common.ReactConstants;
|
||||
import com.facebook.react.common.futures.SimpleSettableFuture;
|
||||
|
||||
/**
|
||||
* Encapsulates a Thread that has a {@link Looper} running on it that can accept Runnables.
|
||||
*/
|
||||
@DoNotStrip
|
||||
/* package */ class MessageQueueThreadImpl implements MessageQueueThread {
|
||||
|
||||
private final String mName;
|
||||
private final Looper mLooper;
|
||||
private final MessageQueueThreadHandler mHandler;
|
||||
private final String mAssertionErrorMessage;
|
||||
private volatile boolean mIsFinished = false;
|
||||
|
||||
private MessageQueueThreadImpl(
|
||||
String name,
|
||||
Looper looper,
|
||||
QueueThreadExceptionHandler exceptionHandler) {
|
||||
mName = name;
|
||||
mLooper = looper;
|
||||
mHandler = new MessageQueueThreadHandler(looper, exceptionHandler);
|
||||
mAssertionErrorMessage = "Expected to be called from the '" + getName() + "' thread!";
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the given Runnable on this Thread. It will be submitted to the end of the event queue even
|
||||
* if it is being submitted from the same queue Thread.
|
||||
*/
|
||||
@DoNotStrip
|
||||
public void runOnQueue(Runnable runnable) {
|
||||
if (mIsFinished) {
|
||||
FLog.w(
|
||||
ReactConstants.TAG,
|
||||
"Tried to enqueue runnable on already finished thread: '" + getName() +
|
||||
"... dropping Runnable.");
|
||||
}
|
||||
mHandler.post(runnable);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether the current Thread is also the Thread associated with this MessageQueueThread.
|
||||
*/
|
||||
public boolean isOnThread() {
|
||||
return mLooper.getThread() == Thread.currentThread();
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts {@link #isOnThread()}, throwing a {@link AssertionException} (NOT an
|
||||
* {@link AssertionError}) if the assertion fails.
|
||||
*/
|
||||
public void assertIsOnThread() {
|
||||
SoftAssertions.assertCondition(isOnThread(), mAssertionErrorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Quits this queue's Looper. If that Looper was running on a different Thread than the current
|
||||
* Thread, also waits for the last message being processed to finish and the Thread to die.
|
||||
*/
|
||||
public void quitSynchronous() {
|
||||
mIsFinished = true;
|
||||
mLooper.quit();
|
||||
if (mLooper.getThread() != Thread.currentThread()) {
|
||||
try {
|
||||
mLooper.getThread().join();
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException("Got interrupted waiting to join thread " + mName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Looper getLooper() {
|
||||
return mLooper;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return mName;
|
||||
}
|
||||
|
||||
public static MessageQueueThreadImpl create(
|
||||
MessageQueueThreadSpec spec,
|
||||
QueueThreadExceptionHandler exceptionHandler) {
|
||||
switch (spec.getThreadType()) {
|
||||
case MAIN_UI:
|
||||
return createForMainThread(spec.getName(), exceptionHandler);
|
||||
case NEW_BACKGROUND:
|
||||
return startNewBackgroundThread(spec.getName(), exceptionHandler);
|
||||
default:
|
||||
throw new RuntimeException("Unknown thread type: " + spec.getThreadType());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a MessageQueueThreadImpl corresponding to Android's main UI thread.
|
||||
*/
|
||||
private static MessageQueueThreadImpl createForMainThread(
|
||||
String name,
|
||||
QueueThreadExceptionHandler exceptionHandler) {
|
||||
Looper mainLooper = Looper.getMainLooper();
|
||||
return new MessageQueueThreadImpl(name, mainLooper, exceptionHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and starts a new MessageQueueThreadImpl encapsulating a new Thread with a new Looper
|
||||
* running on it. Give it a name for easier debugging. When this method exits, the new
|
||||
* MessageQueueThreadImpl is ready to receive events.
|
||||
*/
|
||||
private static MessageQueueThreadImpl startNewBackgroundThread(
|
||||
String name,
|
||||
QueueThreadExceptionHandler exceptionHandler) {
|
||||
final SimpleSettableFuture<Looper> simpleSettableFuture = new SimpleSettableFuture<>();
|
||||
Thread bgThread = new Thread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Looper.prepare();
|
||||
|
||||
simpleSettableFuture.set(Looper.myLooper());
|
||||
|
||||
Looper.loop();
|
||||
}
|
||||
}, "mqt_" + name);
|
||||
bgThread.start();
|
||||
|
||||
return new MessageQueueThreadImpl(name, simpleSettableFuture.get(5000), exceptionHandler);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue