JS 如何跑进两个原生世界

带着下面问题进行阅读:

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

先看启动入口

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

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

一屏源码地图

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

text 复制代码
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

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

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

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

iOS 入口:AppDelegate.m

关键代码:

objc 复制代码
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 注册在:

text 复制代码
Examples/Movies/MoviesApp.ios.js
js 复制代码
AppRegistry.registerComponent('MoviesApp', () => MoviesApp);

所以 iOS 这边有两个名字:

text 复制代码
MoviesApp.ios.bundle
-> 加载哪份 JS bundle

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

Android 入口:MoviesActivity.java

关键代码:

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():

text 复制代码
返回 "MoviesApp"

它对应 iOS 的:

objc 复制代码
moduleName:@"MoviesApp"

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

getJSMainModuleName():

text 复制代码
返回 "Examples/Movies/MoviesApp.android"

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

getBundleAssetName():

text 复制代码
返回 "MoviesApp.android.bundle"

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

iOS 与 Android 配置项对照

作用 iOS Android
指定 JS 入口 / bundle 来源 jsCodeLocation getJSMainModuleName() / getBundleAssetName()
指定运行哪个注册应用 moduleName:@"MoviesApp" getMainComponentName()
平台信息 platform=ios Android dev server URL 中的 platform=android
Root View RCTRootView ReactRootView

记法:

text 复制代码
JS Main Module = 加载哪份 JS
Main Component = 运行哪个注册应用

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

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

关键点是:

text 复制代码
packager 构建 bundle 时知道 platform

iOS 请求中有:

text 复制代码
platform=ios

Android 请求中有:

text 复制代码
platform=android

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

3. Libraries 为什么是 JS 公共层

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

js 复制代码
var ReactNative = require('react-native');
var {
  AppRegistry,
  View,
  Text,
  StyleSheet,
} = ReactNative;

这些能力来自 Libraries。

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

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

共享调用方:

text 复制代码
Libraries/Network/XMLHttpRequest.js

它只写:

js 复制代码
const RCTNetworking = require('RCTNetworking');

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

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

text 复制代码
Libraries/Network/RCTNetworking.ios.js
Libraries/Network/RCTNetworking.android.js

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

js 复制代码
@providesModule RCTNetworking

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

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

js 复制代码
RCTNetworkingNative.sendRequest({
  method,
  url,
  data,
  headers,
  incrementalUpdates,
  timeout
}, callback);

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

js 复制代码
RCTNetworkingNative.sendRequest(
  method,
  url,
  requestId,
  convertHeadersMapToArray(headers),
  data,
  incrementalUpdates,
  timeout
);

这里的结构很典型:

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

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

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

业务代码中写:

jsx 复制代码
<View />

在 0.28 中,View 来自:

text 复制代码
Libraries/Components/View/View.js

关键代码是:

js 复制代码
return <RCTView {...this.props} />;

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

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

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

可以把它理解成:

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

iOS 映射

iOS 路径:

text 复制代码
"RCTView"
-> RCTViewManager
-> RCTView

关键文件:

text 复制代码
React/Views/RCTViewManager.m
React/Views/RCTView.m
React/Modules/RCTUIManager.m
React/Views/RCTComponentData.m

iOS 的映射规则是:

text 复制代码
RCTViewManager
-> 去掉类名末尾的 Manager
-> 得到 "RCTView"

RCTComponentData 做了这件事:

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

创建真实 View 时:

objc 复制代码
- (UIView *)view
{
  return [RCTView new];
}

所以 iOS 是:

text 复制代码
JS 注册名 "RCTView"
-> 找到 RCTViewManager
-> 创建 RCTView

Android 映射

Android 路径:

text 复制代码
"RCTView"
-> ReactViewManager
-> ReactViewGroup

关键文件:

text 复制代码
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 的映射规则是:

java 复制代码
viewManager.getName()

ReactViewManager.getName() 返回:

java 复制代码
return REACT_CLASS;

而 REACT_CLASS 是:

java 复制代码
ViewProps.VIEW_CLASS_NAME // "RCTView"

启动时,ViewManagerRegistry 建立映射:

java 复制代码
mViewManagers.put(viewManager.getName(), viewManager);

创建 View 时:

java 复制代码
ViewManager viewManager = mViewManagers.get(className);
View view = viewManager.createView(...);

