浅谈React Native与实现机制
1. React Native简单介绍
目前App开发的主流方式有三种: Native开发,Hybird开发以及Web开发
原生Native开发
主要采用Object-C/Swift方式进行原生开发。运行效率高,流畅,用户体验好,可以做各种复杂的动画效果。平台独立性,代码无法在其他平台上运行,无法做到跨平台。更新审核周期比较长,不利于App问题的快速修复
Hybird开发
以原生开发为主。
更新频繁,活动页面,运营页面等采用H5方式接入。定义好原生功能与H5之间的协议,拦截特定的URL Schema进行原生功能的调用,App调用H5提供的js方法,给H5传值和通知H5
Web开发
是Web App,以Web为主,通过js或者插件方式调用原生功能,如拨打电话,位置服务等。
一套Web代码可以分别在各个平台上运行。受限制与UIWebView,app的性能和体验都无法与纯原生app相提并论。比较有代表性的:采用cordova和ionic进行web app开发,通过开发原生插件功能供Web端调用
React Native的出现
不同的开发方式都在解决如下的几个问题
- 使得APP的体验效果和原生应用一样好
- 跨平台,提高项目代码的重用性
- 应对广告或者活动更新,能够进行热替换而不用进行APP新发布
因此Facebook在2015年发布了React Native框架,旨在帮助前端程序员解决如上的棘手问题,在发布当初,相比于其他Hybird框架,React Native有如下的特点
- 基于组件开发,提供代码的复用率。
- 各个平台功能代码可以进行复用,官方数据表明:iOS和android功能代码可以达到90%以上的复用。
- 不用Webview,彻底摆脱了Webview的限制:交互和性能问题。
- 相对其它Hybrid 方案,React Native性能更好,用户体验更接近原生。
- 减少编译时间,提高开发效率。
- 可以采用热更新方式进行app功能升级和问题修复,提高app的迭代率和开发效率
React Native实例
- 在JS语言中嵌入了HTML和CSS的元素,这种被扩展了的JavaScript语言称为jsx
- React Native框架中,JavaScript内存中维护了一个Virtual DOM,JSX内容在Virtual DOM中被转化翻译成真实的DOM树,Virtual DOM与真实显示的DOM保持一一对应的关系
- 当界面发生变化时,得益于高效的 DOM Diff 算法,我们能够知道 Virtual DOM 的变化,从而高效的改动 DOM,避免了重新绘制 DOM
JSX实例
var MyComponent = React.createClass({
handleClick: function() {
this.refs.myTextInput.focus();
},
render: function() {
return (
<div>
<input type="text" ref="myTextInput" />
<input type="button" value="Focus the text input" onClick={this.handleClick} />
</div>
);
}
});
ReactDOM.render(
<MyComponent />,
document.getElementById('example')
);
2. 动态配置
在当下移动端App越来越人性化的趋势下,App的更新迭代速度很快,但是限制于App Store和安卓市场的应用版本更新限制,如果每次界面上有部分需要更新,例如广告更换,页面布局调整等,都需要通过发布一个新的版本来实现,着实对应用商和用户来说都是不合理的,因此如果有一种方式可以动态的配置移动端界面便是极好的。
很多时候,我们都是利用 JSON 文件实现动态配置的效果,它的核心流程如下
JSON实现动态配置
- 通过 HTTP 请求获取 JSON 格式的配置文件。
- 配置文件中标记了每一个元素的属性,比如位置,颜色,图片 URL 等。
- 解析完 JSON 后,我们调用 Objective-C 的代码,完成 UI 控件的渲染。
通过这种方法,我们实现了在后台配置 app 的展示样式。从本质上来说,移动端和服务端约定了一套协议,但是协议内容严重依赖于应用内要展示的内容,不利于拓展。也就是说,如果业务要求频繁的增加或修改页面,这套协议很难应付。
然而这种通过JSON通信,配置一些可选项的方式在很多情况下都不能够满足现在快速迭代开发的App,如果想要改变一些业务逻辑或者进行一些复杂度比较高的修改操作,则客户端只读取JSON配置项是做不到的,无法调用处理业务逻辑的方法等,不具备调试功能。
各种移动平台支持JavaScript
然而,基于现在移动设备都支持JavaScript代码的执行这一条件(例如iOS上内置了JavaScript Core来执行JavaScript代码),React Native的推出发挥了这一优点,通过JavaScript代码,不仅仅只是传递简单的配置信息,更可以进行业务逻辑的处理。
Learn once, write everywhere
和其他Hybird框架所宣传的"Write once, run everywhere"不同,React Native其实不能真正意义上称为"跨平台"框架,因为它的本质是使用了各个移动平台都支持JavaScript语言,React Native帮我们做好了将JavaScript代码转化成Object-C或者Java语言,并且帮我们处理好了各种回调问题,因此表面上我们只需要编写JavaScript语言,即可在不同的平台上展现应用,这也是React Native的开发初衷: 分别开发安卓和iOS而不用写一行原生代码
3. 通信机制
iOS -- JavaScript / Objective-C
我们虽然使用的是React Native框架,但还是需要依赖UIKit等框架,从而调用Objective-C代码以在iOS平台上执行,JavaScript其实只是为我们提供了编写业务逻辑和前端界面的辅助工具,React Native在iOS上能够运行的实质是利用JS代码调用OC代码执行,我们需要关注的重点就是JS与OC之间的通信机制,包括JS是如何去调用OC代码,又如何实现回调功能,这是React Native的核心功能之一。
我们都知道,JS作为一种脚本语言,是不需要像C语言那样被编译链接然后执行,在执行脚本语言时,会在运行时动态地进行词法和语法的分析,然后生成一课抽象语法树和对应的字节码,由JS解释器等将字节码转化成对应的机器码,而整个流程都是由JS引擎来加以完成。
JavaScript Core
在iOS平台下,React Native利用了iOS提供的JavaScript Core作为JS解析器,然而RN并没有完全使用JS Core中提供的JS-OC互调的特性,而是自己实现了一套通用的方案,以便兼容不同的版本
OC调用JS
OC向JS传信息有现成接口,stringByEvaluatingJavaScriptFromString方法可以直接在当前context上执行一段JS脚本,并且可以获取执行后的返回值,这个返回值就相当于JS向OC传递信息。
JSContext *context = [[JSContext alloc] init];
JSValue *jsVal = [context evaluateScript:@"21+7"];
int iVal = [jsVal toInt32];
在上述例子中,JSContext
代表了当前JS的执行环境,evaluateScript
方法则会去执行后面跟着的js脚本内容,返回值会存放在 JSValue
中,从而完成OC调用JS并获取JS返回信息。
JS调用OC
React Native基于上述OC调用JS的方法,经过一些封装在OC里面定义了一个模块方法,JS可以直接调用这个模块方法并且可以注册回调函数。
//Objective-C
@implement RCTSQLManager
- (void)query:(NSString *)queryData successCallback:(RCTResponseSenderBlOCk)responseSender
{
RCT_EXPORT();
NSString *ret = @"ret"
responseSender(ret);
}
@end
//JavaScript
RCTSQLManager.query("SELECT * FROM table", function(result) {
//result == "ret";
});
如上图所示,在OC内部定义了一个模块RCTSQLManager,并且在模块内部定义了方法 -query: successCallback
;我们在JS中可以直接调用RCTSQLManager的query方法并且注册回调函数。
模块配置表
-
取出所有可被调用的模块,每个可被调用模块类都实现了
RCTBridgeModule
接口,可以通过runtime接口objc_getClassList
或objc_copyClassList
取出项目里所有类,然后逐个判断是否实现了RCTBridgeModule
接口,就可以找到所有模块类。 -
取出模块中所有可被调用的方法,模块方法里有句代码:
RCT_EXPORT()
,模块里的方法加上这个宏就可以实现暴露给JS,无需其他规则,这个宏的作用是用编译属性__attribute__
给二进制文件新建一个section,属于__DATA
数据段,名字为RCTExport
,并在这个段里加入当前方法名。编译器在编译时会找到__attribute__
进行处理,为生成的可执行文件加入相应的内容。
#define RCT_EXPORT(JS_name) __attribute__((used, section("__DATA,RCTExport" \
))) static const char *__rct_export_entry__[] = { __func__, #JS_name }
- 在读取完所有可被调用模块和可被调用方法后,OC告诉JS有哪些模块,哪些方法是可以被JS调用的,在这里的实现机制是OC生成一份模块配置表然后OC端和JS端分别持有这一份配置表,表的内容大致如下,可以看到每个模块都有对应的编号,每个方法也有对应的编号,在JS调用OC时,通过传递对应的ModuleID和MethodID即可匹配OC模块及方法。
{
"remoteModuleConfig": {
"RCTSQLManager": {
"methods": {
"query": {
"type": "remote",
"methodID": 0
}
},
"moduleID": 4
},
...
},
}
React Native初始化分析
每个应用有一个唯一的 rootWindow
,每一个UIWindow
有一个唯一的rootView
,在React Native 中,对应的就是RCTRootView
,它持有一个RCTBridge
,RCTBridge
的职能是通讯桥,负责各个模块之间和js之间的通讯, RCTBatchedBridge
继承RCTBridge
,它有一个唯一的但是可变的currentBridge
,实际上RCTBridge
是唯一的, RCTBatchedBridge
是唯一的,通讯时,实际上RCTBatchedBridge
承担一个适配的职责。
因此,实际上在创建一个RootView
之前,React Native都会预先创建好一个RCTBridge
,而RCTBridge
的setUp
方法主要是为了初始化BatchedBridge
,BatchedBridge
主要是用来批量读取JavaScript对Objective-C的调用,BatchedBridge
内部还依赖一个JSCExecutor
,用于执行JS代码,下面我们简单地了解一下BatchedBridge
初始化过程中都做了哪些工作。
1. 读取 JavaScript 源码
这个过程将应用的js代码加载到内存,供接下来在OC中调用执行JS代码
2. 初始化模块信息
这一步主要是发现所有需要暴露给JavaScript的模块及模块中需要暴露的方法,每一个需要暴露的模块都会被加上 RCT_EXPORT_MODULE
的宏,宏的内容如下:
#define RCT_EXPORT_MODULE(js_name) \
RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString *)moduleName { return @#js_name; } \
+ (void)load { RCTRegisterModule(self); }
这个类在执行load方法时会调用RCTRegisterModule
方法,将自身注册到RCTModuleClasses
中
void RCTRegisterModule(Class moduleClass)
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
RCTModuleClasses = [NSMutableArray new];
});
[RCTModuleClasses addObject:moduleClass];
}
因此我们可以从RCTModuleClasses
中获取出所有模块信息,每一条模块信息都被存储与RCTModuleData
对象中
for (Class moduleClass in RCTGetModuleClasses()) {
RCTModuleData *moduleData = [[RCTModuleData alloc]initWithModuleClass:moduleClass bridge:self];
[moduleClassesByID addObject:moduleClass];
[moduleDataByID addObject:moduleData];
}
3. 初始化 JavaScript 代码的执行器,即 RCTJSCExecutor 对象
在这一步操作中,通过addSynchronousHookWithName
这一方法向JavaScript的添加了若干的Block对象作为全局变量,以供第5步过程中在执行JavaScript源码时处理这些Block对象
4. 生成模块列表并写入 JavaScript 端
这一步操作是将OC端生成的模块列表信息注入到JavaScript端中,以便双方都持有一份模块列表信息。
- (NSString *)moduleConfig{
NSMutableArray<NSArray *> *config = [NSMutableArray new];
for (RCTModuleData *moduleData in _moduleDataByID) {
[config addObject:@[moduleData.name]];
}
}
可以看到,Objective-C将config信息存储到了JavaScript的全局变量中,名称为__fbBatchedBridgeConfig
5. 执行 JavaScript 源码
在所有的初始化都完成后,只需要运行js代码即可,运行过程中也会执行第3步过程添加进全局变量的Block对象
方法调用流程
-
JS调用模块方法。
-
在JS端有一个JS Bridge专门负责处理JS与OC交互部分,同理在OC端也有一个OC Bridge,JS Bridge将调用的模块方法记录并转化成相应的ModuleName,MethodName和args。
-
然后在MessageQueue中将调用模块方法时的回调函数注册一个CallBack ID,将ID和回调函数存储在一个成员变量的列表中,并将第2步中的ModuleName和MethodName根据模块配置表信息转成对应的ID。
-
JS将moduleID,methodID和args以及CallBackID传递给OC Bridge,这个过程实质上是基于事件处理的,因为在移动平台上如果有代码的执行必定是某个事件触发的,比如滑动屏幕等等,事件触发后OC主动调用JS代码,JS处理业务逻辑过程并将需要调用OC的部分存储到MessageQueue中,再去通知OC执行。
-
OC接收到消息,通过模块配置表拿到对应的模块和方法,在OC Bridge端,对每一个可被调用的模块方法都会有一个RCTModuleMethod对象与之对应。
-
RCTModuleMethod对传进来的参数进行处理,包括类型转化以及创建一个Block对象以供回调,会将JS端传过来的CallBackID以及回调的值存储进Block对象中
-
执行OC端代码
-
执行第6步中生成的Block方法
-
block里带着CallbackID和block传过来的参数去调JS里MessageQueue的方法invokeCallbackAndReturnFlushedQueue。
-
MessageQueue根据CallBackId找到对应的回调函数
-
根据OC传来的回调值,执行回调函数
整个流程就是这样,简单概括下,差不多就是:JS函数调用转ModuleID/MethodID -> callback转CallbackID -> OC根据ID拿到方法 -> 处理参数 -> 调用OC方法 -> 回调CallbackID -> JS通过CallbackID拿到callback执行
4. React Native优缺点
优点
- 能够利用 JavaScript 动态更新的特性,快速迭代。
- 相比于原生平台,开发速度更快,相比于 Hybrid 框架,性能更好。
缺点
- 不能实现真正意义上的跨平台,开发者仍然需要为iOS和Android提供两套实现机制
- 不能直接取代Native Code开发,很大程度上还加重了开发者的学习成本
- 语言互转存在着固定的时间和空间开销