React Native 现在是异常的火爆,我司最近也完成了一个 React Native 编写的项目,现在已经提测审核。大家关心的苹果会不会拒绝RN ,会不会拒绝 CodePush ,我们会用实际行动告诉大家。
本文会介绍 React Native 的工作原理,让移动开发者从代码上了解框架。
React 是 facebook 出品一个前端框架,是目前最火的框架。以组件的形式组织项目结构,代替市面上的 MVC 框架。React 的出现解决了前端许多的痛点:
React 更像的是 MVC 中的 View 层,Model 和 Controller 的角色则由 Redux 代替( React 跟MVC 并无关系,仅仅是为了方便理解),单向数据流使得业务逻辑清晰明了。所以现在的
React 项目的常用体系是 React + Redux + webpack + ES6 + react-router 。我们的 Reat Native 项目使用的是 React + Redux + ES6 。
简单交代了 React 的背景,下面到了咱们的主角 —— React Native 。它可以看作是 React 的亲儿子,把全身的本领都传授了下去,儿子也挺争气,在 iOS 端跟 Android 端也有跨越性的突破。于是乎它就有了“跨平台”、“Javascript编写Native项目”史诗级的技能,他的表叔
Microsoft 对它也是疼爱有加,怕他挨 Native 欺负,给它做了一个叫CodePush 的装备,随时升级加修复,“热更新”这个标签又贴到了它的身上。现在的它是集万千宠爱于一身,要风得风,要雨得雨。我们现在就来扒一扒,看它究竟是何方神圣。
引用React Native 从入门到原理中的一段话。
首先要明白的一点是,即使使用了 React Native,我们依然需要 UIKit 等框架,调用的是 Objective-C 代码。总之,JavaScript 只是辅助,它只是提供了配置信息和逻辑的处理结果。React Native 与 Hybrid 完全没有关系,它只不过是以 JavaScript 的形式告诉 Objective-C 该执行什么代码。
直白的说,Javascript 在上层完成逻辑处理,然后通过 Javascript 引擎,实现 Javascript 与 Objective-C 交互,调用 Objective-C 中的原生UI组件,来实现页面的渲染。
JavaScript 是一种单线程的语言,它不具备自运行的能力,因此总是被动调用。很多介绍 React Native 的文章都会提到 “JavaScript 线程” 的概念,实际上,它表示的是 Objective-C 创建了一个单独的线程,这个线程只用于执行 JavaScript 代码,而且 JavaScript 代码只会在这个线程中执行。
在 AppDelegate
的didFinishLaunchingWithOptions
方法中,我们找到了RN的入口。
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"demo"
initialProperties:nil
launchOptions:launchOptions];
首先创建了一个根控制器的View,然后将RN创建的view添加到窗口上显示。这个方法分为两步。在initWithBundleURL:moduleProvider:launchOptions
这个方法内,React Native创建了一个实现 Objective-C 与 Javascript 交互的全局bridge
,后续的所有交互全部都是通过这个桥接实现的。
第二步initWithBridge:moduleName:initialProperties
方法中返回了刚才的RCTRootView
。
第一步初始化的核心方法是setUp
。这个方法主要是创建了加载main.jsbundle
的地址和创建了BatchedBridge
。这个BatchedBridge
才是真正的主角,它的主要作用就是读取 Javascript 对 Objective-C 的方法调用,而且它的内部持有一个
JavascriptExecutor 对象,用来执行 Javascript 代码。
- (void)setUp
{
...
[self createBatchedBridge];
[self.batchedBridge start];
...
}
RCTBatchedBridge
中最重要的就是Star
t方法。该方法主要包含以下几步:
下面我们详细的解析下每个步骤
异步加载 JSBundle 。
[self loadSource:^(NSError *error, NSData *source, __unused int64_t sourceLength) {
if (error) {
RCTLogWarn(@"Failed to load source: %@", error);
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf stopLoadingWithError:error];
});
}
sourceCode = source;
dispatch_group_leave(initModulesAndLoadSource);
} onProgress:^(RCTLoadingProgress *progressData) {
#ifdef RCT_DEV
RCTDevLoadingView *loadingView = [weakSelf moduleForClass:[RCTDevLoadingView class]];
[loadingView updateProgress:progressData];
#endif
}];
初始化Native模块化信息。
// Synchronously initialize all native modules that cannot be loaded lazily
[self initModulesWithDispatchGroup:initModulesAndLoadSource];
这个方法很复杂,咱们一步步来。
首先创建了2个数组跟一字典,分别存贮 module 的类、module 数据跟两者组合的键值对,这样就形成了三份配置表。
NSMutableArray *moduleClassesByID = [NSMutableArray new];
NSMutableArray *moduleDataByID = [NSMutableArray new];
NSMutableDictionary<NSString *, RCTModuleData *> *moduleDataByName = [NSMutableDictionary new];
遍历需要暴露给 Javascript 的类(也就是下文的 module ),将所有的 module 加入配置表中。每一个 module 在实例化的时候都会开一个自己的队列,保证每个模块内部的通信都是串行执行。感兴趣的同学们可以阅读源码,这个段写的非常好,处理了很多像“死锁”这样的多线程操作。
for (Class moduleClass in RCTGetModuleClasses()) {
NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass);
// Check for module name collisions
RCTModuleData *moduleData = moduleDataByName[moduleName];
...
moduleData = [[RCTModuleData alloc] initWithModuleClass:moduleClass
bridge:self];
moduleDataByName[moduleName] = moduleData;
[moduleClassesByID addObject:moduleClass];
[moduleDataByID addObject:moduleData];
}
每个 module 都使用了RCTRegisterModule
这个宏,在这个类的 load 方法的时候注册了自己的 moduleName 到 RCTModuleClasses
这个数组中。这样,RN 遍历这个数组就能找到所有注册的 module 了。
在RCTBridgeModule.h
这个文件中我们可以看到实现。
#define RCT_EXPORT_MODULE(js_name)
RCT_EXTERN void RCTRegisterModule(Class);
+ (NSString *)moduleName { return @#js_name; }
+ (void)load { RCTRegisterModule(self); }
void RCTRegisterModule(Class moduleClass)
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
RCTModuleClasses = [NSMutableArray new];
});
RCTAssert([moduleClass conformsToProtocol:@protocol(RCTBridgeModule)],
@"%@ does not conform to the RCTBridgeModule protocol",
moduleClass);
// Register module
[RCTModuleClasses addObject:moduleClass];
}
`moduleName`方法,返回 `@#js_name`。`@# `是把宏参数
js_name 转为字符串,若字符串为空则返回的就是空。在 RCTBridgeModuleNameForClass()
获取模块名的方法里,如果 moduleName 长度为0,那么就会调用 NSStringFromClass()
方法获取类名。
Tip:@#是将传入单字符参数名转换成字符。传送门--[define宏定义的#,##,@#及符号](http://blog.csdn.net/xdsoft365/article/details/5911596)。
初始化 JavaScript 代码的执行器,即 RCTJSCExecutor
对象
上一步是module加入到配置表中,有一个非常特殊的类叫 RCTJSCExecutor
,它需要创建一个实例并保存在一个RCTModuleData
的实例中并且被RCTBatchedBridge
持有。
if (!_javaScriptExecutor) {
id
RCTModuleData *moduleData = [[RCTModuleData alloc] initWithModuleInstance:executorModule
bridge:self];
moduleDataByName[moduleData.name] = moduleData;
[moduleClassesByID addObject:self.executorClass];
[moduleDataByID addObject:moduleData];
// NOTE: _javaScriptExecutor is a weak reference
_javaScriptExecutor = executorModule;
}
被持有后,初始化RCTJSCExecutor
。
// Asynchronously initialize the JS executor
dispatch_group_async(setupJSExecutorAndModuleConfig, bridgeQueue, ^{
[performanceLogger markStartForTag:RCTPLJSCExecutorSetup];
[weakSelf setUpExecutor];
[performanceLogger markStopForTag:RCTPLJSCExecutorSetup];
});
前端的同学们肯定都知道js是单线程的,所有的js代码都是在一个单独的线程上调用的。
在RCTModuleClasses
的setUp
方法中,开了一条专门为 Javascript
运行的巴拿马运河。
- (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block
{
if ([NSThread currentThread] != _javaScriptThread) {
[self performSelector:@selector(executeBlockOnJavaScriptQueue:)
onThread:_javaScriptThread withObject:block waitUntilDone:NO];
} else {
block();
}
}
原理中的原理来了就是大家嘴中常说的“RN就是JS调OC啊”。
[self executeBlockOnJavaScriptQueue:^{
...
self->_context = [[RCTJavaScriptContext alloc] initWithJSContext:context onThread:self->_javaScriptThread];
[[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptContextCreatedNotification
object:context];
...
__weak RCTJSCExecutor *weakSelf = self;
context[@"nativeRequireModuleConfig"] = ^NSArray *(NSString *moduleName) {
RCTJSCExecutor *strongSelf = weakSelf;
if (!strongSelf.valid) {
return nil;
}
...
};
在JSThread内创建了一个JSContext
,并且为Contextz设置了不同的block,如上图。这些block会集中讲一下。
生成配置表
上一步已经获得了module的数据,这个一步则将数据转成json。
- (NSString *)moduleConfig
{
NSMutableArray<NSArray *> *config = [NSMutableArray new];
for (RCTModuleData *moduleData in _moduleDataByID) {
if (self.executorClass == [RCTJSCExecutor class]) {
[config addObject:@[moduleData.name]];
} else {
[config addObject:RCTNullIfNil(moduleData.config)];
}
}
return RCTJSONStringify(@{
@"remoteModuleConfig": config,
}, NULL);
}
将配置表传入JS端
在生成Native配置表跟初始化RCTJSCExecutor
完成后,就要将配置表传入JS端了,使两端拥有同一份配置表。
- (void)injectJSONConfiguration:(NSString *)configJSON
onComplete:(void (^)(NSError *))onComplete
{
...
[_javaScriptExecutor injectJSONText:configJSON
asGlobalObjectNamed:@"__fbBatchedBridgeConfig"
callback:onComplete];
}
JS拥有了变量名为__fbBatchedBridgeConfig
的全局变量。在JS端获得配置表后就开始执行js内部业务逻辑了。 JS 端如何使用这个配置我们下一篇会介绍。
执行 js 代码
在所有的准备工作都完成后,就开始通过executeSourceCode
执行
js 代码了。
dispatch_group_notify(initModulesAndLoadSource, bridgeQueue, ^{
RCTBatchedBridge *strongSelf = weakSelf;
if (sourceCode && strongSelf.loading) {
[strongSelf executeSourceCode:sourceCode];
}
});
在executeSourceCode
这个方法里,运行指定 url 的 js 代码。dev 模式下运行的是你的 bundleURL 下的代码,release 环境下运行的是你已经打包好的 jsbundle ,如果你使用了 codePush 对应的运行的是你 codePush 下发的 jsbundle。executeSourceCode
开启了一个 NSRunLoop
不断的打印引入 native 模块中的 log 。
本文一步步剖析了 ReactNative 这个伟大的框架,当然我这只是蜻蜓点水, ReactNative 还有许多值得我们学习的东西没有指出来,希望大家都去看看源码提高自己。后续,我会继续写文讲解下ReactNative如何将一个View页面展示出来和其他的一些小细节的东西。
今天是2017-06-02,我们的APP已经过审,大家放心可用~