当 className 是 "RCTView",就会找到 ReactViewManager。

ReactViewManager 最终创建:

java 复制代码
return new ReactViewGroup(context);

为什么 Android 真实 View 不叫 RCTView

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

text 复制代码
"RCTView"
-> 跨 Bridge 注册名

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

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

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

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

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

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

5. 三条 Bridge 路径

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

iOS Bridge 路径

Movies iOS 路径:

text 复制代码
Examples/Movies/Movies/AppDelegate.m
-> RCTRootView
-> RCTBridge
-> RCTBatchedBridge
-> RCTJSCExecutor
-> 不进入 ReactCommon/cxxreact

对应目录:

text 复制代码
React/Base
React/Executors

关键文件:

text 复制代码
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。

所以这一章的结论是:

text 复制代码
iOS 不进入 ReactCommon/cxxreact

Android 默认 bridge 路径

Movies Android 默认路径:

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

关键判断点:

java 复制代码
import com.facebook.react.bridge.CatalystInstanceImpl;

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

ReactBridge.java 中有很多 native 方法:

java 复制代码
public native void callFunction(...);
public native void invokeCallback(...);
public native void loadScriptFromAssets(...);

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

绑定位置在:

text 复制代码
ReactAndroid/src/main/jni/react/jni/OnLoad.cpp

关键代码:

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

这证明:

text 复制代码
com.facebook.react.bridge.ReactBridge
-> 绑定到默认 react/jni

Android cxxbridge 路径

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

它从:

text 复制代码
XReactInstanceManager.builder()

开始。

完整路径:

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

关键判断点:

java 复制代码
import com.facebook.react.cxxbridge.CatalystInstanceImpl;

这个包名中的 cxxbridge 表示 cxxbridge。

进入 C++ 后,在:

text 复制代码
ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp

可以看到:

cpp 复制代码
#include <cxxreact/Instance.h>
#include <cxxreact/MethodCall.h>
#include <cxxreact/ModuleRegistry.h>

这说明 xreact/jni 接入了:

text 复制代码
ReactCommon/cxxreact

6. ReactCommon/cxxreact 的真实边界

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

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

准确说法是:

text 复制代码
ReactCommon/cxxreact
-> 当前由 Android cxxbridge 路径使用
-> iOS 不使用
-> Android 默认 bridge 不使用

三条路径的结论:

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

一句话记:

text 复制代码
iOS 走 RCT;
Android 默认走 bridge + react/jni;
Android X 走 cxxbridge + xreact + ReactCommon。

7. JNI 在这里是什么意思

JNI 是 Java Native Interface。

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

java 复制代码
public native void callFunction(...);

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

RN 默认 Android bridge 中:

text 复制代码
Java
-> ReactBridge.java

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

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

所以 JNI 在这里的作用是:

text 复制代码
把 Java 的 native 方法接到 C++ 实现上

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

text 复制代码
Java ReactBridge.initialize()
-> C++ bridge::create()

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

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

相关推荐
小羊没烦恼!4 小时前
T-SQL查询高级—SQL Server索引中的碎片和填充因子
开发语言·前端·javascript·数据库·后端·python·web安全
定栓4 小时前
Webpack迁到Vite第一节:模块系统迁移:require → import
运维·前端·webpack
小熊在忙fpga4 小时前
N32G430芯片的flash表示
前端·javascript·html
Data analyse4564 小时前
App埋点版本兼容的字段迁移校验
大数据·运维·服务器·前端·数据库·数据分析
todoitbo4 小时前
本来只想给内网开个口子,Natapp 却多给了一道防护
前端·javascript·vue.js·内网穿透·网络防护·natapp
会灭火的程序员5 小时前
SuperMap AgentX Server智能体接入WEB应用技术文档
前端·supermap·agentx·agentx server
CRMEB5 小时前
商品 SKU 设计,从简单规格到多规格踩过什么
java·前端·数据库·开源
Chloe.Zz5 小时前
一篇入门Agent
开发语言·前端·javascript
java1234_小锋5 小时前
【技术专题】HTML5 网页技术 - HTML5 表格标签
前端·html·html5
雪芽蓝域zzs5 小时前
JS 项目转 TS 项目(Vue3 + 若依这类后台项目)完整改造清单
前端·vue.js