100、React Native与Android混合开发:RN集成、Native Module、Bridge通信

说实话,React Native 和 Android 的混合开发,是很多团队绕不开的坎。

我记得刚带项目那会儿,产品经理一拍脑袋说“我们要在现有 App 里嵌入 RN 页面”。我当时心里咯噔一下——不是技术难,而是坑多。但做下来之后发现,只要把集成流程、Native Module 和 Bridge 通信这三块理清楚,其实挺顺的。

今天我就把这套东西掰开揉碎讲给你听。

一、为什么要在 Android 里集成 React Native?

说白了,就是“既要又要”。

  • 原生性能:核心页面、复杂动画、硬件交互,还得靠 Android 原生。
  • 动态更新:业务页面频繁改版,用 RN 可以热更新,不用等发版。
  • 跨平台复用:一套 RN 代码,iOS 和 Android 都能跑,节省人力。

但代价也很明显——你得维护两套通信机制,还要处理生命周期冲突。嗯,这里要注意,不是所有页面都适合 RN。

二、RN 集成到 Android 项目的完整流程

我习惯把集成分成三步:环境准备、依赖注入、页面加载。

2.1 环境准备

首先,你的 Android 项目得支持 React Native。核心依赖就一个:

// app/build.gradle
dependencies {
    implementation "com.facebook.react:react-native:0.73.4"
    // 如果用了 hermes 引擎
    implementation "com.facebook.react:hermes-engine:0.73.4"
}

然后在 settings.gradle 里加上 Maven 仓库:

maven {
    url "$rootDir/../node_modules/react-native/android"
}

别忘了,你的项目根目录下要有 package.jsonnode_modules。我一般用 npx react-native init 初始化一个 RN 项目,然后把 Android 部分拷过来。

2.2 创建 React Native 页面

在 Android 里,RN 页面本质上是一个 ReactRootView。你可以在 Activity 或 Fragment 里加载它:

// MyReactActivity.java
public class MyReactActivity extends AppCompatActivity {
    private ReactRootView mReactRootView;
    private ReactInstanceManager mReactInstanceManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mReactInstanceManager = ReactInstanceManager.builder()
                .setApplication(getApplication())
                .setCurrentActivity(this)
                .setBundleAssetName("index.android.bundle")
                .setJSMainModulePath("index")
                .addPackage(new MainReactPackage())
                .setUseDeveloperSupport(BuildConfig.DEBUG)
                .setInitialLifecycleState(LifecycleState.RESUMED)
                .build();

        mReactRootView = new ReactRootView(this);
        mReactRootView.startReactApplication(
                mReactInstanceManager,
                "MyReactNativeApp",  // 对应 JS 端的 AppRegistry.registerComponent
                null
        );

        setContentView(mReactRootView);
    }
}

JS 端只需要注册一个组件:

// index.js
import { AppRegistry } from 'react-native';
import App from './App';

AppRegistry.registerComponent('MyReactNativeApp', () => App);
⚠️ 注意ReactInstanceManager 是重量级对象,建议做成单例,不要每个 Activity 都 new 一个。我曾经在项目里踩过这个坑,内存直接飙到 200MB+。

2.3 加载离线 Bundle

生产环境不能走 Metro 开发服务器。你需要把 JS Bundle 打包成 index.android.bundle,放到 assets 目录下:

react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res/

然后在 ReactInstanceManager 里指定 setBundleAssetName("index.android.bundle")

三、Native Module:让 JS 调用原生能力

RN 能调用的原生 API 有限。如果你想用蓝牙、指纹、或者自定义硬件,就得自己写 Native Module。

我举个例子:获取设备唯一标识(不是简单的 IMEI,Android 10 以后拿不到)。

3.1 创建 Native Module 类

// DeviceInfoModule.java
public class DeviceInfoModule extends ReactContextBaseJavaModule {

    public DeviceInfoModule(ReactApplicationContext reactContext) {
        super(reactContext);
    }

    @Override
    public String getName() {
        return "DeviceInfoModule";  // JS 端通过这个名称调用
    }

    @ReactMethod
    public void getDeviceId(Promise promise) {
        try {
            String deviceId = Settings.Secure.getString(
                getReactApplicationContext().getContentResolver(),
                Settings.Secure.ANDROID_ID
            );
            promise.resolve(deviceId);
        } catch (Exception e) {
            promise.reject("DEVICE_ID_ERROR", e.getMessage());
        }
    }
}

3.2 注册 Package

// DeviceInfoPackage.java
public class DeviceInfoPackage implements ReactPackage {
    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        List<NativeModule> modules = new ArrayList<>();
        modules.add(new DeviceInfoModule(reactContext));
        return modules;
    }

    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }
}

然后在 ReactInstanceManageraddPackage 里加上:

.addPackage(new DeviceInfoPackage())

3.3 JS 端调用

import { NativeModules } from 'react-native';

const { DeviceInfoModule } = NativeModules;

