Files
react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstance.java
Martin Konicek 42eb5464fd Release React Native for Android
This is an early release and there are several things that are known
not to work if you're porting your iOS app to Android.

See the Known Issues guide on the website.

We will work with the community to reach platform parity with iOS.
2015-09-14 18:13:39 +01:00

420 lines
15 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.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.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
/**
* A higher level API on top of the asynchronous JSC bridge. This provides an
* environment allowing the invocation of JavaScript methods and lets a set of
* Java APIs be invokable from JavaScript as well.
*/
@DoNotStrip
public class CatalystInstance {
private static final int BRIDGE_SETUP_TIMEOUT_MS = 15000;
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;
// 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 @Nullable JavaScriptModuleRegistry mJSModuleRegistry;
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<NotThreadSafeBridgeIdleDebugListener>();
mJavaRegistry = registry;
mNativeModuleCallExceptionHandler = nativeModuleCallExceptionHandler;
final CountDownLatch initLatch = new CountDownLatch(1);
mCatalystQueueConfiguration.getJSQueueThread().runOnQueue(
new Runnable() {
@Override
public void run() {
initializeBridge(jsExecutor, registry, jsModulesConfig, jsBundleLoader);
mJSModuleRegistry =
new JavaScriptModuleRegistry(CatalystInstance.this, 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,
NativeModuleRegistry registry,
JavaScriptModulesConfig jsModulesConfig,
JSBundleLoader jsBundleLoader) {
mCatalystQueueConfiguration.getJSQueueThread().assertIsOnThread();
Assertions.assertCondition(mBridge == null, "initializeBridge should be called once");
mBridge = new ReactBridge(
jsExecutor,
new NativeModulesReactCallback(),
mCatalystQueueConfiguration.getNativeModulesQueueThread());
mBridge.setGlobalVariable(
"__fbBatchedBridgeConfig",
buildModulesConfigJSONProperty(registry, jsModulesConfig));
jsBundleLoader.loadScript(mBridge);
}
/* 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
/* 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);
}
}
});
}
/**
* 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();
}
}
// 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;
}
/**
* 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();
}
public CatalystQueueConfiguration getCatalystQueueConfiguration() {
return mCatalystQueueConfiguration;
}
@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();
}
/**
* 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.
*/
public void addBridgeIdleDebugListener(NotThreadSafeBridgeIdleDebugListener listener) {
mBridgeIdleListeners.add(listener);
}
/**
* Removes a NotThreadSafeBridgeIdleDebugListener previously added with
* {@link #addBridgeIdleDebugListener}
*/
public void removeBridgeIdleDebugListener(NotThreadSafeBridgeIdleDebugListener listener) {
mBridgeIdleListeners.remove(listener);
}
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();
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();
}
});
}
}
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));
}
}
}