JS 如何跑进两个原生世界

27 阅读7分钟

带着下面问题进行阅读:

  • Examples/Movies 里为什么有 iOS 和 Android 两套入口。
  • Libraries 为什么是 JS 公共层,但两端 bundle 不完全相同。
  • <View /> 为什么能在 iOS 上变成 RCTView,在 Android 上变成 ReactViewGroup
  • Android 默认 bridge 和 Android cxxbridge 为什么是两条路径。
  • ReactCommon/cxxreact 在 0.28 中到底参与哪条路径。

先看启动入口

你启动 iOS App
-> 操作系统进入 iOS 宿主代码
-> iOS 宿主加载 iOS bundle
你启动 Android App
-> 操作系统进入 Android 宿主代码
-> Android 宿主加载 Android bundle

一个跨端 RN 项目维护两套 Native 宿主;运行时只启动当前平台那一套。

一屏源码地图

先把第 1 章的地图放在这里,后面逐段解释:

Examples/Movies
  |
  |-- iOS Native 宿主
  |     AppDelegate.m
  |     -> RCTRootView
  |     -> RCTBridge
  |     -> RCTBatchedBridge
  |
  |-- Android Native 宿主
        MoviesActivity.java
        -> ReactActivity
        -> ReactInstanceManagerImpl

Native 宿主指定 JS 入口和 platform
  |
  v
packager
  |
  |-- platform=ios     -> 选择 .ios.js / 共享 .js
  |-- platform=android -> 选择 .android.js / 共享 .js
  v
bundle
  |
  v
Libraries
  |
  |-- AppRegistry
  |-- View / Text / StyleSheet
  |-- UIManager
  |-- NativeModules
  v
Bridge 协议
  |
  |-- iOS: React
  |     RCTBridge / RCTBatchedBridge / RCTUIManager / RCTViewManager
  |
  |-- Android 默认 bridge: ReactAndroid
  |     ReactInstanceManagerImpl / bridge.CatalystInstanceImpl / ReactBridge / react/jni
  |
  |-- Android cxxbridge: ReactAndroid + ReactCommon
        XReactInstanceManagerImpl / cxxbridge.CatalystInstanceImpl / xreact/jni / ReactCommon/cxxreact

这一章要记住的不是每个类,而是几条分界线:

Libraries 是 JS 公共层
React 是 iOS Native 实现
ReactAndroid 是 Android Native 实现
ReactCommon/cxxreact 只接入 Android cxxbridge
packager 负责按平台构建 bundle

1. Movies 里有两套 Native 宿主入口

iOS 入口:AppDelegate.m

关键代码:

jsCodeLocation = [NSURL URLWithString:
  @"http://localhost:8081/Examples/Movies/MoviesApp.ios.bundle?platform=ios&dev=true"];

RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                    moduleName:@"MoviesApp"
                                             initialProperties:nil
                                                 launchOptions:launchOptions];

这一段做了两件事。

  1. 指定要加载哪份 JS bundle:
  2. 告诉 packager 当前目标平台是 iOS:platform=ios

dev=true 不是平台选择,它只是开发模式配置。

moduleName:@"MoviesApp" 也不是入口文件。它表示 bundle 执行完后,要从 AppRegistry 中运行哪个注册应用。

对应 JS 注册在:

Examples/Movies/MoviesApp.ios.js
AppRegistry.registerComponent('MoviesApp', () => MoviesApp);

所以 iOS 这边有两个名字:

MoviesApp.ios.bundle
-> 加载哪份 JS bundle

"MoviesApp"
-> 运行 bundle 中注册的哪个应用

Android 入口:MoviesActivity.java

关键代码:

public class MoviesActivity extends ReactActivity {
  @Override
  protected String getMainComponentName() {
    return "MoviesApp";
  }

  @Override
  protected @Nullable String getBundleAssetName() {
    return "MoviesApp.android.bundle";
  };

  @Override
  protected String getJSMainModuleName() {
    return "Examples/Movies/MoviesApp.android";
  }
}

这三个方法分别解决三个问题。

getMainComponentName()

返回 "MoviesApp"

它对应 iOS 的:

moduleName:@"MoviesApp"

意思是:bundle 加载完后,运行 AppRegistry 里注册名为 "MoviesApp" 的应用。

getJSMainModuleName()

返回 "Examples/Movies/MoviesApp.android"

它对应 iOS 的 jsCodeLocation 中的 JS 入口部分。开发模式下,Android 会据此向 packager 请求 Android bundle。

getBundleAssetName()

返回 "MoviesApp.android.bundle"

它用于预打包 bundle。可以把它理解成生产环境或离线场景中的构建产物名。

iOS 与 Android 配置项对照

作用iOSAndroid
指定 JS 入口 / bundle 来源jsCodeLocationgetJSMainModuleName() / getBundleAssetName()
指定运行哪个注册应用moduleName:@"MoviesApp"getMainComponentName()
平台信息platform=iosAndroid dev server URL 中的 platform=android
Root ViewRCTRootViewReactRootView

