在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
在这篇文章之前我们假设你已经了解了React Native的基础知识,我们会重点关注当native和JavaScript进行信息交流时的内部运行原理。 主线程在开始之前,我们需要知道在React Native中有三个主要的线程:
另外,一般情况下每个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); } 它做了以下工作:
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代码这非常直观,只需要从指定的任何提供程序中加载源代码,通常在开发过程中从打包程序中加载源代码,在生产环境中从磁盘加载。 执行JavaScript代码一旦所有事情准备就绪,我们可以在JS虚拟机中加载应用的源代码,复制代码,解析并执行它。在第一次执行时需要注册所有CommonJS模块并且需要入口文件。 JSValueRef jsError = NULL; JSStringRef execJSString = JSStringCreateWithCFString((__bridge CFStringRef)script); JSStringRef jsURL = JSStringCreateWithCFString((__bridge CFStringRef)sourceURL.absoluteString); JSValueRef result = JSEvaluateScript(strongSelf->_context.ctx, execJSString, NULL, jsURL, 0, &jsError); JSStringRelease(jsURL); JSStringRelease(execJSString); JavaScript中的Modules在JS侧我们现在可以通过react-native的NativeModules拿到前面的JSON配置信息构成的module: 它运行的方式是当你调用一个方法的时候它被放到一个队列,包括module的名称,method的名称以及所有的参数,在JavsScript执行的最后这个队列会给原生模块执行。 调用周期现在如果我们用上面的代码调用module,它将会是这个样子的: 调用必须从native开始,native调用JS(这张图只是截取了JS运行的某个时刻),在执行过程中,因为JS调用NativeModules的方法,它把这个调用入队,因为这个调用必须在原生那边执行。当JS执行完后,原生模块遍历入队的所有调用,然后当它执行这些调用后,通过桥接进行回调(一个原生模块可以通过_bridge实例来调用enqueueJSCall:args:),来再次回调JS。 (如果您一直在关注该项目,过去也有来自native-> JS的调用队列,该调用队列会在每个vSYNC上分派,但为了缩短启动时间已将其删除) 参数类型native到JS的调用很容易,参数被NSArray传递,我们将其编码为JSON数据,但是对于JS对native的调用,我们需要native的类型,为此我们检查基本类型(ints,floats,chars...)但是就像上边提及那样,对于任何对象(结构),运行时我们不会从NSMthodSignature获得足够的信息,所以我们把类型保存为字符串。 我们使用正则表达式从method签名中提取类型,并使用RCTConvert类来实际转换对象,默认情况下它为每种类型都提供了方法,并且尝试将JSON输入转换为所需要的类型。 除非是一个struct,否则我们使用objc_msgSend动态调用该方法,因为arm64上没有objc_msgSend_stret的版本,因此我们使用NSInvocation。 转换完所有参数后,我们将使用另一个NSInvocation来调用目标module和method。 例子: // If you had the following method in a given module, e.g. `MyModule` RCT_EXPORT_METHOD(methodWithArray:(NSArray *) size:(CGRect)size) {} // And called it from JS, like: require('NativeModules').MyModule.method(['a', 1], { x: 0, y: 0, width: 200, height: 100 }); // 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 ** @[ @[ @0 ], // module IDs @[ @1 ], // method IDs @[ // arguments @[ @[@"a", @1], @{ @"x": @0, @"y": @0, @"width": @200, @"height": @100 } ] ] ]; // This would convert into the following calls (pseudo code) NSInvocation call call[args][0] = GetModuleForId(@0) call[args][1] = GetMethodForId(@1) call[args][2] = obj_msgSend(RCTConvert, NSArray, @[@"a", @1]) call[args][3] = NSInvocation(RCTConvert, CGRect, @{ @"x": @0, ... }) call() 线程正如以上提及那样,每个module默认都有一个GCD队列,除非它通过实现-methodQueue方法或将methodQueue属性与有效队列合并来指定要在哪个队列运行。ViewManagers*是例外(扩展了RCTViewManager),将默认使用Shadow Queue,而特殊目标RCTJSThread仅是一个占位符,因为它是线程而不是队列。 (其实View Managers不是真正的例外,因为基类显式的将Shadow Queue指定为目标队列了) 当前线程规则如下:
当接收到JS的一批调用时,这些调用会按目标队列进行分组,并行调用: // group `calls` by `queue` in `buckets` for (id queue in buckets) { dispatch_block_t block = ^{ NSOrderedSet *calls = [buckets objectForKey:queue]; for (NSNumber *indexObj in calls) { // Actually call } }; if (queue == RCTJSThread) { [_javaScriptExecutor executeBlockOnJavaScriptQueue:block]; } else if (queue) { dispatch_async(queue, block); } } 结尾这就是React Native桥接工作原理的更深入概述。我希望者对想要构建更复杂modules或者想对核心框架有贡献的人有所帮助。 到此这篇关于深入理解React Native核心原理(React Native的桥接(Bridge)的文章就介绍到这了,更多相关React Native原理内容请搜索极客世界以前的文章或继续浏览下面的相关文章希望大家以后多多支持极客世界! |
请发表评论