我们知道 RN 之所以受欢迎的其中一个原因就是把之前只有在 React 中有的 jsx 带进了 Native 开发的世界
在这一个篇章中,我们会深入了解 RN 是如何将 <View>
、<Text>
标签转换成 UIView
(IOS)、ViewGroup
(Android)
当然,还有 yoga
究竟在其中做了什么?以及为什么要有 yoga
但是,在深入之前,我们要先聊聊一个方法:runApplication
runApplication
顾名思义,这是一个启动应用的方法,但这里启动的不是原生应用,而是在 JS bundle 在加载完之后,由 RN 在原生应用中启动 React 应用,它的调用过程涵盖了三个线程,其调用流程如下:

可以看到,我们的 RN 程序启动以后,会在客户端的 RootView
中调用 runApplication
方法,这个方法的调用会通过我们在通信机制中讲到的 Instance -> Bridge -> JSCExecutor
这条通道一路走进 JS 程序
当 JS 接收到 AppRegistry.runApplication
的调用后,它会去找到我们 RN 项目根目录的 index.js
注册的组件(默认在 App.js
),最后调用 ReactNative.js
的 render
方法
ReactNative.js
中包含着 RN 在 JS 侧的核心代码,他的主要任务是将 React diff 完的 fiber 转换成为一系列的 UIManager.xxxxx
调用
这些调用最后会触发 Native 中的 UIManager(UIManager 也是一个 Native module) 的逻辑生成原生元素(UIView
,ViewGroup
等等) ,最后在 yoga 这个布局引擎的帮助下完成原生页面的渲染
createView, setChildren 与 yoga 布局
接下来我们以一个简单的例子来聊聊我们写的 RN jsx 是如何最后转变为原生元素并显示在屏幕中的
html
<View>
<Text>Hello world!</Text>
</View>
当我们这个组件被 ReactNative.js
的 render
执行后,会有以下方法被调用:
题外话,在具体的场景中,上述例子可能不止有下述方法被调用了,被调用的方法也可能会有区别,但是他们的目的与功能是类似的,本文为了方便理解做了部份简化
UIManager.createView(tagV, 'RCTView', rootTag, {})
UIManager.createView(tagT, 'RCTText', rootTag, { text: 'Hello world!' })
UIManager.setChildren(tagV, [tagT])
createView
接受 4 个参数:
- 第一个参数是一个自增的数字,会唯一标识一个创建的元素
- 第二个参数是需要创建的元素类型,因为我们需要的是
View
元素,其对应的是RCTView
(在原生平台中,它是一个继承自各自平台 View 元素的类,其中定义了一些 RN 需要的方法) - 第三个参数是根容器(root container)的唯一标识符,根容器在 native 侧创建,是 RN 创建的元素的根结点。由于一个 APP 中可以创建多个根容器,
createView
需要确保当前创建的元素被归类到正确的容器中 - 最后一个参数代表元素的属性
setChildren
接受 2 个参数:
- 第一个参数与 createView 一致,唯一标识着一个父元素
- 第二个参数是一个数组,其中包含子元素的标签
当这两个方法通过 bridge 最后进入 Native 侧的 UIManager
时,会根据 IOS 与 Android 的平台特性区分为两套实现,分别是:
RCTUIManager.m
:IOS 中UIManager
的实现UIManagerModule.java
:Android 中UIManager
的实现
下面我们分别聊聊这两者都做了些什么
UIManager in IOS
在 IOS 的 createView
实现中,主要做了 3 件事:
- 根据
RCTView
这个类型分别创建了一个shadowView
以及一个离屏的 UIKitUIView
(RCTText
类也同理,后不赘述) - 根据
RCTView
这个类型的规则,从元素的属性中筛选了部份shadowView
需要的属性赋值给shadowView
的props
- 将当前的
shadowView
放进_shadowViewsWithUpdatedProps
中等待后续消费
其中,shadowView
是 RN 为了方便 yoga 计算布局而设计的类型,而 UIView
是 IOS 正儿八经在屏幕上渲染的元素
两者的区别在于 shadowView
负责接受元素布局相关的属性(如 width
, height
, border
, margin
等),然后交给 yoga 计算布局;UIView
只需要处理布局之外的 backgroundColor
, opacity
, shadow
等等属性就好
属性的分类依据每个类型不同而不同,比如 RCTView 的定义在 RCTViewManager.m 中
这样做的好处在于可以将计算量较大的布局工作交给另外一个线程防止 IOS 的主线程阻塞
在 IOS 的 setChildren
实现中,主要做了 3 件事:
- 将子元素的
shadowView
插入成为父元素shadowView
的subView
- 将子元素插入成为父元素的
subView
- 将当前的
shadowView
放进_shadowViewsWithUpdatedChildren
中等待后续消费
最后,我们在之前讲 runApplication
的调用流程的时候留了一个伏笔:在 JSCExecutor.cpp
中调用 JS 的方法用的是 callFunctionReturnFlushedQueue
,以下是它的实现:
js
callFunctionReturnFlushedQueue(module: string, method: string, args: any[]) {
this.__guard(() => {
// 调用对应的 js 方法
this.__callFunction(module, method, args);
});
// 返回到目前为止积压在 queue 中的 native module 调用请求
return this.flushedQueue();
}
可以看到在执行完 js 侧的 runApplication
后,该方法会将执行过程中累积的 native module 调用一下子清空,明确告知 native 侧:我这个方法调用过程中发生的请求已经全部给你了
当 native 侧接受到这个信息之后,它会去轮询所有注册过 batchDidComplete
方法的 native module(UIManager
也是其中一员)并执行他们的 batchDidComplete
方法
在 UIManager
中 batchDidComplete
调用了最重要的一个方法:_layoutAndMount
我们来看看实现:
objc
// in RCTUIManager.m
- (void)_layoutAndMount
{
// 消费上述 _shadowViewsWithUpdatedProps:把有变化的 props 经过转换后赋值给 yogaNode(后续 yoga 会根据这些节点的属性来计算布局
[self _dispatchPropsDidChangeEvents];
// 消费上述 _shadowViewsWithUpdatedChildren:根据不同的 view 类型做不同处理(shadowView 场景的话什么都不做)
[self _dispatchChildrenDidChangeEvents];
// 遍历所有的 root container(reactTag)
for (NSNumber *reactTag in _rootViewTags) {
// 找到每一个 root container 的 shadowView(也就是 rootShadowView),由于 view 跟 shadowView 是一一对应的关系,所以 rootShadowView 也有可能有多个)
RCTRootShadowView *rootView = (RCTRootShadowView *)_shadowViewRegistry[reactTag];
// 触发 yoga 的布局计算,并且把布局结果包装到一个代码片段中返回,返回的代码片段会被加到一个等待队列中等待被主线程执行(因为在 ios 中只有主线程能操纵 UIKit)
[self addUIBlock:[self uiBlockWithLayoutUpdateForRootView:rootView]];
}
// 执行上述的代码片段,将计算好的布局应用给元素
[self flushUIBlocksWithCompletion:^{}];
}
补充一点,我们说到 uiBlockWithLayoutUpdateForRootView
方法除了计算新的元素布局之外,还会返回一个代码片段,这个代码片段除了在普通情况下将计算好的布局应用给元素之外,还负责判断该元素是否需要动画,如果需要的话,还会将对应的动画效果应用给对应元素
至此,我们完成了对 IOS 中 UIManager 的部份方法与核心机制讲解
UIManager in Android
UIManager 在 Android 中的目标跟在 IOS 中是一致的,主要区别在于加入了一个 NativeViewHierarchyOptimizer
的优化机制
至于加入的原因我们会在后文描述,现在我们先来看看 Android 是如何实现 createView
, setChildren
, batchDidComplete
的
在 Android createView
的实现中,RN 也做了三件事:
- 根据
RCTView
这个类型创建了一个shadowView
,并将其保存至mShadowNodeRegistry
(一个用来保存所有 shadowView 的类) - 将元素属性中
shadowView
需要的属性赋值给新创建的shadowView
- 将创建原生 View 元素的任务交给
NativeViewHierarchyOptimizer
,它会在符合条件的情况下创建原生元素
NativeViewHierarchyOptimizer
就是 Android 与 IOS 在 UIManager 中最大的区别,它的工作主要就是将元素用是否为布局专用元素 进行区分:如果是布局专用元素它将不会创建真正的原生元素;反之则会跟 IOS 一样创建原生元素
在 setChildren
中,则是:
- 将子元素的
shadowView
插入成为父元素shadowView
的mChildren
(对应 IOS 中的subView
) - 将插入原生子元素的任务交给
NativeViewHierarchyOptimizer
,在其中会判断父元素是否为布局专用元素 ,如果是,则会将子元素插入到最近的不是布局专用元素的父元素上
最后,在 JS 侧所有请求结束后,Android 会执行 dispatchViewUpdates
方法(对应 IOS 中的 _layoutAndMount
)
java
// in UIImplementation.java
public void dispatchViewUpdates(int batchId) {
try {
// 1. 调用 yoga 计算布局
// 2. 将布局结果转换成一些对元素的操作并将这些操作入栈等待执行
// 3. 执行 JS 侧的 onLayout 回调
updateViewHierarchy();
// 清理布局过程中使用到的一些标识
mNativeViewHierarchyOptimizer.onBatchComplete();
// 将操作一一出栈并应用布局(调用元素的 measure 以及 layout 方法)
mOperationsQueue.dispatchViewUpdates(batchId, commitStartTime, mLastCalculateLayoutTime);
}
}
Android vs IOS
在上文中,我们说到 Android 会比 IOS 多一个 NativeViewHierarchyOptimizer
用来防止为一些布局专用元素创建真正的元素,为什么呢?
首先,什么是布局专用元素?布局专用元素需要同时满足个条件:
- 该元素是
RCTView
- 该元素的 collapsable 元素是 true(也就是默认值)
- 该元素所有属性都是布局专用属性(LAYOUT_ONLY_PROPS),包含:
java
// in ViewProps.java
private static final HashSet<String> LAYOUT_ONLY_PROPS =
new HashSet<>(
Arrays.asList(
ALIGN_SELF,
ALIGN_ITEMS,
COLLAPSABLE,
FLEX,
FLEX_BASIS,
FLEX_DIRECTION,
FLEX_GROW,
FLEX_SHRINK,
FLEX_WRAP,
JUSTIFY_CONTENT,
ALIGN_CONTENT,
DISPLAY,
/* position */
POSITION,
RIGHT,
TOP,
BOTTOM,
LEFT,
START,
END,
/* dimensions */
WIDTH,
HEIGHT,
MIN_WIDTH,
MAX_WIDTH,
MIN_HEIGHT,
MAX_HEIGHT,
/* margins */
MARGIN,
MARGIN_VERTICAL,
MARGIN_HORIZONTAL,
MARGIN_LEFT,
MARGIN_RIGHT,
MARGIN_TOP,
MARGIN_BOTTOM,
MARGIN_START,
MARGIN_END,
/* paddings */
PADDING,
PADDING_VERTICAL,
PADDING_HORIZONTAL,
PADDING_LEFT,
PADDING_RIGHT,
PADDING_TOP,
PADDING_BOTTOM,
PADDING_START,
PADDING_END));
在这种情况下,NativeViewHierarchyOptimizer
将不会创建真正的原生元素
为什么要在 Android 中应用这个优化呢?这个我们要从 Android 的 Choreographer 开始说起:
对于非 RN 的 Android app来说,当 app 接受到硬件传来的 vsync 信号之后,他会启动 choreographer 程序:
js
Choreographer
→ ViewRootImpl.performTraversals() // 开始从程序根节点向下遍历所有元素
→ performMeasure() // 执行元素 measure 方法
→ performLayout() // 计算元素布局
→ performDraw() // 绘制元素
其中 measure 以及 layout 这两步只有当元素本身判断需要(元素调用了 requestLayout )之后才会启动,由于 RN 引入了 yoga 引擎来计算布局(取代了 performMeasure 与 performLayout 的功能),所以 RN 的目标就是让 Android 本身的 performMeasure 以及 performLayout 尽可能少的被启动
所以在 Android 中才需要 NativeViewHierarchyOptimizer
来尽可能减少多余的节点被挂在渲染树上
那么为什么 IOS 不需要呢?
因为 IOS 用的是完全不同的机制,IOS 提供了两种渲染机制:Frame-Based Layout 和 Constraint-Based Layout
RN 选用了 Frame-Based Layout,它的好处就是:系统会直接根据我们计算好的结果来渲染下一帧,不会有多余的操作