在这篇文章中,我假设你已经掌握了React Native的基础知识,并且有兴趣了解JavaScript和本地通信的内部工作原理。
主线程
在开始之前,我们首先要知道React Native中的3个重要的线程:
Shadow Queue
: 负责布局的控制Main Thread
: UIKit运行的线程JavaScript Thread
: JS代码在该线程运行
此外,如果没有特殊说明,每一个单独的本地模块都有自己的GCD队列。
注:
Shadow Queue
顾名思义,实际上是一个GCD队列而不是一个线程。
本地模块
如果你还不知道如何创建一个本地模块,我推荐你可以先看看官方文档。
下面是一个Person模块,实现了JS调用本地模块的交互过程。
@interface Person @H_404_44@: NSObject <RCTBridgeModule> @end @implementation Logger RCT_EXPORT_MODULE@H_404_44@(@H_404_44@) RCT_EXPORT_METHOD@H_404_44@(greet@H_404_44@:@H_404_44@(NSString *@H_404_44@)name@H_404_44@) @H_404_44@{ NSLog@H_404_44@(@"Hi,%@!"@H_404_44@, name@H_404_44@)@H_404_44@; @H_404_44@[_bridge@H_404_44@.eventDispatcher sendAppEventWithName@H_404_44@:@"greeted" body@H_404_44@:@@H_404_44@{ @"name"@H_404_44@: name @H_404_44@}@H_404_44@]@H_404_44@; @H_404_44@} @end
我们主要来看看RCT_EXPORT_MODULE
和RCT_EXPORT_METHOD
这两个宏的实现,了解它们所扮演的角色以及内部是如何工作的。
RCT_EXPORT_MODULE([js_name])
顾名思义,这个宏会导出你的模块,但在特定的环境中,导出
的具体含义是什么?这里的意思是把你的模块暴露给React中的Bridge
。
它的实现也相当简单:
#define RCT_EXPORT_MODULE(js_name) \ RCT_EXTERN void RCTRegisterModule@H_404_44@(Class@H_404_44@)@H_404_44@; \ + @H_404_44@(NSString \*@H_404_44@)moduleName @H_404_44@{ return @#js_name@H_404_44@; @H_404_44@} \ + @H_404_44@(void@H_404_44@)load @H_404_44@{ RCTRegisterModule@H_404_44@(self@H_404_44@)@H_404_44@; @H_404_44@}
这段代码做了哪些事情?
-
它首先声明
RCTRegisterModule
extern`(外部)函数。意味着这个函数的实现对于编译器是不可见的,但在链接时可见。 -
声明一个
moduleName
方法,该方法返回宏的可选参数js_name
。这样,你就可以让你的模块在JS中有一个区别与Objective-C类名的名称。 -
load方法(当app被加载到内存之后,它会调用所有类的load方法),该方法调用上面声明的
RCTRegisterModule
函数,让bridge
知道这个模块。
RCT_EXPORT_METHOD(method)
这个宏更有意思,实际上他没有给你的方法添加任何东西,而是额外创建了一个新的方法。
这个新的方法看起来像下面的例子一样:
+ @H_404_44@(NSArray *@H_404_44@)__rct_export__120 @H_404_44@{ @@H_404_44@[ @""@H_404_44@, @"log:(NSString *)message" @H_404_44@]@H_404_44@; @H_404_44@}
“这是什么玩意儿?”,你一定有这样的想法。
实际上,它是由前缀(rct_export),一个可选的js_name
(这里为空)和函数定义的行号(这里是12)还有一个__COUNTER__(累加器)的宏拼接而成的。
这个方法唯一的目的就是返回一个包含js_name
和方法签名的字符串数组。在名字上的处理是为了避免冲突。
注:如果你使用Category,这里依然可能存在两个同名的方法,虽然Xcode会有警告,但仍然会表现出异常。
运行时
整个组装过程提供了信息给bridge
,所以它可以找到任何已经导出的模块和方法。但是这个过程发生在加载期间,下面我们看看在运行期它是如何被使用的。
初始化模块
RCTRegisterModule
方法所做的事情就是把类添加到一个数组中,后面如果创建新的bridge
实例,就可以直接找到这个类了。下面,程序遍历模块数组并为每一个模块创建实例对象,然后把bridge
的引用赋值给模块,再把对象引用储存到bridge
中(实现相互调用),最后检查模块是否有指定运行的队列,如果没有则为其创建一个新的队列,从其他模块中隔离开来。
NSMutableDictionary *modulesByName@H_404_44@; // = ... for @H_404_44@(Class moduleClass in RCTGetModuleClasses@H_404_44@(@H_404_44@)@H_404_44@) @H_404_44@{ // ... module = @H_404_44@[moduleClass new@H_404_44@]@H_404_44@; if @H_404_44@(@H_404_44@[module respondsToSelector@H_404_44@:@selector@H_404_44@(setBridge@H_404_44@:@H_404_44@)@H_404_44@]@H_404_44@) @H_404_44@{ module@H_404_44@.bridge = self@H_404_44@; @H_404_44@} modulesByName@H_404_44@[moduleName@H_404_44@] = module@H_404_44@; // ... @H_404_44@}
配置模块
一旦我们在后台线程运行了模块,就可以列出并且调用该模块的所有以__rct_export__
开头的方法,也可以得到方法签名的字符串表示。这是非常重要的,这样一来,我们就知道了参数实际的类型。比如,在运行时我们只能知道参数的名称是id
,但通过这种方式就可以知道参数的类型是NSString *
了。
unsigned int methodCount@H_404_44@; Method *methods = class_copyMethodList@H_404_44@(moduleClass@H_404_44@, &methodCount@H_404_44@)@H_404_44@; for @H_404_44@(int i = 0@H_404_44@; i < methodCount@H_404_44@; i++@H_404_44@) @H_404_44@{ Method method = methods@H_404_44@[i@H_404_44@]@H_404_44@; SEL selector @H_334_502@method_getName@H_404_44@(method@H_404_44@)@H_404_44@; if @H_404_44@(@H_404_44@[NSStringFromSelector@H_404_44@(selector@H_404_44@) hasPrefix@H_404_44@:@"__rct_export__"@H_404_44@]@H_404_44@) @H_404_44@{ IMP imp @H_334_502@method_getImplementation@H_404_44@(method@H_404_44@)@H_404_44@; NSArray *entries = @H_404_44@(@H_404_44@(NSArray *@H_404_44@(*@H_404_44@)@H_404_44@(id@H_404_44@, SEL@H_404_44@)@H_404_44@)imp@H_404_44@)@H_404_44@(_moduleClass@H_404_44@, selector@H_404_44@)@H_404_44@; //... @H_404_44@[moduleMethods addObject@H_404_44@:/* Object representing the method */@H_404_44@]@H_404_44@; @H_404_44@} @H_404_44@}
安装JavaScript Executor
JavaScript Executors有一个-setUp
方法,允许其来做一些昂贵的工作,例如在后台线程初始化一个JavaScriptCore
。同时,它也省去了一些不必要的工作,比如,只有激活状态下的executor
才会收到setUp
的调用指令,而不是所有的excutor
都会收到指令。
JSGlobalContextRef ctx @H_334_502@JSGlobalContextCreate@H_404_44@(NULL@H_404_44@)@H_404_44@; _context = @H_404_44@[@H_404_44@[RCTJavaScriptContext alloc@H_404_44@] initWithJSContext@H_404_44@:ctx@H_404_44@]@H_404_44@;
注入JSON配置
JSON配置仅仅包含自己模块的信息,如下:
@H_404_44@{ "remoteModuleConfig": @H_404_44@{ "Logger": @H_404_44@{ "constants": @H_404_44@{ /* If we had exported constants... */ @H_404_44@}@H_404_44@, "moduleID": 1@H_404_44@,85);">"methods": @H_404_44@{ "requestPermissions": @H_404_44@{ "type": "remote"@H_404_44@, "methodID"@H_334_502@1 @H_404_44@} @H_404_44@} @H_404_44@} @H_404_44@} @H_404_44@}
这个信息被作为全局变量储存在JavaScript虚拟机中,所以JS这边的bridge
一被初始化就可以使用这些信息去创建模块。
加载JavaScript代码
这个过程很直观,就是从指定的地方载入源码。通常,在开发期间从packager
下载导入,在生产环境下,直接从从本地存储加载。
执行JavaScript代码
一切准备就绪,程序就可以加载JavaScriptCore虚拟机中的应用源码,拷贝、解析、执行代码。首次执行需要注册所有CommanJS模块,指明入口文件。
JSValueRef jsError = NULL@H_404_44@; JSStringRef execJSString @H_334_502@JSStringCreateWithCFString@H_404_44@(@H_404_44@(__bridge CFStringRef@H_404_44@)script@H_404_44@)@H_404_44@; JSStringRef jsURL @H_334_502@JSStringCreateWithCFString@H_404_44@(@H_404_44@(__bridge CFStringRef@H_404_44@)sourceURL@H_404_44@.absoluteString@H_404_44@)@H_404_44@; JSValueRef result @H_334_502@JSEvaluateScript@H_404_44@(strongSelf->_context@H_404_44@.ctx@H_404_44@, execJSString@H_404_44@, NULL@H_404_44@, jsURL@H_404_44@, 0@H_404_44@,204);">&jsError@H_404_44@)@H_404_44@; JSStringRelease@H_404_44@(jsURL@H_404_44@)@H_404_44@; JSStringRelease@H_404_44@(execJSString@H_404_44@)@H_404_44@;
JavaScript模块
模块从JSON配置中生成,在JavaScript中通过NativeModules
对象进行使用。
var @H_404_44@{ NativeModules @H_404_44@} @H_334_502@require@H_404_44@('react-native'@H_404_44@)@H_404_44@; var @H_404_44@{ Person @H_404_44@} = NativeModules@H_404_44@; Person@H_404_44@.greet@H_404_44@('Tadeu'@H_404_44@)@H_404_44@;
这种工作方式的流程是,当你调用一个方法,请求会被推进一个队列中,它包含了模块名称、方法名称和所有调用所需的参数。在JavaScript执行之后,该队列会被传回到本地去执行请求。
调用周期
调用从本地模块到JS。在执行期间,当调用NativeModules
上的方法时,程序把将会在本地执行的调用加入队列中。JS执行结束后,本地程序会遍历并运行队列中的调用请求,回调和调用结果(使用_bridge
实例可以通过本地模块去调用enqueueJSCall:args:
)最终会通过bridge
传回给JS。
上图仅表示了JavaScript执行中期的流程
注:如果你关注过该项目,曾经有一个本地到JS的调用队列,它会被指派到每一个vSYNC,但为了加快启动的速度已经将这个功能移除。
参数类型
从本地到JS的调用是比较容易的,参数被传入一个NSArray
然后转换成JSON。但对于来自JS的调用,需要本地类型,为此,我们显式地检查初始类型(ints,floats,chars等等)。但是正如之前所说,对于对象和结构体,运行时没有从NSMethodSignature
中给我们提供足够的信息,所以我们把类型保存为字符串。
我们使用正则表达式从方法签名中提取类型,实际中还使用RCTConvert
工具类去转换对象,对于每个支持的类型它都有一个默认的方法,尝试把JSON
输入转换为所需的类型。
除了结构体以外,我们使用objc_msgSend
去动态地调用方法,因为在arm64上没有objc_msgSend_stret
对应的版本,我们退回到NSInvocation
。
一旦我们转换了所有的参数,我们使用另一个NSInvocation
带着参数去调用目标模块和方法。
下面是一个例子:
// If you had the following method in a given module,e.g. `MyModule` RCT_EXPORT_METHOD@H_404_44@(methodWithArray@H_404_44@:@H_404_44@(NSArray *@H_404_44@) size@H_404_44@:@H_404_44@(CGRect@H_404_44@)size@H_404_44@) @H_404_44@{@H_404_44@} // And called it from JS,like: 'NativeModules'@H_404_44@)@H_404_44@.MyModule@H_404_44@.method@H_404_44@(@H_404_44@['a'@H_404_44@,73);">1@H_404_44@]@H_404_44@, @H_404_44@{ x@H_404_44@: 404_44@: 404_44@: 200@H_404_44@, height@H_404_44@: 100 @H_404_44@}@H_404_44@)@H_404_44@; // The JS queue sent to native would then look like the following: // ** Remember that it's a queue of calls,so all the fields are arrays ** @@H_404_44@[ @@H_404_44@[ @0 @H_404_44@]@H_404_44@, // module IDs @H_334_502@1 @H_404_44@]@H_404_44@,153);">// method IDs @@H_404_44@[ // arguments @@H_404_44@[ @@H_404_44@[@"a"@H_404_44@, @"x"@H_404_44@: @"y"@H_404_44@: @"width"@H_404_44@: @"height"@H_404_44@: @H_334_502@100 @H_404_44@} @H_404_44@] @H_404_44@] @H_404_44@]@H_404_44@; // This would convert into the following calls (pseudo code) NSInvocation call call@H_404_44@[args@H_404_44@]@H_404_44@[0@H_404_44@] @H_334_502@GetModuleForId@H_404_44@(@H_334_502@0@H_404_44@) call@H_404_44@[args@H_404_44@]@H_404_44@[1@H_404_44@] @H_334_502@GetMethodForId@H_404_44@(@H_334_502@1@H_404_44@) call@H_404_44@[args@H_404_44@]@H_404_44@[2@H_404_44@] @H_334_502@obj_msgSend@H_404_44@(RCTConvert@H_404_44@, NSArray@H_404_44@,73);">1@H_404_44@]@H_404_44@) call@H_404_44@[args@H_404_44@]@H_404_44@[3@H_404_44@] @H_334_502@NSInvocation@H_404_44@(RCTConvert@H_404_44@, CGRect@H_404_44@, @H_404_44@.@H_404_44@.@H_404_44@. @H_404_44@}@H_404_44@) call@H_404_44@(@H_404_44@)
线程
正如上面提到的,每个模块默认都会有自己的GCD队列,除非通过实现-methodQueue
方法或跟一个有效的队列合成methodQueue
属性来指定了特定的运行队列。View Manager
(继承自RCTViewManager)是一个特例,它默认使用Shadow Queue
。RCTJSThread
也比较特殊,它仅仅是为了占位,因为它是一个线程而不是队列。
shadow queue
作为目标队列。
当前线程的规则如下:
@H_314_1301@-init和-setBridge
是为了保证在主线程中被调用。
RCTInvalidating
协议,invalidate同样保证在目标队列中被调用。-dealloc
将会在哪个线程被调用。如果从JS有批量的调用请求,请求将会被目标队列分组,并行地被调度。
// group `calls` by `queue` in `buckets` for @H_404_44@(id queue in buckets@H_404_44@) @H_404_44@{ dispatch_block_t block = ^@H_404_44@{ NSOrderedSet *calls = @H_404_44@[buckets objectForKey@H_404_44@:queue@H_404_44@]@H_404_44@; for @H_404_44@(NSNumber *indexObj in calls@H_404_44@) @H_404_44@{ // Actually call @H_404_44@} @H_404_44@}@H_404_44@; if @H_404_44@(queue == RCTJSThread@H_404_44@) @H_404_44@{ @H_404_44@[_javaScriptExecutor executeBlockOnJavaScriptQueue@H_404_44@:block@H_404_44@]@H_404_44@; @H_404_44@} else if @H_404_44@(queue@H_404_44@) @H_404_44@{ dispatch_async@H_404_44@(queue@H_404_44@, block@H_404_44@)@H_404_44@; @H_404_44@} @H_404_44@}
结尾
以上就是稍加深入地对bridge
的工作原理进行了介绍,我希望能对那些想开发更加复杂的模块和想为核心框架贡献代码的人能有所帮助。