ReactNative源码分析 - 渲染原理
1.ReactNative源码分析 - 概述
2.ReactNative源码分析 - JavaScriptCore C语言篇
3.ReactNative源码分析 - 启动流程
4.ReactNative源码分析 - 通信机制
5.ReactNative源码分析 - 渲染原理
- 一、前言
- 二、React简介
- 三、原生端与渲染流程相关的类
- 1.原生端UI组件体系
- 2.原生端UI管理者体系
- 四、原生控件信息如何传递到JS端
- 1.RCTViewManager定义导出属性
- 2.RCTUIManager收集原生控件信息
- 3.UIManager获取原生控件信息
- 4.原生控件信息处理、获取
- 五、渲染流程
- 1.注册JS组件
- 2.运行JS组件,执行渲染
- 3.获取React计算结果
- 4.驱动Native执行渲染流程
- 六、JS端调用原生控件导出函数
- 七、结语
一、前言
- 1.本文主要分析JS端业务层编写的React组件最终如何渲染为原生控件。渲染原理的完整流程梳理起来有点困难,难在此处开始与React衔接,而React又是一个非常庞大的框架。本文对React不过多展开讨论,首先笔者并不擅长React,没有从源码层面完整梳理过它的工作流程,再者这个流程分析起来又是一个系列的文章。本文以React最终产生的结果(组件信息JSON数据)为基础,分析它如何驱动原生端进行UI绘制。
- 2.ReactNative以React的模式开发,最终通过底层Bridge驱动原生端进行相应操作。从UI渲染原理层面分析,大致流程是:
- 承载业务逻辑的React组件最终生成描述原生控件信息的JSON数据,通过底层Bridge传递到原生端,驱动原模块绘制原生UI控件;
- 用户行为触发原生UI控件交互事件/回调事件从原生端传递到JS端以处理业务逻辑,业务逻辑处理结果通常表现为原生控件信息的更新,新的信息数据会再次传递到原生端,驱动原生模块进行新一轮UI更新……
- 3.本文很多逻辑是基于通信机制(eg:原生模块信息导出流程、Native&JS通信),本文假设你已了解这些知识。
二、React简介
import React from 'react';
import {View, TouchableOpacity} from 'react-native';
export default class TestRender extends React.Component {
render() {
return (
<View style={{ backgroundColor: 'white', paddingTop: 64,}} testID="white">
<View style={{ backgroundColor: "yellow",width: 50, height: 50}} testID="white-yellow"/>
<View
style={{backgroundColor: 'green',width: 100, height: 100,justifyContent:"center",alignItems:"center"}} testID="white-green"
>
<View style={{backgroundColor: 'red', width: 50, height: 50,}} testID="white-green-red"></View>
</View>
<TouchableOpacity
style={{backgroundColor: 'blue', width: 50, height: 50}}
testID="white-blue"
onPress={() => console.log("onPress") }
/>
</View>
);
}
}
上述JSX转码后变成如下代码。每个React元素转化为React.createElement
函数调用,最终返回一个描述组件信息的JS对象,其中嵌套关系表示父子组件关系。
export default class TestRender extends React.Component {
render() {
return
React.createElement(View, {
style: {
backgroundColor: 'white',
paddingTop: 64
},
testID: "white"
}, React.createElement(View, {
style: {
backgroundColor: "yellow",
width: 50,
height: 50
},
testID: "white-yellow"
}),
React.createElement(View, {
style: {
backgroundColor: 'green',
width: 100,
height: 100,
justifyContent: "center",
alignItems: "center"
},
testID: "white-green"
}, React.createElement(View, {
style: {
backgroundColor: 'red',
width: 50,
height: 50
},
testID: "white-green-red"
})),
React.createElement(TouchableOpacity, {
style: {
backgroundColor: 'blue',
width: 50,
height: 50
},
testID: "white-blue",
onPress: () => console.log("onPress")})
);
}
}
- React Element即React元素是React应用的最小单元。
const element = <h1>Hello, world</h1>;
-
ReactComponent即React组件是独立、可复用的UI模块,它可同时包含UI渲染、业务逻辑。组件是由元素构成的,React应用由一系列的React组件组合起来的。
关于React的源码分析,可以参考React源码解析(一):组件的实现与挂载,结合React源码分析React.createElement
最终返回JS对象的过程。
三、原生端与渲染流程相关的类
1.原生端UI组件体系
View体系.jpg原生端与UI渲染相关的视图类体系如图所示,ReactNative封装了一系列原生控件,把控件信息导出到JS端并且包装为对应的组件供JS业务层使用,我们暂且把前者称为控件
,后者称为组件
,两者在Native端、JS端一一对应。
-
RCTComponent
协议定义了一套标准接口,作为视图树的逻辑结点。RCTShadowView
和UIView
遵守该协议,使得对这两者组成的树View Tree、ShadowView Tree的操作更统一。 - ReactNative封装的原生控件(RCTView、RCTTextView、RCTImageView……)最终都继承自
UIView
,即默认实现了RCTComponent
协议;
RCTShadowView
可理解为布局结点,js端传递到原生端的控件信息,与布局相关的会赋值给它,并通过它驱动yoga计算出组件的布局信息(frame、center);
总的来说:在ReactNative渲染过程中,ShadowView与UIView是对应关系,每个UIView都有一个与之对应的ShadowView。UIView是最终渲染出来的视图、ShadowView是它的布局结点,负责为它计算布局信息。渲染过程中存在两棵树:控件树、布局结点树,两者的结点一一对应,互为镜子。 -
RCTRootView
根视图是ReactNative暴露给原生端使用的控件,可以像普通UIView
一样使用。主要负责运行js端通过AppRegistry.registerComponent
注册的根组件。混合开发应用中通常是存在多个RCTRootView,共用同一个底层Bridge。 -
RCTRootContentView
作为RCTRootView
的子视图,RCTRootView
只是负责运行JS组件,后续JS端驱动创建的控件绘制到RootContentView上。 -
RCTRootContentView
与RCTRootShadowView
是对应关系。
2.原生端UI管理者体系
Manager体系.jpg原生端与UI渲染相关的管理者类体系如图所示
-
RCTUIManager、RCTViewManager都是原生模块,由于懒加载机制他们实际上是单例,最终都通过底层Bridge把模块信息导出到JS端使用。
- RCTViewManager与导出控件一一对应,负责管理对应的原生控件,包括定义要导出的控件属性、接口;接收通过
RCT_CUSTOM_VIEW_PROPERTY
导出的属性赋值;创建控件、创建布局结点…… - RCTUIManager是渲染流程中的集大成者,它接收JS端发送过来的指令,执行UI控件创建/移除、属性设置/更新、动画、调用原生控件导出函数等。
RCTViewManager创建原生控件并提供给RCTUIManager,RCTUIManager则会反过来委托它们在需要的时候去设置和更新视图的属性,甚至调用控件导出函数。
- RCTViewManager与导出控件一一对应,负责管理对应的原生控件,包括定义要导出的控件属性、接口;接收通过
-
RCTComponentData包装每一个RCTViewManager,RCTUIManager通过它来:
- 1.创建原生控件、布局结点;
- 2.设置控件属性、布局属性;
- 3.获取控件导出属性集合。
四、原生控件信息如何传递到JS端
这里以RCTWKWebView为例分析原生控件信息导出流程,理解RCTWKWebView的封装,你一定可以封装自定义原生控件供JS端使用
1.RCTViewManager定义导出属性
RCTViewManager控件属性导出宏生成的对应函数,用于后续获取导出属性信息
-
RCT_EXPORT_VIEW_PROPERTY
生成属性名、属性类型获取函数。后续通过Runtime获取以propConfig_
为前缀的函数并调用以获取这些信息。 -
RCT_EXPORT_SHADOW_PROPERTY
生成布局属性名、属性类型获取函数。后序通过Runtime获取以propConfigShadow_
为前缀的函数并调用以获取这些信息。 -
RCT_CUSTOM_VIEW_PROPERTY
生成属性名、属性类型获取函数,并生成属性setter
// RCTWKWebViewManager.m
RCT_EXPORT_VIEW_PROPERTY(source, NSDictionary)
RCT_EXPORT_VIEW_PROPERTY(onLoadingStart, RCTDirectEventBlock)
RCT_CUSTOM_VIEW_PROPERTY(bounces, BOOL, RCTWKWebView) {
view.bounces = json == nil ? true : [RCTConvert BOOL: json];
}
// 生成属性type keypath获取函数
+ (NSArray<NSString *> *)propConfig_source {
return @[@"NSDictionary"];
}
+ (NSArray<NSString *> *)propConfig_onLoadingStart {
return @[@"RCTDirectEventBlock"];
}
+ (NSArray<NSString *> *)propConfig_bounces {
return @[@"BOOL", @"__custom__"];
}
- (void)set_bounces:(id)json forView:(RCTWKWebView *)view withDefaultView:(RCTWKWebView *)defaultView {
view.bounces = json == ((void *)0) ? 1 : [RCTConvert BOOL: json];
}
// RCTViewManager.m
RCT_EXPORT_SHADOW_PROPERTY(marginTop, YGValue)
+ (NSArray<NSString *> *)propConfigShadow_marginTop {
return @[@"YGValue"];
}
2.RCTUIManager收集原生控件信息
-
RCTUIManager
实例化后执行setBridge:
,会收集所有的原生控件管理者RCTViewManager
,并创建对应的RCTComponentData
。
// RCTUIManager.m
- (void)setBridge:(RCTBridge *)bridge
{
_bridge = bridge;
...
// 收集RCTViewManager
_componentDataByName = [NSMutableDictionary new];
for (Class moduleClass in _bridge.moduleClasses) {
if ([moduleClass isSubclassOfClass:[RCTViewManager class]]) {
RCTComponentData *componentData = [[RCTComponentData alloc] initWithManagerClass:moduleClass bridge:_bridge];
_componentDataByName[componentData.name] = componentData;
}
}
}
-
RCTComponentData
包装了RCTViewManager
,因此它可获取原生控件的一切导出信息。
RCTViewManager
通过宏导导出属性/布局属性,分别生成的导出函数前缀propConfig_
、propConfigShadow_
。
函数viewConfig
使用Runtime遍历ViewManager所有类函数以获取导出属性信息,收集以propConfig
开头的函数,截取函数名字符串_
后面的内容获取属性名;调用函数获取属性类型。
// RCTComponentData.m
// 获取控件导出属性
- (NSDictionary<NSString *, id> *)viewConfig
{
NSMutableArray<NSString *> *bubblingEvents = [NSMutableArray new];
NSMutableArray<NSString *> *directEvents = [NSMutableArray new];
// Runtime获取属性/布局属性导出宏生成的函数,函数前缀`propConfig`
unsigned int count = 0;
NSMutableDictionary *propTypes = [NSMutableDictionary new];
Method *methods = class_copyMethodList(object_getClass(_managerClass), &count);
for (unsigned int i = 0; i < count; i++) {
SEL selector = method_getName(methods[i]);
const char *selectorName = sel_getName(selector);
// 过滤无propConfig前缀的函数
if (strncmp(selectorName, "propConfig", strlen("propConfig")) != 0) {
continue;
}
// 控件导出的属性/布局属性生成的函数分别是propConfig_* propConfigShadow_* ,定位'_'的位置,进而获取属性名
const char *underscorePos = strchr(selectorName + strlen("propConfig"), '_');
if (!underscorePos) {
continue;
}
NSString *name = @(underscorePos + 1);
// 调用函数获取属性类型
NSString *type = ((NSArray<NSString *> *(*)(id, SEL))objc_msgSend)(_managerClass, selector)[0];
// 回调类型属性,用BOOL代替
if ([type isEqualToString:@"RCTBubblingEventBlock"]) {
[bubblingEvents addObject:RCTNormalizeInputEventName(name)];
propTypes[name] = @"BOOL";
} else if ([type isEqualToString:@"RCTDirectEventBlock"]) {
[directEvents addObject:RCTNormalizeInputEventName(name)];
propTypes[name] = @"BOOL";
} else {
propTypes[name] = type;
}
}
free(methods);
Class superClass = [_managerClass superclass];
return @{
@"propTypes": propTypes,
@"directEvents": directEvents,
@"bubblingEvents": bubblingEvents,
@"baseModuleName": superClass == [NSObject class] ? (id)kCFNull : moduleNameForClass(superClass),
};
}
例子RCTWKWebViewManager返回结果如下
propTypes存放所有导出属性的属性名、类型;
directEvents、bubblingEvents存放回调类型;
baseModuleName表示控件基类,用于在JS端添加基类导出属性以得到完整属性表。
{
"propTypes": {
"allowsInlineMediaPlayback": "BOOL",
"automaticallyAdjustContentInsets":"BOOL",
"bounces" : "BOOL",
"contentInset": "UIEdgeInsets",
...
"onMessage":""BOOL,
"onShouldStartLoadWithRequest":"BOOL"
},
"directEvents": [
"topLoadingStart",
"topLoadingFinish",
"topLoadingError",
"topMessage",
"topShouldStartLoadWithRequest"
],
"bubblingEvents": [],
"baseModuleName": "RCTViewManager"
}
-
RCTUIManager
原生模块导出常量constantsToExport
用来导出所有原生控件属性信息。
这里遍历所有原生控件以收集属性信息,最终加工为一个包含所有原生控件属性信息的集合,导出到JS端。
// 收集所有导出供JS端使用的原生控件的信息
- (NSDictionary<NSString *, id> *)constantsToExport
{
return [self getConstants];
}
- (NSDictionary<NSString *, id> *)getConstants
{
NSMutableDictionary<NSString *, NSDictionary *> *constants = [NSMutableDictionary new];
NSMutableDictionary<NSString *, NSDictionary *> *directEvents = [NSMutableDictionary new];
NSMutableDictionary<NSString *, NSDictionary *> *bubblingEvents = [NSMutableDictionary new];
[_componentDataByName enumerateKeysAndObjectsUsingBlock:^(NSString *name, RCTComponentData *componentData, __unused BOOL *stop) {
NSMutableDictionary<NSString *, id> *moduleConstants = moduleConstantsForComponent(directEvents, bubblingEvents, componentData);
constants[name] = moduleConstants;
}];
return constants;
}
static NSMutableDictionary<NSString *, id> *moduleConstantsForComponent(
NSMutableDictionary<NSString *, NSDictionary *> *directEvents,
NSMutableDictionary<NSString *, NSDictionary *> *bubblingEvents,
RCTComponentData *componentData) {
NSMutableDictionary<NSString *, id> *moduleConstants = [NSMutableDictionary new];
NSMutableDictionary<NSString *, NSDictionary *> *bubblingEventTypes = [NSMutableDictionary new];
NSMutableDictionary<NSString *, NSDictionary *> *directEventTypes = [NSMutableDictionary new];
moduleConstants[@"Manager"] = RCTBridgeModuleNameForClass(componentData.managerClass);
NSDictionary<NSString *, id> *viewConfig = [componentData viewConfig];
moduleConstants[@"NativeProps"] = viewConfig[@"propTypes"];
moduleConstants[@"baseModuleName"] = viewConfig[@"baseModuleName"];
moduleConstants[@"bubblingEventTypes"] = bubblingEventTypes;
moduleConstants[@"directEventTypes"] = directEventTypes;
for (NSString *eventName in viewConfig[@"directEvents"]) {
if (!directEvents[eventName]) {
directEvents[eventName] = @{
@"registrationName": [eventName stringByReplacingCharactersInRange:(NSRange){0, 3} withString:@"on"],
};
}
directEventTypes[eventName] = directEvents[eventName];
}
// Add bubbling events
for (NSString *eventName in viewConfig[@"bubblingEvents"]) {
if (!bubblingEvents[eventName]) {
NSString *bubbleName = [eventName stringByReplacingCharactersInRange:(NSRange){0, 3} withString:@"on"];
bubblingEvents[eventName] = @{
@"phasedRegistrationNames": @{
@"bubbled": bubbleName,
@"captured": [bubbleName stringByAppendingString:@"Capture"],
}
};
}
bubblingEventTypes[eventName] = bubblingEvents[eventName];
}
return moduleConstants;
}
最终生成一张表包含所有原生控件导出属性信息<原生控件名:导出信息>
,格式如下。作为RCTUIManager
原生模块常量导出到JS端,原生模块信息导出流程详解通信机制。
{
"RCTWKWebView": {
"Manager": "WKWebViewManager",
"NativeProps" : {
"allowsInlineMediaPlayback" :"BOOL",
...
"onLoadingError" : "BOOL",
"onLoadingFinish" : "",
"onLoadingStart" : "BOOL",
"onMessage" : "BOOL",
"source" : "NSDictionary",
},
"baseModuleName" : "RCTView",
"bubblingEventTypes" : { };
"directEventTypes" {
"topLoadingError" : { "registrationName" : "onLoadingError",},
"topLoadingFinish" : { "registrationName" : "onLoadingFinish",},
...
},
},
"RCTView": {...},
...
}
3.UIManager获取原生控件信息
-
注:
通常情况下:ReactNative原生模块(特别是逻辑模块)在JS端会包装为对应的JS模块,例如RCTUIManager在JS端包装为UIManager。 这样做有诸多好处,首先Native、JS端一一对应,其次是更清晰的模块化,还是可以缓存原生模块信息。
原生控件管理者ViewManager通常不会包装,原因是原生模块信息几乎都通过UIManager获取,不会直接使用。但原生控件信息导出到JS端,会包装为对应组件,例如RCTWKWebView在JS端会包装为WebView.ios.js
,这么做的好处同上。
- UIManager实际上就做一件事情:加载所有原生控件信息并缓存,供调用者使用。
上文分析过RCTUIManager
导出常量是所有原生控件信息,根据通信机制:原生模块信息如何导入到JS端,可知相应的JS端模块UIManager能获取到这些原生控件信息。UIManager.js
脚本运行时,会遍历UIManager
模块信息,找出原生控件信息(判断是否有Manager属性),并且进行加工、缓存。
加工的过程很简单:获取相应ViewManager并把导出函数getter、常量getter添加到原生控件信息中。至此,JS端缓存的原生控件信息相对完整
,包含原生控件的导出属性、函数、常量、管理者ViewManager、基类。
UIManager把原生控件信息都命名为ViewManagerConfig
,即原生控件管理者信息,其实是就是原生控件信息。
// UIManager.js
const {UIManager} = NativeModules;
// 存放所有原生控件信息 { viewName: viewConfig }
const viewManagerConfigs = {};
// 获取控件信息
UIManager.getViewManagerConfig = function(viewManagerName: string) {
const config = viewManagerConfigs[viewManagerName];
if (config) {
return config;
}
// 防御作用,获取不到viewConfig时,使用同步函数获取试图获取
if (UIManager.lazilyLoadView && !triedLoadingConfig.has(viewManagerName)) {
...
}
return viewManagerConfigs[viewManagerName];
};
// 原生控件配置信息viewConfig
function lazifyViewManagerConfig(viewName) {
const viewConfig = UIManager[viewName];
if (viewConfig.Manager) {
// viewConfig存入组件信息表viewManagerConfigs
viewManagerConfigs[viewName] = viewConfig;
// 定义取值函数getter,用于获取Constants,即ViewManager导出常量
defineLazyObjectProperty(viewConfig, 'Constants', {
get: () => {
const viewManager = NativeModules[viewConfig.Manager];
const constants = {};
viewManager &&
Object.keys(viewManager).forEach(key => {
const value = viewManager[key];
if (typeof value !== 'function') {
constants[key] = value;
}
});
return constants;
},
});
// 定义取值函数,用于获取Commands,即ViewManager导出函数(索引)
defineLazyObjectProperty(viewConfig, 'Commands', {
get: () => {
const viewManager = NativeModules[viewConfig.Manager];
const commands = {};
let index = 0;
viewManager &&
Object.keys(viewManager).forEach(key => {
const value = viewManager[key];
if (typeof value === 'function') {
commands[key] = index++;
}
});
return commands;
},
});
}
}
// 遍历UIManager模块信息,获取、加工组件信息,并缓存
if (Platform.OS === 'ios') {
Object.keys(UIManager).forEach(viewName => {
lazifyViewManagerConfig(viewName);
});
}
// 导出UIManager对象,单例
module.exports = UIManager;
最终原生控件信息存放在viewManagerConfigs
,格式如下
{
RCTWKWebView: {
Commands: {
getConstants: 7,
goBack: 1,
goForward: 2,
...
},
Constants: {},
Manager: "WKWebViewManager",
NativeProps: {
allowsInlineMediaPlayback: "BOOL",
bounces: "BOOL",
...
onMessage: "BOOL",
},
baseModuleName: "RCTView",
bubblingEventTypes:{ },
directEventTypes: {
topLoadingStart: {registrationName: "onLoadingStart"},
topMessage: {registrationName: "onMessage"},
...
}
},
RCTView: {
...
}
...
}
4.原生控件信息处理、获取
上诉流程中,UIManager单例收集所有原生控件信息,但还不能直接使用,下面进一步分析信息的处理、使用流程。
原生控件信息传递.jpg- 1.原生控件在JS端封装对应JS组件,会执行
requireNativeComponent
函数,传入原生端导入的原生组件名。
// WebView.ios.js
const RCTWKWebView = requireNativeComponent('RCTWKWebView');
该函数其实是createReactNativeComponentClass
函数的一层包装,传入一个回调函数。
// requireNativeComponent.js
const requireNativeComponent = (uiViewClassName: string): string =>
createReactNativeComponentClass(uiViewClassName, () =>
getNativeComponentAttributes(uiViewClassName),
);
getNativeComponentAttributes
,负责从UIManager获取原生控件信息,并做进一步加工:添加基类属性;添加属性比较/处理函数。最终得到完整
的原生控件信息。它作为回调函数存放到ReactNativeViewConfigRegistry
中,以懒加载完整的原生控件信息。
// 获取原生控件信息
// getNativeComponentAttributes.js
function getNativeComponentAttributes(uiViewClassName: string) {
// 从UIManager获取原生控件信息
const viewConfig = UIManager.getViewManagerConfig(uiViewClassName);
// 添加基类原生模块信息
let {baseModuleName, bubblingEventTypes, directEventTypes} = viewConfig;
let nativeProps = viewConfig.NativeProps;
while (baseModuleName) {
const baseModule = UIManager.getViewManagerConfig(baseModuleName);
...
}
// 添加属性比较、处理函数
const validAttributes = {};
for (const key in nativeProps) {
const typeName = nativeProps[key];
const diff = getDifferForType(typeName);
const process = getProcessorForType(typeName);
validAttributes[key] =
diff == null && process == null ? true : {diff, process};
}
Object.assign(viewConfig, {
uiViewClassName,
validAttributes,
bubblingEventTypes,
directEventTypes,
});
return viewConfig;
}
- 2.createReactNativeComponentClass函数,则调用
ReactNativeViewConfigRegistry
模块的register
函数,注册原生控件信息获取回调。
// 注册原生控件信息获取回调
// createReactNativeComponentClass.js
const createReactNativeComponentClass = function(
name: string,
callback: ViewConfigGetter,
): string {
return register(name, callback);
};
ReactNativeViewConfigRegistry
模块就是一个原生控件信息注册机,它包含所有需要的原生控件信息,并且是懒加载机制。
调用register
函数注册原生控件信息获取回调,并原路返回传入的view name,作为组件名书写JSX。
// ReactNativeViewConfigRegistry.js
const viewConfigCallbacks = new Map(); // 原生控件信息获取回调函数表
const viewConfigs = new Map(); // 原生控件信息表
// 注册原生控件信息获取回调,用于从UIManager获取组件信息
exports.register = function(name: string, callback: ViewConfigGetter): string {
viewConfigCallbacks.set(name, callback);
return name;
};
// 获取原生控件信息,懒加载机制
exports.get = function(name: string): ReactNativeBaseComponentViewConfig<> {
let viewConfig;
if (!viewConfigs.has(name)) {
const callback = viewConfigCallbacks.get(name);
viewConfigCallbacks.set(name, null);
viewConfig = callback();
processEventTypes(viewConfig);
viewConfigs.set(name, viewConfig);
} else {
viewConfig = viewConfigs.get(name);
}
return viewConfig;
};
总结
1.封装组件时调用requireNativeComponent
注册一个原生控件信息获取/处理回调到原生控件信息注册机ReactNativeViewConfigRegistry
;
2.真正执行渲染,使用到对应组件,调用get
函数从原生控件信息注册机获取原生控件信息,若控件信息存在,则直接使用;否则执行原生控件信息获取/处理回调,以得到完整并原生控件信息并缓存。
这一流程驱使原生控件数据进行加工,从UIManager
流行ReactNativeViewConfigRegistry
,并且具备懒加载特性。
五、渲染流程
1.注册JS组件
- ReactNative项目会在JS执行的入口文件,注册根组件到
AppRegistry
// index.js
AppRegistry.registerComponent('Main', () => Main);
-
AppRegistry
:App注册机,准确来说是根组件注册机,因为一个应用中可存在多个根组件。AppRegistry
主要负责:根组件的注册、运行、卸载。它注册到BatchedBridge
作为JS模块,可供原生端调用。- 1.JS端通过
registerComponent
注册根组件到runnables
表中; - 2.原生端通过
runApplication
运行指定的根组件; - 3.原生端在组件销毁时,通过
unmountApplicationComponentAtRootTag
卸载指定的根组件;
- 1.JS端通过
// AppRegistry.js
const runnables: Runnables = {};
const AppRegistry = {
// 注册组件
registerComponent(appKey: string, componentProvider: ComponentProvider, section?: boolean): string {
runnables[appKey] = {
componentProvider,
run: ...
};
return appKey;
},
// 运行组件:获取注册表中的组件,运行
runApplication(appKey: string, appParameters: any): void {
runnables[appKey].run(appParameters);
},
// 卸载组件
unmountApplicationComponentAtRootTag(rootTag: number): void {
ReactNative.unmountComponentAtNodeAndRemoveContainer(rootTag);
},
}
// 注册JS模块 AppRegistry
BatchedBridge.registerCallableModule('AppRegistry', AppRegistry);
2.运行JS组件,执行渲染
- 原生端
RCTRootView
在js bundle执行完毕后,运行指定的JS组件
// RCTRootView.m
- (void)runApplication:(RCTBridge *)bridge
{
NSString *moduleName = _moduleName ?: @"";
NSDictionary *appParameters = @{
@"rootTag": _contentView.reactTag,
@"initialProps": _appProperties ?: @{},
};
// 调用js模块AppRegistry runApplication,运行组件
[bridge enqueueJSCall:@"AppRegistry"
method:@"runApplication"
args:@[moduleName, appParameters]
completion:NULL];
}
- 上述操作会执行JS模块
AppRegistry
运行根组件接口runApplication
。根据组件名appKey获取对应的根组件,调用run
运行根组件,最终执行renderApplication
进行渲染。
// AppRegistry.js
runnables[appKey] = {
// 组件获取函数
componentProvider,
// 组件运行函数, 执行render
run: appParameters => {
renderApplication(
componentProviderInstrumentationHook(componentProvider),
appParameters.initialProps, // 原生层传递过来的初始化属性
appParameters.rootTag, // rootTag
wrapperComponentProvider && wrapperComponentProvider(appParameters),
appParameters.fabric,
);
},
};
-
renderApplication
主要负责- 使用
AppContainer
包装根组件。AppContainer
主要是包含了YellowBox
之类的调试组件; - 调用
ReactNative
的render
函数进行渲染。
ReactFabric
应该是React的正在重构的下一代渲染机制,目前还没有生效。
- 使用
// renderApplication.js
function renderApplication<Props: Object>(
RootComponent: React.ComponentType<Props>, // 根组件
initialProps: Props,
rootTag: any,
WrapperComponent?: ?React.ComponentType<*>,
fabric?: boolean,
showFabricIndicator?: boolean,)
{
// 根组件RootComponent嵌入容器组件AppContainer(用于包装yellowBox等调试组件)
let renderable = (
<AppContainer rootTag={rootTag} WrapperComponent={WrapperComponent}>
<RootComponent {...initialProps} rootTag={rootTag} />
</AppContainer>
);
if (fabric) {
require('ReactFabric').render(renderable, rootTag);
} else {
// 执行渲染
require('ReactNative').render(renderable, rootTag);
}
}
-
ReactNative
根据运行环境执行相应脚本。调试环境使用ReactNativeRenderer-dev
;生产环境使用ReactNativeRenderer-prod
。这里分析调试环境。
// ReactNative.js
if (__DEV__) {
ReactNative = require('ReactNativeRenderer-dev');
} else {
ReactNative = require('ReactNativeRenderer-prod');
}
- 再往下追踪,发现上述两个文件的代码量都是万行级的,这显然不是人看的代码。当然笔者相信FaceBook工程师不会写可读性这么差的代码。经探索发现这两个不是源文件,真正的源文件可在React中查看。
React项目十分庞大,本文不作展开,我们只需知道它采用monorepo管理方式,一个项目拆分为多个独立包的。React中应用于ReactNative的包主要是如下红框所示,负责渲染、事件系统、协调等,使得React与ReactNative能链接起来,详见源码概览。
早在远古时期react-native-0.45.0,React的源码是直接引入ReactNative,这种应该比较好梳理逻辑。毕竟在无法调试代码的情况下研究源码太考验想象力了。
3.获取React计算结果
React与ReactNative的衔接是个大工程,此处省略一万字,直接分析两者最终的计算结果如何驱动原生端执行UI渲染。
Debug环境下,调用ReactNativeRenderer-dev.js
导出对象ReactNativeRenderer
的渲染函数render
执行渲染。经过一个长长的调用栈之后会执行根组件的render
(可在根组件render打断点追踪调用栈),返回一个根React Element。这就是React计算结果了,即我们用React编写的JSX代码的最终产物。
DEMO中的例子TestRender
,最终返回React Element对象如下(省略了部分信息,testID为测试id),这其实就是一棵树,包含原生控件信息。
{
$$typeof: Symbol(react.element),
type: {$$typeof: Symbol(react.forward_ref), displayName: "View",},
props: {
children: [
{
$$typeof: Symbol(react.element),
type: {$$typeof: Symbol(react.forward_ref), displayName: "View"},
props: {
style: {backgroundColor: "yellow", width: 50, height: 50},
testID: "white-yellow"
},
},
{
$$typeof: Symbol(react.element),
type: {$$typeof: Symbol(react.forward_ref), displayName: "View"},
props:{
children: [{
$$typeof: Symbol(react.element),
props:{
style: {backgroundColor: "red", width: 50, height: 50},
testID: "white-green-red"
},
type: {$$typeof: Symbol(react.forward_ref), displayName: "View"}
}],
style: {backgroundColor: "green", width: 100, height: 100},
testID: "white-green",
},
},
{
$$typeof: Symbol(react.element),
type: {displayName: "TouchableOpacity"},
props: {
style: {backgroundColor: "blue", width: 50, height: 50},
testID: "white-blue",
activeOpacity: 0.2,
onPress: ƒ
},
}
],
style: {backgroundColor: "white", paddingTop: 64},
testID: "white",
},
}
4.驱动Native执行渲染流程
-
0.RCTUIManager简析
前面提到RCTUIManager
是渲染流程的集大成者,分析渲染流程,得先分析RCTUIManager的多个容器和队列ShadowQueue
。渲染流程同样采用批处理思想。-
_rootViewTags:根控件集合,存放所有根控件,UI布局计算就是从根控件开始递归结点树。
-
_pendingUIBlocks:UI操作集合,创建控件、设置属性、改变视图层级、设置布局信息…等UI操作会先缓存,在特定时机派发到主线程。
-
_shadowViewRegistry:布局结点集合,存放整个应用的布局结点
-
_viewRegistry:原生控件集合,存放整个应用的原生控件
-
_shadowViewsWithUpdatedProps:已更新属性的shadowView集合,记录更新了属性值的布局结点,布局时机一到会遍历该集合逐一更新布局结点的YOGA值
-
_shadowViewsWithUpdatedChildren:已更新子控件的shadowView集合,记录子控件有变动的布局结点,布局时机一到会遍历该集合逐一更新控件的子控件。
-
渲染过程大多数操作发生在队列
ShadowQueue
中,它是串行队列,作为RCTUIManager
导出函数的执行队列。
上述容器的使用有严格的线程规定,以保证线程安全和UI操作在主线程执行。所有容器的创建、销毁在主线程,使用(增删改查)则有差别,_viewRegistry涉及UI操作(创建控件)需要在主线程使用,其他容器其实都是从数据层面的操作,并非正在执行UI操作,因此都在ShadowQueue执行;布局计算也在ShadowQueue执行(异步计算布局结果)。
-
-
注:通过简洁的线程管理来实现:JS&Native(ReactNativeRenderer&RCTUIManager)的交互、控件层次结构(数据层面)更新、布局结点属性更新、布局计算等都在
ShadowQueue
队列执行;控件属性更新、更改视图层级、渲染等正在操作UI则主线程执行。即:非UI操作在ShadowQueue
执行,操作UI在主线程进行,结合批处理机制,达到为主线程减负、高效渲染的目的。
@implementation RCTUIManager
{
// 根控件集合 reactTag <reactTag>
NSMutableSet<NSNumber *> *_rootViewTags;
// 暂存UI操作;
NSMutableArray<RCTViewManagerUIBlock> *_pendingUIBlocks;
// 布局结点集合 { reactTag : RCTShadowView }
NSMutableDictionary<NSNumber *, RCTShadowView *> *_shadowViewRegistry; // RCT thread only
// 控件集合 {reactTag : UIView}
NSMutableDictionary<NSNumber *, UIView *> *_viewRegistry; // Main thread only
// 已更新属性的shadowView集合 { RCTShadowView: [props key] }
NSMapTable<RCTShadowView *, NSArray<NSString *> *> *_shadowViewsWithUpdatedProps; // UIManager queue only.
// 已更新子控件的shadowView集合
NSHashTable<RCTShadowView *> *_shadowViewsWithUpdatedChildren; // UIManager queue only.
...
}
1.JS端根据计算结果,驱动原生端创建原生控件
- JS端根据计算结果,驱动原生端创建原生控件
- 生成reactTag,作为每个控件的标识;
- 从原生控件信息注册机获取对应的控件信息,并处理控件属性(过滤非法属性……);
- 调用原生模块UIManager创建原生控件
// ReactNativeRenderer-dev.js
function createInstance(
type,
props,
rootContainerInstance,
hostContext,
internalInstanceHandle
) {
// 创建组件tag
var tag = allocateTag();
// 获取原生控件信息
var viewConfig = ReactNativeViewConfigRegistry.get(type);
var updatePayload = create(props, viewConfig.validAttributes);
// 调用原生模块,创建原生控件
UIManager.createView(
tag, // reactTag
viewConfig.uiViewClassName, // viewName
rootContainerInstance, // rootTag
updatePayload // props
);
var component = new ReactNativeFiberHostComponent(tag, viewConfig);
precacheFiberNode(internalInstanceHandle, tag);
updateFiberProps(tag, props);
return component;
}
- 原生端创建原生控件和对应的布局结点;设置布局属性、控件属性值;
设置属性的过程相对冗长,详见源码,大致流程是:根据属性名、是否是布局结点(isShadowView),构建导出属性的导出函数SEL以获得属性类型,进而构建出属性setter,并包装为Block,执行Block设置属性,最终控件属性值设置到原生控件,布局属性值设置到布局结点ShadowView
。
// RCTUIManager.m
// 创建原生控件
RCT_EXPORT_METHOD(createView:(nonnull NSNumber *)reactTag
viewName:(NSString *)viewName
rootTag:(nonnull NSNumber *)rootTag
props:(NSDictionary *)props)
{
RCTComponentData *componentData = _componentDataByName[viewName];
// 创建对应的shadowView,存入容器_shadowViewRegistry,设置属性
RCTShadowView *shadowView = [componentData createShadowViewWithTag:reactTag];
if (shadowView) {
[componentData setProps:props forShadowView:shadowView];
_shadowViewRegistry[reactTag] = shadowView;
RCTShadowView *rootView = _shadowViewRegistry[rootTag];
shadowView.rootView = (RCTRootShadowView *)rootView;
}
// 主线程 创建NativeView,存入View注册表_viewRegistry
__block UIView *preliminaryCreatedView = nil;
void (^createViewBlock)(void) = ^{
if (preliminaryCreatedView) {
return;
}
preliminaryCreatedView = [componentData createViewWithTag:reactTag];
if (preliminaryCreatedView) {
self->_viewRegistry[reactTag] = preliminaryCreatedView;
}
};
RCTExecuteOnMainQueue(createViewBlock);
// 设置控件属性(对UIView的操作都先暂存在_pendingUIBlocks,批处理)
[self addUIBlock:^(__unused RCTUIManager *uiManager, __unused NSDictionary<NSNumber *, UIView *> *viewRegistry) {
createViewBlock();
if (preliminaryCreatedView) {
[componentData setProps:props forView:preliminaryCreatedView];
}
}];
// 更新props,存入_shadowViewsWithUpdatedProps
[self _shadowView:shadowView didReceiveUpdatedProps:[props allKeys]];
}
2.根据计算结果,设置视图层次结构
// ReactNativeRenderer-dev.js
function finalizeInitialChildren( parentInstance, type, props, rootContainerInstance, hostContext) {
if (parentInstance._children.length === 0) {
return false;
}
var nativeTags = parentInstance._children.map(function(child) {
return typeof child === "number" ? child // Leaf node (eg text): child._nativeTag;
});
UIManager.setChildren(
parentInstance._nativeTag, // 父控件tag
nativeTags // 子控件tag集合
);
return false;
}
原生端布局结点树更改层次结构,此时仅从数据层面改变层次结构,并未真正addSubVew
(详见UIView+React.m)
// RCTUIManager.m
RCT_EXPORT_METHOD(setChildren:(nonnull NSNumber *)containerTag
reactTags:(NSArray<NSNumber *> *)reactTags)
{
RCTSetChildren(containerTag, reactTags,(NSDictionary<NSNumber *, id<RCTComponent>> *)_shadowViewRegistry);
[self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry){
// 仅仅是数据层面,并不正真改成视图层次
RCTSetChildren(containerTag, reactTags,(NSDictionary<NSNumber *, id<RCTComponent>> *)viewRegistry);
}];
[self _shadowViewDidReceiveUpdatedChildren:_shadowViewRegistry[containerTag]];
}
3.布局时机一到,真正执行UI渲染
- _layoutAndMount按顺序做了以下几件事情,详见源码或DEMO
-
_dispatchPropsDidChangeEvents
向属性值发生改变的原生控件/布局结点发送消息,主要驱使布局结点更新YOGA属性值; -
_dispatchChildrenDidChangeEvents
向视图层次结构发生改变的原生控件/布局结点发送消息,主要驱使原生控件更新视图树(addSubview); - 遍历根结点,调用
uiBlockWithLayoutUpdateForRootView
,封装渲染操作。
先驱动根结点执行layout操作,底层会从根结点开始递归,使用yoga计算布局结果(shadowView.layoutMetrics);
再把正真的布局(reactSetFrame)、动画(performAnimations)操作封装为一个uiBlock并收集到UI操作容器_pendingUIBlocks
- 最后调用
flushUIBlocksWithCompletion
,统一把所有UI操作派发到主线程执行,包括最后一个渲染/动画操作block
-
// RCTUIManager.m
// 布局时机
- (void)batchDidComplete
{
[self _layoutAndMount];
}
- (void)_layoutAndMount
{
// 通知 RCTShadowView、RCTComponent 属性已更新
[self _dispatchPropsDidChangeEvents];
// 通知 RCTShadowView、RCTComponent 控件/布局结点层次结构已更新(subView已改变)
[self _dispatchChildrenDidChangeEvents];
// 构建一个UI渲染、执行布局动画的Block,添加到_pendingUIBlocks
for (NSNumber *reactTag in _rootViewTags) {
RCTRootShadowView *rootView = (RCTRootShadowView *)_shadowViewRegistry[reactTag];
[self addUIBlock:[self uiBlockWithLayoutUpdateForRootView:rootView]];
}
// 统一执行所有 _pendingUIBlocks,包括UI渲染
[self flushUIBlocksWithCompletion:^{
[self->_observerCoordinator uiManagerDidPerformMounting:self];
}];
}
// 构建一个UI渲染、执行布局动画的Block
- (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView *)rootShadowView
{
...
return block;
}
-
布局时机理解:渲染过程中布局的时机是
batchDidComplete
,追溯回JSIExecutor.cpp
可知这个时间点就是执行Native call JS、 JS callback后把所有JS端暂存JS call Native调用信息传递给原生端,原生端发起执行(并未执行完毕)后,就触发布局。为何在这个时间点布局,笔者的猜想如下(不一定准确,欢迎探讨
):- 1.首先明确一个前提:UI渲染的优先级无疑是最高的,原生开发中UI操作都是在主线程进行。因此JS端的计算结果(通常表现为原生控件信息的更新,即UI数据的更新)必须及时发送到原生端,执行UI更新。
- 2.再回顾前言对ReactNative渲染原理的介绍:
用户行为触发原生UI控件交互事件/回调事件从原生端传递到JS端,JS端处理业务逻辑,得到新的UI数据,再次传递到原生端,驱动原生模块进行新一轮UI更新。
- 例子1:用户点击按钮触发一个弹窗。点击事件在
RCTRootContentView
通过RCTTouchHandler
、RCTEventDispatcher
,最终调用JS模块RCTEventEmitter
函数receiveEvent
通知JS端处理UI交互事件(Native call JS
)。JS端收到信号后进行业务逻辑处理,计算结果是带弹窗的新UI数据。数据需要立即传递到原生端进行UI更新,绘制弹窗,在Native Call JS执行完毕后进行无疑是最及时的,这就是布局时机1。 - 例子2:用户调用原生社交分享模块,完毕需要显示toast条提示用户分享结果。JS端发起一个带回调JS call Native调起原生模块进行社交分享,原生端分享完毕,执行
JS callback
通知JS端处理分享结果。JS端收到信号后进行业务逻辑处理,计算结果是带toast提示条的新UI数据。数据需要立即传递到原生端进行UI更新,绘制toast提示条,在JS callback执行完毕后进行是最及时的,这就是布局时机2。
- 例子1:用户点击按钮触发一个弹窗。点击事件在
至此,UI渲染的基本流程就分析完毕了。
六、JS端调用原生控件导出函数
JS端调用原生控件的导出函数,流程如下
- 1.JS端调用
RCTUIManager
导出函数dispatchViewManagerCommand
,传递reactTag、Command - 2.触发原生端走JS call Native流程的后半部分,最终定位到对应原生控件的函数(可参考
WebView.ios.js
)
// RCTUIManager.m
/*
向ViewManager派发命令:用于RN端调用控件导出函数
reactTag 控件标签
commandID 命令id
commandArgs 参数
*/
RCT_EXPORT_METHOD(dispatchViewManagerCommand:(nonnull NSNumber *)reactTag
commandID:(NSInteger)commandID
commandArgs:(NSArray<id> *)commandArgs)
{
RCTShadowView *shadowView = _shadowViewRegistry[reactTag];
RCTComponentData *componentData = _componentDataByName[shadowView.viewName];
Class managerClass = componentData.managerClass;
RCTModuleData *moduleData = [_bridge moduleDataForName:RCTBridgeModuleNameForClass(managerClass)];
id<RCTBridgeMethod> method = moduleData.methods[commandID];
// 带上reactTag,用于获取对应Native控件
NSArray *args = [@[reactTag] arrayByAddingObjectsFromArray:commandArgs];
// 调用控件导出函数
[method invokeWithBridge:_bridge module:componentData.manager arguments:args];
}
// RCTWKWebViewManager.m
RCT_EXPORT_METHOD(goBack:(nonnull NSNumber *)reactTag)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RCTWKWebView *> *viewRegistry) {
RCTWKWebView *view = viewRegistry[reactTag];
if (![view isKindOfClass:[RCTWKWebView class]]) {
} else {
[view goBack];
}
}];
}
七、结语
- 这一系列文章就到此为止了,希望有助于大家理解ReactNative源码;
- ReactNative还有很多设计值得研究,例如原生如何端驱动JS端定时任务、观察者模式的设计等;