ReactNative启动过程简析

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.jsstart命令,其最终走到./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的方式调试。如下图所示:

react-native-xcode.sh

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的过程中,主要做了这些事:

  1. 创建了RN线程_jsThread,并开启了runloop
  2. 初始化了所有不能被懒加载的native模块。
  3. 初始化了NativeToJSBridgeJSToNativeBridge,用于Native端与JS端的通信。
  4. 创建了JSCExecutor,它是实际上的最主要的方法执行者。JSCExecutor创建了一个global的JSContext
  5. loadSource:也就是在这一步,调用了[RCTJavaScriptLoader loadBundleAtURL:onProgress:onComplete:]对bundle进行加载(不分包的情况下),主要是将bundle以NSData的方式加载到内存中。
  6. 在上述步骤都完成后,会调用executeSourceCode将已经加载到内存中的bundle执行。这一步是在[bridge enqueueApplicationScript:url:onComplete:completion]中进行的。
  7. 在第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.jsrunApplication方法,并将moduleNameinitialProps等参数传到js端。其中,moduleName就是在业务入口文件index.ios.js中注册的业务名称:

AppRegistry.registerComponent('QRMedalHallRN', () => QRMedalHallRN);

Stage 4. 完成!

控制权交给js端。AppRegistry.js中保存了一份所有通过AppRegistry.registerComponent()注册的业务的映射表runnables,其key为appKey,也就是上面说的moduleName。每个key对应一个run()函数。

在收到Native端传过来的moduleNameinitialProps参数后,会从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)

UIManagercreateView 方法也是 js 调用 Native 创建视图的入口。

至此,初始化工作完成。

总结

这篇文章归纳整理了一下RN初始化流程。虽然这些流程大部分都被封装成了几个简单的接口,但了解这一流程的原理还是有好处的,比如说,在分工程调试(一个主工程,一个RN单独的工程)的时候,可以修改一些参数,使得主工程可以直接读取到RN工程中的业务js代码。

刚刚接触RN(前端)的知识,如果有说的不对的地方还请指教。^^


Reference

ReactNative源码解析——通信机制详解系列

React Native 源码导读(零) – 创建/运行/调试

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容