ReactNative(以下简称RN)是近年移动端非常火的技术。我们也在前段时间用RN实现了一个小的功能模块,上线效果还可以。因此,暂时对前一阶段的工作进行一下梳理和小结。这一系列将从原理、实践的角度谈谈RN,以及在实现过程中的一些经(da)验(keng)。
这篇文章,主要从启动流程的角度,谈谈在启动背后,RN都做了些什么。
对RN的探讨和总结都基于0.46.4版本,下同。
假定,我们已经通过react-native init
命令,或是集成到现有项目中的方式,拥有了一个可以跑起来的RN项目了。当我们在xcode中点击了编译按钮,到最终,一个让人欣喜的app在模拟器或者设备中运行了起来,这中间发生了些什么呢。
Stage 1. 准备工作
首先,第一阶段和两个脚本有关。
在React.xcodeproj
工程的build phase
中,可以看到Start Packager
。这里会执行一段脚本:
if [ -z "${RCT_NO_LAUNCH_PACKAGER+xxx}" ] ; then
if nc -w 5 -z localhost 8091 ; then
if ! curl -s "http://localhost:8081/status" | grep -q "packager-status:running" ; then
echo "Port 8081 already in use, packager is either not running or not running correctly"
exit 2
fi
else
open "/Users/gaoyang/Documents/code/QRMedalHallRN/node_modules/react-native/scripts/launchPackager.command" || echo "Can't start packager automatically"
fi
fi
在这里执行了launchPackager.command
,该命令会调用./local-cli/cli.js
的start
命令,其最终走到./local-cli/server/runServer.js - runServer()
中,创建了一个server,端口号默认为8081,用于接下来客户端请求本地js文件。
在主工程的build phase
中,同样会执行一段脚本Bundle React Native code and images
:
export NODE_BINARY=node
/Users/gaoyang/Documents/code/QRMedalHallRN/node_modules/react-native/scripts/react-native-xcode.sh
这个脚本的作用主要是判断各种环境,条件,来判断是应当采用本地server模式,还是静态bundle模式来加载业务js文件。
- 本地server模式,即从上面所说的创建的本地server实时获取经过编译/打包后的bundle文件;
- 静态bundle模式,是指将业务js文件打包成bundle后,置入asset中,当做静态资源来使用。
首先,脚本会判断是dev/release环境,模拟器/真机。如果是dev环境+模拟器,那么什么都不会做,因为默认会采用本地server模式。如果是dev环境+真机,或者是release环境,那么会调用到./local-cli/cli.js bundle
命令,将本地所有js文件打包成main.jsbundle
,并置入app bundle中。此外,如果是dev环境+真机,还会获取当前网络ip地址,写入ip.txt
并置入app bundle,这是为了真机调试也可以采用server的方式调试。如下图所示:
Stage 2. 获取bundle地址
接下来,就到了iOS代码中。加载RN业务的时候,我们得知道从哪里获取业务的bundle。其实这一步是和上面react-native-xcode.sh
干的活儿对应的。这里逻辑主要在[RCTBundleURLProvider jsBundleURLForBundleRoot:fallbackResource:]
方法中。该方法就是去获取业务所需的bundle路径。
该方法中,同样会判断是否是Dev/Release环境。如果是Release环境,那么直接从[NSBundle mainBundle]
中获取上面生成好的main.jsbundle
文件;如果是Dev环境,那么会去获取本地server的host地址,即上面写入的ip.txt
文件,或者采用localhost
:
static NSString *ipGuess;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *ipPath = [[NSBundle mainBundle] pathForResource:@"ip" ofType:@"txt"];
ipGuess = [[NSString stringWithContentsOfFile:ipPath encoding:NSUTF8StringEncoding error:nil]
stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]];
});
NSString *host = ipGuess ?: @"localhost";
接着,会发送一条网络请求到该server,判断server是否处于running状态。如果这里失败了,那么还是会从[NSBundle mainBundle]
中取静态的main.jsbundle
文件;否则,就从server请求实时的bundle文件。
Stage 3. 初始化环境
在上一步,拿到了业务bundle地址后,就可以着手准备初始化环境了。这里分两种:一种是RN默认的不分包加载的方式,一种是业界采用的分包加载的方式。这两种方式的区别在于RCTBridge
的初始化时机不同(RCTBridge
是Native端负责Native与JS通信的桥梁):
- 不分包加载
不分包加载的情况下,通过上一步取到业务bundle的地址后,就可以直接调用
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"QRMedalHallRN"
initialProperties:nil
launchOptions:launchOptions];
方法来初始化一个rootView
了。在这个方法中会首先去初始化RCTBridge
,在初始化的过程中会调用[RCTJavaScriptLoader loadBundleAtURL:onProgress:onComplete:]进行bundle的加载。
- 分包加载
分包加载的情况下,通常会将common bundle
进行预加载。RCTBridge
在这时就已经初始化完成了。后面的各个业务bundle可以使用同一个RCTBridge
来创建RCTRootView
,不必重复创建。因此,在分包加载的情况下,通过上一步取到业务bundle的地址后,直接手动调用[RCTJavaScriptLoader loadBundleAtURL:onProgress:onComplete:]进行bundle的加载。
初始化RCTBridge
的过程中,主要做了这些事:
- 创建了RN线程
_jsThread
,并开启了runloop
。 - 初始化了所有不能被懒加载的native模块。
- 初始化了
NativeToJSBridge
和JSToNativeBridge
,用于Native端与JS端的通信。 - 创建了
JSCExecutor
,它是实际上的最主要的方法执行者。JSCExecutor
创建了一个global的JSContext
。 -
loadSource
:也就是在这一步,调用了[RCTJavaScriptLoader loadBundleAtURL:onProgress:onComplete:]对bundle进行加载(不分包的情况下),主要是将bundle以NSData
的方式加载到内存中。 - 在上述步骤都完成后,会调用
executeSourceCode
将已经加载到内存中的bundle执行。这一步是在[bridge enqueueApplicationScript:url:onComplete:completion]中进行的。 - 在第6步执行完毕后,会做两件事情:创建一个
CADisplayLink
,并添加到RN线程的runloop
中;并抛出一个RCTJavaScriptDidLoadNotification
,通知jsBundle已经完成加载。
关于RCTBridge
具体的初始化细节,以及Native与JS通信的原理和过程,请参考我师父的文章
RCTRootView
在接收到RCTJavaScriptDidLoadNotification
通知后,会创建一个RCTRootContentView
,用于页面的实际展示。然后,会调用runApplication
方法:
- (void)runApplication:(RCTBridge *)bridge
{
NSString *moduleName = _moduleName ?: @"";
NSDictionary *appParameters = @{
@"rootTag": _contentView.reactTag,
@"initialProps": _appProperties ?: @{},
};
RCTLogInfo(@"Running application %@ (%@)", moduleName, appParameters);
[bridge enqueueJSCall:@"AppRegistry"
method:@"runApplication"
args:@[moduleName, appParameters]
completion:NULL];
}
该方法调用了js端,AppRegistry.js
的runApplication
方法,并将moduleName
,initialProps
等参数传到js端。其中,moduleName
就是在业务入口文件index.ios.js
中注册的业务名称:
AppRegistry.registerComponent('QRMedalHallRN', () => QRMedalHallRN);
Stage 4. 完成!
控制权交给js端。AppRegistry.js
中保存了一份所有通过AppRegistry.registerComponent()
注册的业务的映射表runnables
,其key为appKey
,也就是上面说的moduleName
。每个key对应一个run()
函数。
在收到Native端传过来的moduleName
,initialProps
参数后,会从runnables
中找到该注册过的业务,执行相应的run()
函数。run()
函数中最终调用了ReactNative.render()
函数。接着,RN会根据配置,决定是采用新的React引擎Fiber
,还是老的Stack
来进行渲染。目前,还是使用老的Stack
进行渲染,React 16中使用Fiber
。
在ReactNativeStack-dev
中,我们可以看到,最终调用到了mountComponent
函数来进行渲染:
mountComponent: function(transaction, hostParent, hostContainerInfo, context) {
var tag = ReactNativeTagHandles_1.allocateTag();
this._rootNodeID = tag, this._hostParent = hostParent, this._hostContainerInfo = hostContainerInfo;
for (var key in this.viewConfig.validAttributes) this._currentElement.props.hasOwnProperty(key) && deepFreezeAndThrowOnMutationInDev(this._currentElement.props[key]);
var updatePayload = ReactNativeAttributePayload_1.create(this._currentElement.props, this.viewConfig.validAttributes), nativeTopRootTag = hostContainerInfo._tag;
return UIManager.createView(tag, this.viewConfig.uiViewClassName, nativeTopRootTag, updatePayload),
ReactNativeComponentTree_1.precacheNode(this, tag), this.initializeChildren(this._currentElement.props.children, tag, transaction, context),
tag;
}
可以看到,这里将代码控制权又交给了 Native 侧的 UIManager
,调用了 createView
方法,在 Native 侧进行页面、视图的创建等。
RCT_EXPORT_METHOD(createView:(nonnull NSNumber *)reactTag
viewName:(NSString *)viewName
rootTag:(__unused NSNumber *)rootTag
props:(NSDictionary *)props)
UIManager
的createView
方法也是 js 调用 Native 创建视图的入口。
至此,初始化工作完成。
总结
这篇文章归纳整理了一下RN初始化流程。虽然这些流程大部分都被封装成了几个简单的接口,但了解这一流程的原理还是有好处的,比如说,在分工程调试(一个主工程,一个RN单独的工程)的时候,可以修改一些参数,使得主工程可以直接读取到RN工程中的业务js代码。
刚刚接触RN(前端)的知识,如果有说的不对的地方还请指教。^^
Reference
ReactNative源码解析——通信机制详解系列
React Native 源码导读(零) – 创建/运行/调试