在这篇文章之前我们假设你已经了解了React Native的基础知识,我们会重点关注当native和JavaScript进行信息交流时的内部运行原理。
主线程在开始之前,我们需要知道在React Native中有三个主要的线程:
shadow queue:负责布局工作
main thread:UIKit 在这个线程工作(译者注:UI Manager线程,可以看成主线程,主要负责页面交互和控件绘制的逻辑)
JavaScript thread:运行JS代码的线程
另外,一般情况下每个native模块都有自己的GCD队列,除非有特殊说明(后面会解释)
*shadow queue其实更像一个GCD队列而不是线程
Native模块如果你还不知道怎么创建一个Native模块,我推荐你去阅读一下文档
这是一个native模块Person的例子,它既受JavaScript的调用,也可以调用JavaScript
@interface Person : NSObject <RCTBridgeModule> @end @implementation Logger RCT_EXPORT_MODULE() RCT_EXPORT_METHOD(greet:(NSString *)name) { NSLog(@"Hi, %@!", name); [_bridge.eventDispatcher sendAppEventWithName:@"greeted" body:@{ @"name": name }]; } @end
我们重点关注RCT_EXPORT_MODULE和RCT_EXPORT_METHOD这两个宏,它们扩展成什么,它们的角色是什么,它们是如何运行的。
RCT_EXPORT_MODULE([js_name])正如这个方法的名字那样,它export出你的module,但是在这个特定的上下文中export是什么意思呢,它意味着桥接知道你的模块。
它的定义实际上非常简单:
#define RCT_EXPORT_MODULE(js_name) \ RCT_EXTERN void RCTRegisterModule(Class); \ + (NSString \*)moduleName { return @#js_name; } \ + (void)load { RCTRegisterModule(self); }
它做了以下工作:
首先声明RCTRegisterModule为外部函数,意味着这个函数的实现对于编译器不可见,但是在链接阶段可用
声明一个方法moduleName,返回可选的宏参数js_name,这样这个模块在JS中具有和Objective-C中不一样的类名
声明一个load方法(当app加载到内存中后,每个类的load方法都会被调用),load方法调用RCTRegisterModule,然后桥接才知道这个暴露出来的模块
RCT_EXPORT_METHOD(method)这个宏更有趣,它没有在你的method中增加任何东西,除了声明指定的方法外,它还创建了一个新方法。新方法如下所示:
+ (NSArray *)__rct_export__120 { return @[ @"", @"log:(NSString *)message" ]; }
它是通过将前缀(__rct_export__)和可选的js_name(本例子为空)和声明的行号以及__COUNTER__宏构成。
这个方法的目的是返回一个包含可选js_name和method签名的数组,这个js_name的作用是避免方法命名冲突。
Runtime这整个设置仅仅是为了给桥接提供信息,让它可以找到export出来的所有东西,modules和methods,但是这些都是在加载的时候发生的,现在我们来看看运行的时候是怎么使用的。
这是桥接初始化时的依赖关系图:
初始化模块RCTRegisterModule所做的事就是把类推进数组,这样在实例化一个新的桥接的时候就能找到这个类。桥接遍历数组中的所有模块,为每个模块创建一个实例,在桥接那边存储一个实例的引用,同时给这个模块实例一个桥接的引用(所以我们能两边都互相调用),然后检查这个模块实例是否有指定要在哪个队列运行,否则给它一个新队列,与其他模块分开:
NSMutableDictionary *modulesByName; // = ... for (Class moduleClass in RCTGetModuleClasses()) { // ... module = [moduleClass new]; if ([module respondsToSelector:@selector(setBridge:)]){ module.bridge = self; modulesByName[moduleName] = module; // ... }
配置模块一旦我们有了这些modules,在后台线程中,我们列出每个module的所有methods,然后调用以__rct__export__开头的methods,我们得到一个method签名的字符串。这很重要因为我们现在知道了参数的实际类型,在运行的时候我们只知道其中一个参数是id,但是通过这个途径我们可以知道这个id实际上是NSString *
unsigned int methodCount; Method *methods = class_copyMethodList(moduleClass, &methodCount); for (unsigned int i = 0; i < methodCount; i++) { Method method = methods[i]; SEL selector = method_getName(method); if ([NSStringFromSelector(selector) hasPrefix:@"__rct_export__"]) { IMP imp = method_getImplementation(method); NSArray *entries = ((NSArray *(*)(id, SEL))imp)(_moduleClass, selector); //... [moduleMethods addObject:/* Object representing the method */]; } }
设置JavaScript执行器JS执行器有一个 -setUp 方法允许它做更复杂的工作,例如在后台线程初始化JS代码,这同时节约了一些工作,因为只有活跃的执行器会接受 setUp 方法的调用,而不是所有的执行器:
JSGlobalContextRef ctx = JSGlobalContextCreate(NULL); _context = [[RCTJavaScriptContext alloc] initWithJSContext:ctx];
注入JSON配置JSON配置仅包含我们的module,例如:
这个配置信息作为全局变量存储在JavaScript虚拟机,所以当JS那边的桥接初始化后它可以用这个信息来创建modules
加载JavaScript代码