记法:

JS Main Module = 加载哪份 JS
Main Component = 运行哪个注册应用

2. packager 为什么会生成不同 bundle

packager 不创建 Native View,也不执行 Bridge 调用。它负责把 JS 源码整理成当前平台可执行的 bundle。

关键点是:

packager 构建 bundle 时知道 platform

iOS 请求中有:

platform=ios

Android 请求中有:

platform=android

所以,两端 bundle 不完全相同,不是因为 JS 运行后才临时挑文件,而是 packager 在构建依赖图时就已经按平台选文件

3. Libraries 为什么是 JS 公共层

Libraries 是 RN 向业务 JS 暴露公共能力的地方。业务代码写:

var ReactNative = require('react-native');
var {
  AppRegistry,
  View,
  Text,
  StyleSheet,
} = ReactNative;

这些能力来自 Libraries

但“公共层”不等于“所有文件两端完全一样”。Libraries 里既有共享文件,也有平台专属文件。

一个具体例子是网络模块。

共享调用方:

Libraries/Network/XMLHttpRequest.js

它只写:

const RCTNetworking = require('RCTNetworking');

它不关心当前是 iOS 还是 Android。

但真正的 RCTNetworking 有两个平台实现:

Libraries/Network/RCTNetworking.ios.js
Libraries/Network/RCTNetworking.android.js

两个文件都声明同一个模块名:

@providesModule RCTNetworking

但内部调用 Native 的参数形态不同。

iOS 把请求整理成一个对象:

RCTNetworkingNative.sendRequest({
  method,
  url,
  data,
  headers,
  incrementalUpdates,
  timeout
}, callback);

Android 在 JS 包装层生成 requestId,并把 headers 转成数组:

RCTNetworkingNative.sendRequest(
  method,
  url,
  requestId,
  convertHeadersMapToArray(headers),
  data,
  incrementalUpdates,
  timeout
);

这里的结构很典型:

共享调用方
-> 依赖统一模块名
-> packager 按平台选择具体 JS 包装层
-> JS 包装层再调用各自 Native 模块

所以 Libraries 的公共性体现在统一 API,而不是每个文件都相同。

4. <View /> 如何落到不同平台的真实 View

业务代码中写:

<View />

在 0.28 中,View 来自:

Libraries/Components/View/View.js

关键代码是:

return <RCTView {...this.props} />;

const RCTView = requireNativeComponent('RCTView', View, {
  nativeOnly: {
    nativeBackgroundAndroid: true,
  }
});

这里出现的 "RCTView" 很重要。

它不是说 Android 一定有一个 Java 类叫 RCTView。它是 JS 与 Native 之间约定的组件注册名。

可以把它理解成:

JS 说:请创建一个类型名为 "RCTView" 的 Native 组件。
当前平台再决定由谁来创建。

iOS 映射

iOS 路径:

"RCTView"
-> RCTViewManager
-> RCTView

关键文件:

React/Views/RCTViewManager.m
React/Views/RCTView.m
React/Modules/RCTUIManager.m
React/Views/RCTComponentData.m

iOS 的映射规则是:

RCTViewManager
-> 去掉类名末尾的 Manager
-> 得到 "RCTView"

RCTComponentData 做了这件事:

_name = RCTBridgeModuleNameForClass(_managerClass);
if ([_name hasSuffix:@"Manager"]) {
  _name = [_name substringToIndex:_name.length - @"Manager".length];
}

创建真实 View 时:

- (UIView *)view
{
  return [RCTView new];
}

所以 iOS 是:

JS 注册名 "RCTView"
-> 找到 RCTViewManager
-> 创建 RCTView

Android 映射

Android 路径:

"RCTView"
-> ReactViewManager
-> ReactViewGroup

关键文件:

ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java
ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagerRegistry.java
ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java

Android 的映射规则是:

viewManager.getName()

ReactViewManager.getName() 返回:

return REACT_CLASS;

REACT_CLASS 是:

ViewProps.VIEW_CLASS_NAME // "RCTView"

启动时,ViewManagerRegistry 建立映射:

mViewManagers.put(viewManager.getName(), viewManager);

创建 View 时:

ViewManager viewManager = mViewManagers.get(className);
View view = viewManager.createView(...);

className"RCTView",就会找到 ReactViewManager

ReactViewManager 最终创建:

return new ReactViewGroup(context);

为什么 Android 真实 View 不叫 RCTView

因为注册名和平台实现类名不是同一个层次。

"RCTView"
-> 跨 Bridge 注册名

ReactViewManager
-> Android 上负责这种组件的 View Manager

ReactViewGroup
-> Android 上真正显示并承载子 View 的对象

ReactViewGroup 继承 Android 的 ViewGroup,它可以包含子 View,适合实现 RN 的 <View> 容器。

Manager 可以理解为某类 Native View 的工厂兼适配器:

它声明自己管理哪个组件名
它创建真实 Native View
它把 JS props 设置到真实 View 上
它处理这类 View 的命令和子 View 关系