DeviceInfoModule.getDeviceId()
  .then(id => console.log('Device ID:', id))
  .catch(err => console.error(err));
💡 技巧@ReactMethod 注解的方法默认跑在 JS 线程。如果你要做耗时操作(比如文件读写),记得用 AsyncTaskHandler 切到子线程,否则会卡 UI。

四、Bridge 通信:双向数据流

Bridge 是 RN 和原生之间的“翻译官”。它负责把 JS 的调用转成原生方法,再把结果传回去。

我画了一张图,帮你理解整个通信链路:

JavaScript 层 React 组件 / JS 业务逻辑 调用 NativeModules.xxx 接收回调 / Promise 原生层 (Android) Native Module 实现 @ReactMethod 注解方法 返回 Promise / 发送事件 Bridge 桥接层 序列化 / 反序列化 · 消息队列 · 线程切换 JS 调用 → Bridge 序列化 → 原生执行 → Bridge 返回结果 → JS 回调 所有通信都是异步的,不会阻塞 UI 线程

4.1 通信原理

Bridge 的核心是 消息队列。JS 端调用原生方法时,不会直接执行,而是把调用信息(模块名、方法名、参数)序列化成 JSON,塞进队列。原生端从队列里取出来执行,再把结果塞回队列,JS 端轮询拿到结果。

所以,Bridge 通信是异步的。你没法在 JS 里同步等待原生返回数据。

4.2 原生向 JS 发送事件

有时候原生需要主动通知 JS,比如蓝牙连接成功、传感器数据变化。这时候可以用 DeviceEventEmitter

// 原生端
private void sendEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params) {
    reactContext
        .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
        .emit(eventName, params);
}

// 调用
WritableMap map = Arguments.createMap();
map.putString("status", "connected");
sendEvent(getReactApplicationContext(), "BluetoothStatus", map);

JS 端监听:

import { DeviceEventEmitter } from 'react-native';

useEffect(() => {
    const subscription = DeviceEventEmitter.addListener('BluetoothStatus', (event) => {
        console.log('蓝牙状态:', event.status);
    });
    return () => subscription.remove();
}, []);

4.3 线程模型

这里有个容易踩的坑:Native Module 的方法默认跑在 JS 线程。如果你在方法里直接操作 UI,会崩溃。

我建议这样处理:

  • 轻量操作(读 SharedPreferences、取设备信息):直接跑在 JS 线程,没问题。
  • 耗时操作(网络请求、文件读写):用 AsyncTaskHandler 切到子线程,然后通过 Promise 回调。
  • UI 操作:用 UiThreadUtil.runOnUiThread() 切到主线程。
📌 核心原则
• JS 调用原生:通过 Bridge 异步调用,返回 Promise 或回调。
• 原生调用 JS:通过 DeviceEventEmitter 发送事件。
• 所有通信都是异步的,不要假设执行顺序。

五、避坑指南

做混合开发,坑是真的多。我挑几个最常见的说说。

5.1 生命周期管理

RN 页面在 Android 里是 ReactRootView,它有自己的生命周期。如果你在 Activity 的 onPause 里没有通知 RN,可能会导致内存泄漏。

正确的做法:

@Override
protected void onPause() {
    super.onPause();
    if (mReactInstanceManager != null) {
        mReactInstanceManager.onHostPause(this);
    }
}

@Override
protected void onResume() {
    super.onResume();
    if (mReactInstanceManager != null) {
        mReactInstanceManager.onHostResume(this, null);
    }
}

@Override
protected void onDestroy() {
    super.onDestroy();
    if (mReactInstanceManager != null) {
        mReactInstanceManager.onHostDestroy(this);
    }
    if (mReactRootView != null) {
        mReactRootView.unmountReactApplication();
    }
}

5.2 Bundle 加载失败

我曾经遇到过一个问题:打 Release 包后,RN 页面白屏。查了半天发现是 Bundle 文件没打进 APK。

解决方案:在 build.gradle 里确保 assets 目录被包含:

android {
    sourceSets {
        main {
            assets.srcDirs = ['src/main/assets', 'src/main/assets/index.android.bundle']
        }
    }
}

5.3 多实例冲突

如果你的 App 里有多个 RN 页面,不要每个页面都创建 ReactInstanceManager。做成单例,所有页面共享同一个实例。否则内存会爆炸。

六、总结

React Native 与 Android 混合开发,说白了就是三件事:

  • 集成:把 RN 运行时塞进 Android 项目,加载 Bundle 页面。
  • Native Module:让 JS 能调用原生能力,比如硬件、系统 API。
  • Bridge 通信:JS 和原生之间异步传数据,双向流通。

这套架构一旦搭好,后续的业务迭代会非常快。我现在的团队,新功能从开发到上线,平均只要 3 天——因为核心逻辑在原生,UI 和交互在 RN,各司其职。

嗯,如果你正在做混合开发,希望这篇能帮你少踩几个坑。


公众号:蓝海资料掘金营,微信deep3321