Mobile Development 14 min read

How React Native Bridges Java and JavaScript on Android: A Deep Dive

This article provides a comprehensive source‑code analysis of React Native's Java↔JavaScript communication mechanism on Android, detailing module registration, the Java‑to‑JS and JS‑to‑Java call flows, and the underlying native bridge implementation.

Tencent TDS Service
Tencent TDS Service
Tencent TDS Service
How React Native Bridges Java and JavaScript on Android: A Deep Dive

This article analyzes the Java↔JS communication mechanism in React Native for Android (Release 20) from a source‑code perspective.

Module Registry

When a React Native Android app starts, ReactPackage generates two module registries: NativeModuleRegistry and JavaScriptModuleRegistry, containing system and custom modules. Both Java and JS sides share identical registries, and modules are identified as native or JS by implementing the appropriate interfaces and adding instances via CreactXXModules in the ReactPackage.

CoreModulesPackage.java

@Override
public List<NativeModule> createNativeModules(
    ReactApplicationContext catalystApplicationContext) {
  return Arrays.<NativeModule>asList(
    new AndroidInfoModule(),
    new DeviceEventManagerModule(catalystApplicationContext, mHardwareBackBtnHandler),
    new DebugComponentOwnershipModule(catalystApplicationContext));
}

@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
  return Arrays.asList(
    DeviceEventManagerModule.RCTDeviceEventEmitter.class,
    JSTimersExecution.class,
    RCTEventEmitter.class,
    RCTNativeAppEventEmitter.class,
    AppRegistry.class);
}

@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
  return new ArrayList<>(0);
}

JS modules extend JavaScriptModule and are mapped to corresponding JS files via dynamic proxies. For example, AppRegistry.java is the entry point that launches the React application after the JS bundle loads.

AppRegistry.java

public interface AppRegistry extends JavaScriptModule {
  void runApplication(String appKey, WritableMap appParameters);
  void unmountApplicationComponentAtRootTag(int rootNodeTag);
}

Java modules extend BaseJavaModule. By overriding getName and getConstants, and annotating methods with @ReactMethod, they become callable from JS. All Java‑side module interfaces are exposed through NativeModuleRegistry.

AndroidInfoModule.java

public class AndroidInfoModule extends BaseJavaModule {

  @Override
  public String getName() {
    return "AndroidConstants";
  }

  @Override
  public @Nullable Map<String, Object> getConstants() {
    HashMap<String, Object> constants = new HashMap<String, Object>();
    constants.put("Version", Build.VERSION.SDK_INT);
    return constants;
  }
}

Java → JS

The communication flow consists of five steps:

CatalystInstanceImpl acts as the high‑level wrapper for JS↔Java calls; business modules communicate indirectly via ReactInstanceManager and CatalystInstanceImpl.

Java‑side calls are broken into moduleID, methodID, and params, which are handled by JavaScriptModuleInvocationHandler through dynamic proxies.

The handler forwards the data to the native bridge via ReactBridge (JNI). ReactBridge passes the call to the C++ layer.

Finally, JSCHelper.evaluateScript uses JSC to deliver the call to the JS side.

JavaScriptModuleInvocationHandler.java

private static class JavaScriptModuleInvocationHandler implements InvocationHandler {
  private final CatalystInstanceImpl mCatalystInstance;
  private final JavaScriptModuleRegistration mModuleRegistration;

  public JavaScriptModuleInvocationHandler(CatalystInstanceImpl catalystInstance,
                                         JavaScriptModuleRegistration moduleRegistration) {
    mCatalystInstance = catalystInstance;
    mModuleRegistration = moduleRegistration;
  }

  @Override
  public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    String tracingName = mModuleRegistration.getTracingName(method);
    mCatalystInstance.callFunction(
      mModuleRegistration.getModuleId(),
      mModuleRegistration.getMethodId(method),
      Arguments.fromJavaArgs(args),
      tracingName);
    return null;
  }
}
CatalystInstanceImpl

