React Native 初探(iOS)
不知从何时起,移动端App开发,采用Native还是使用Web的争论不绝于耳。二者的优缺点不再赘述。Web App当然是开发者期待的理想结果,但是由于Native App在用户体验上的绝对碾压,大部分移动端App还是采用Native的方式,少数架构复杂、对Web依赖较多的App,会采用一种称为Hybrid(Web + Native)的开发方式,在iOS上,Native通过-[UIWebView stringByEvaluatingJavaScriptFromString:]调用Web,而Web则是通过设置WebView iframe的src,搭建JSBridge进行通讯。由于微信和手机支付宝的的成功,Hybrid App这种开发方式确实引起了关注,但从我这么一个最底层iOS开发者的技术角度来说,这种JSBridge的通讯方式,实在不是特别高明,能解决的场景也十分有限。
前几天FB正式推出了React Native。由于惯性思维,我总想着往它身上贴个「Web」或者「Native」或者「Hybrid」的标签,可是贴上去扯下来,并没有一个适合的标签。事实上,React Native重新定义了一种新的模式。
浏览器引擎是如何工作的
在说React Native之前,让我们以WebKit为例,先扯一扯一个浏览器引擎的工作流程。从下图可以看出,一个网页的生命周期,大致经历了加载、解析、排版、绘制(JS引擎暂时不提)。
接触过iOS平台上的简易的浏览器引擎,大致的工作流程,也是如此。由于加载流程涉及网络模块,部分排版和渲染流程涉及Native UI控件,为解决不同平台的差异性,一般是抽象接口,由不同平台实现各自的网络模块和网页的绘制。
简单来说,一个浏览器渲染引擎,其实就是将网页从服务器或者本地load下来,用一套规则解释这个网页,最后用平台最舒服的方式,展现到屏幕上去。
React Native
一个浏览器引擎
由于对浏览器印象深刻,这是React Native给我的第一印象。由于我对前端的了解,只停留在html和Javascript的简单语法上,完全不知ReactJS为何物,所以我只能尝试着从开源的iOS React Native的OC端代码,解释一下。
- 加载:OC层加载JS源数据(可以称为:使用ReactJS框架的?),并利用JavascriptCore.framework搭建起OCBridge,作为和JS层通讯的工具。
- 解析:解析过程由JS端完成,通过JSBridge,调用OC层将解析结果映射到Native(事实上并没有JSBridge,后面细讲)。映射结果包括了视图的层次结构,Native UI节点的属性值(颜色、文字内容等)。
- 排版:OC层通过css-layout确定节点的位置。
- 绘制:Native UI节点进行drawRect。
得益于JavascriptCore,React Native能够抛弃WebView直接运行JS,在React Native,OC层只负责控制程序生命周期,以及提供平台Native控件的工作;而JS层则负责提供数据,响应交互事件,充当了DataSource和Delegate的角色。
通信机制
这里的通信,是指JS和OC之间的通信。
前面已经提到,OCBridge是利用JavascriptCore直接调用JS代码的。OC层实现这个类的是RCTBridge,目前的代码是使用RCTContextExecutor作为具体的执行者。JavascriptCore是iOS7才开放的接口,不过目前的代码还有另外一套RCTWebViewExecutor,里面用的是通过UIWebView调用JS,可能是为了以后兼容旧版本的iOS。使用JavascriptCore最显而易见的优势就是,整个执行过程都可以在后台线程执行,事实上RCTContextExecutor单独开了一个名为「com.facebook.React.JavaScript」的线程,供自己使用。
上面只提到OCBridge,那JSBridge呢?
答案是,没有JSBridge。前面提到,OC层提供Native控件,JS层更多地是扮演DataSource和Delegate的角色。回想一下UITableview的使用,为UITableview设置DataSource和Delegate之后,使用者并不需要关心UITableview是如何被创建绘制,以及如何监听点击长按之类的交互事件。同理,JS层作为使用者,并不需要关心Native事件是如何触发的,需要关心的是,当事件触发时该如何响应。所以,一个原本需要双向通信的机制,被简化成单向通信。
这个机制,可以通过查看 -[RCTBridge enqueueJSCall:args:]这个函数的Callers来验证(这个函数是OC层调用JS的入口函数),它的 Callers包括了:Device Event(如前后台切换)、Input State(如控件Value改变)、Timer回调、Touch事件回调等等。
那JS层是如何实现调用OC层的呢?是通过返回值。在事件触发OC层调用JS之后,会获得一段JSON数据作为返回值,OC层只需要按照协议,解析这段JSON数据,依次调用Native代码即可。
通信协议
JS调用OC的协议,是-[RCTBridge setUp]的时候,通过 RCTRemoteModulesConfig()创建并传给JS层的。 RCTRemoteModulesConfig()主要做了几个事情:
- 通过 RCTBridgeModuleClassesByModuleID() 遍历所有的OC类,取出所有符合 RCTBridgeModule protocol的module,以moduleID做标识。
- 遍历第一步取到的类,通过RCTExportedMethodsByModuleID()取出每一个类暴露给JS层的OC method,以methodID做标识,打包到module中
- 第二步中,暴露给JS的method,接口实现的第一句都会加上RCT_EXPORT(js_name)这个宏(实现机制十分奇特,这里不提)。
- 假如module需要传递给JS一些常量(比方说Native UI控件的属性枚举值),则通过实现-[RCTBridgeModule constantsToExport],打包到module中。
- 将所有的module打包成Config Dictionary
当JS返回JSON数据时,实际上返回了一段包含了moduleID和methodID的队列,OC层按照协议的约定,执行对应方法。
至于OC调用JS的协议,也是通过module、method来标识的。不过这些module、method都是OC层写死的字符串,应该是和JS强绑定的,没有啥特殊之处。
解析和排版
浏览器引擎,离不开的就是dom tree 和render tree。简单来说,dom tree 是根据源数据解析而来的,包含了原始的节点信息;而render tree 则是dom tree + css。排版的目的,就是生成render tree,确定每个节点在屏幕上的大小位置。
在React Native中,解析过程是在JS层完成的,原理未知。在OC层,RCTUIManager负责将JS层的解析结果,映射到OC层的视图层级,它本身不做任何的解析操作,只是提供方法,让JS层调用而已。最终dom tree映射到OC层的结果,是一棵「RCTShadowView tree」。RCTShadowView这个名字也起得很有意思,它不是真正展现的视图,只是一个映射结果而已,每一个RCTShadowView对应一个真正的视图。RCTShadowView的另一个意义在于,它拥有一个成员变量cssNode,可以通过FB的开源项目css-layout(代码里面难得一见的两个C文件),完成排版。剩下的细节工作,就交给RCTShadowView对应的真实视图了。
其实一开始并没有打算看源码的,只是因为Demo中一张图片无法显示,让我不得不调试图片下载模块来确定问题 -_-|||(图片下载使用的是NSURLSession,这货也是iOS7才有的接口,看来React Native还没打算支持旧版本的iOS)。时间匆忙,水平有限,肯定错误连篇,还望指正。