它不是整个应用的渲染管理者。

5. 三条 Bridge 路径

第 1 章只要求建立地图,不要求读完 Bridge 内部实现。这里先记住三条路径和边界。

iOS Bridge 路径

Movies iOS 路径:

Examples/Movies/Movies/AppDelegate.m
-> RCTRootView
-> RCTBridge
-> RCTBatchedBridge
-> RCTJSCExecutor
-> 不进入 ReactCommon/cxxreact

对应目录:

React/Base
React/Executors

关键文件:

React/Base/RCTRootView.m
React/Base/RCTBridge.m
React/Base/RCTBatchedBridge.m
React/Executors/RCTJSCExecutor.mm

iOS 0.28 使用 Objective-C Bridge。React/ 目录中没有引用 ReactCommon/cxxreact

所以这一章的结论是:

iOS 不进入 ReactCommon/cxxreact

Android 默认 bridge 路径

Movies Android 默认路径:

Examples/Movies/android/.../MoviesActivity.java
-> ReactActivity
-> ReactInstanceManager.builder()
-> ReactInstanceManagerImpl
-> com.facebook.react.bridge.CatalystInstanceImpl
-> ReactBridge
-> ReactAndroid/src/main/jni/react
-> 不进入 ReactCommon/cxxreact

关键判断点:

import com.facebook.react.bridge.CatalystInstanceImpl;

这个包名中的 bridge 表示默认 bridge。

ReactBridge.java 中有很多 native 方法:

public native void callFunction(...);
public native void invokeCallback(...);
public native void loadScriptFromAssets(...);

这些 Java 方法通过 JNI 绑定到 C++ 实现。

绑定位置在:

ReactAndroid/src/main/jni/react/jni/OnLoad.cpp

关键代码:

registerNatives("com/facebook/react/bridge/ReactBridge", {
  makeNativeMethod("initialize", ..., bridge::create),
  makeNativeMethod("callFunction", bridge::callFunction),
  ...
});

这证明:

com.facebook.react.bridge.ReactBridge
-> 绑定到默认 react/jni

Android cxxbridge 路径

cxxbridge 是另一条 Android 路径,不是 Movies 默认路径。

它从:

XReactInstanceManager.builder()

开始。

完整路径:

XReactInstanceManager.builder()
-> XReactInstanceManagerImpl
-> com.facebook.react.cxxbridge.CatalystInstanceImpl
-> ReactAndroid/src/main/jni/xreact
-> ReactCommon/cxxreact

关键判断点:

import com.facebook.react.cxxbridge.CatalystInstanceImpl;

这个包名中的 cxxbridge 表示 cxxbridge。

进入 C++ 后,在:

ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp

可以看到:

#include <cxxreact/Instance.h>
#include <cxxreact/MethodCall.h>
#include <cxxreact/ModuleRegistry.h>

这说明 xreact/jni 接入了:

ReactCommon/cxxreact

6. ReactCommon/cxxreact 的真实边界

ReactCommon/cxxreact 是 0.26 引入、到 0.28 仍在演进的共享 C++ Bridge 核心。

但在 0.28 中,不能把它说成“iOS 和 Android 共用的 Native 核心”。

准确说法是:

ReactCommon/cxxreact
-> 当前由 Android cxxbridge 路径使用
-> iOS 不使用
-> Android 默认 bridge 不使用

三条路径的结论:

iOS
-> RCTRootView
-> RCTBridge
-> RCTBatchedBridge
-> RCTJSCExecutor
-> 不进入 ReactCommon/cxxreact
Android 默认 bridge
-> ReactInstanceManagerImpl
-> bridge.CatalystInstanceImpl
-> ReactBridge
-> react/jni
-> 不进入 ReactCommon/cxxreact
Android cxxbridge
-> XReactInstanceManagerImpl
-> cxxbridge.CatalystInstanceImpl
-> xreact/jni
-> ReactCommon/cxxreact

一句话记:

iOS 走 RCT;
Android 默认走 bridge + react/jni;
Android X 走 cxxbridge + xreact + ReactCommon。

7. JNI 在这里是什么意思

JNI 是 Java Native Interface。

在 Android 里,Java 方法可以声明为:

public native void callFunction(...);

这表示方法不是 Java 实现的,真正实现由 C 或 C++ 提供。

RN 默认 Android bridge 中:

Java
-> ReactBridge.java

JNI 注册
-> ReactAndroid/src/main/jni/react/jni/OnLoad.cpp

C++ 实现
-> ReactAndroid/src/main/jni/react

所以 JNI 在这里的作用是:

把 Java 的 native 方法接到 C++ 实现上

registerNatives("com/facebook/react/bridge/ReactBridge", ...) 的意思就是:

Java ReactBridge.initialize()
-> C++ bridge::create()

Java ReactBridge.callFunction()
-> C++ bridge::callFunction()

这不是普通 JS Bridge 的概念,而是 Android Java 到 C++ 的语言边界。