creates the bridge and registers global variables. The native bridge implements several JNI methods such as loadScriptFromAssets, callFunction, and invokeCallback.

ReactBridge.java

public native void loadScriptFromAssets(AssetManager assetManager, String assetName);
public native void loadScriptFromFile(@Nullable String fileName, @Nullable String sourceURL);
public native void callFunction(int moduleId, int methodId, NativeArray arguments);
public native void invokeCallback(int callbackID, NativeArray arguments);
public native void setGlobalVariable(String propertyName, String jsonEncodedArgument);
public native boolean supportsProfiling();
public native void startProfiler(String title);
public native void stopProfiler(String title, String filename);
private native void handleMemoryPressureModerate();
private native void handleMemoryPressureCritical();

The C++ side entry point OnLoad.cpp forwards calls to Bridge.cpp, which ultimately invokes the JavaScript executor.

OnLoad.cpp

static void callFunction(JNIEnv* env, jobject obj, jint moduleId, jint methodId,
                         NativeArray::jhybridobject args) {
  auto bridge = extractRefPtr<Bridge>(env, obj);
  auto arguments = cthis(wrap_alias(args));
  try {
    bridge->callFunction((double)moduleId, (double)methodId, std::move(arguments->array));
  } catch (...) {
    translatePendingCppExceptionToJavaException();
  }
}

JS → Java

The reverse flow also follows five steps:

JS calls an exposed Java API.

The call is split into moduleID, methodID, and params and pushed into the JS MessageQueue.

When the native side processes the queue, the Java‑to‑JS chain described above is executed.

After callFunctionReturnFlushedQueue runs, the flushed queue is returned.

The Java side’s JavaRegistry looks up the module configuration and invokes the appropriate method.

MessageQueue.js

__nativeCall(module, method, params, onFail, onSucc) {
  if (onFail || onSucc) {
    (this._callbackID > (1 << 5)) && (this._debugInfo[this._callbackID >> 5] = null);
    this._debugInfo[this._callbackID >> 1] = [module, method];
    onFail && params.push(this._callbackID);
    this._callbacks[this._callbackID++] = onFail;
    onSucc && params.push(this._callbackID);
    this._callbacks[this._callbackID++] = onSucc;
  }
  this._queue[MODULE_IDS].push(module);
  this._queue[METHOD_IDS].push(method);
  this._queue[PARAMS].push(params);
  var now = new Date().getTime();
  if (global.nativeFlushQueueImmediate && now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS) {
    global.nativeFlushQueueImmediate(this._queue);
    this._queue = [[], [], [], this._callID];
    this._lastFlush = now;
  }
  Systrace.counterEvent('pending_js_to_native_queue', this._queue[0].length);
}

callFunctionReturnFlushedQueue(module, method, args) {
  guard(() => {
    this.__callFunction(module, method, args);
    this.__callImmediates();
  });
  return this.flushedQueue();
}

flushedQueue() {
  this.__callImmediates();
  let queue = this._queue;
  this._queue = [[], [], [], this._callID];
  return queue[0].length ? queue : null;
}

When the native side receives the queued call information, it looks up the corresponding Java module in JavaRegistry and invokes the method via reflection.

BaseJavaModule.java

@Override
public void invoke(CatalystInstance catalystInstance, ReadableNativeArray parameters) {
  Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "callJavaModuleMethod");
  try {
    mMethod.invoke(BaseJavaModule.this, mArguments);
  } catch (IllegalArgumentException ie) {
    // handle error
  } finally {
    Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
  }
}

The described mechanism offers higher communication efficiency compared to traditional hybrid approaches, and the same bridge also supports view mapping such as <text/> to TextView, which will be explored in a future article.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

AndroidReact NativeNative ModulesMessageQueueJavaScript Bridge
Tencent TDS Service
Written by

Tencent TDS Service

TDS Service offers client and web front‑end developers and operators an intelligent low‑code platform, cross‑platform development framework, universal release platform, runtime container engine, monitoring and analysis platform, and a security‑privacy compliance suite.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.