mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-02-07 17:27:56 +08:00
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
511 lines
17 KiB
Java
511 lines
17 KiB
Java
/**
|
|
* 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));
|
|
}
|
|
}
|
|
}
|