React Native开发指南react native

ReactNative iOS源码解析(一)

2017-08-02  本文已影响45人  冷洪林

今天在逛论坛的时候发现这篇文章写得特别好,所以转帖来分享给大家,顺便自己也可以学习学习。原文阅读

ReactNative 概要

ReactNative,动态,跨平台,热更新,这几个词现在越来越火了,一句使用JavaScript写源生App吸引力了无数人的眼球,并且诞生了这么久也逐渐趋于稳定,携程,天猫,QZone也都在大产品线的业务上,部分模块采用这个方案上线,并且效果得到了验证(见2016 GMTC 资料PPT)


我们把这个单词拆解成2部分


那么这两个不相干的东西是如何关联在一起的呢?
React.JS是一个前端框架,在浏览器内H5开发上被广泛使用,他在渲染render()这个环节,在经过各种flexbox布局算法之后,要在确定的位置去绘制这个界面元素的时候,需要通过浏览器去实现。他在响应触摸touchEvent()这个环节,依然是需要浏览器去捕获用户的触摸行为,然后回调React.JS

上面提到的都是纯网页,纯H5,但如果我们把render()这个事情拦截下来,不走浏览器,而是走native会怎样呢?

当React.JS已经计算完每个页面元素的位置大小,本来要传给浏览器,让浏览器进行渲染,这时候我们不传给浏览器了,而是通过一个JS/OC的桥梁,去通过[[UIView alloc]initWithFrame:frame]的OC代码,把这个界面元素渲染了,那我们就相当于用React.JS绘制出了一个native的View

拿我们刚刚绘制出得native的View,当他发生native源生的- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event触摸事件的时候,通过一个OC/JS的桥梁,去调用React.JS里面写好的点击事件JS代码

这样React.JS还是那个React.JS,他的使用方法没发生变化,但是却获得了纯源生native的体验,native的组件渲染,native的触摸响应

于是,这个东西就叫做React-Native


ReactNative 结构

大家可以看到,刚才我说的核心就是一个桥梁,无论是JS=>OC,还是OC=>JS。

刚才举得例子,就相当于把纯源生的UI模块,接入这个桥梁,从而让源生UI与React.JS融为一体。

那我们把野心放长远点,我们不止想让React.JS操作UI,我还想用JS操作数据库!无论是新玩意Realm,还是老玩意CoreData,FMDB,我都希望能用JS操作应该怎么办?好办,把纯源生的DB代码模块,接入这个桥梁。

如果我想让JS操作Socket做长连接呢?好办,把源生socket代码模块接入这个桥梁。如果我想让JS能操作支付宝,微信,苹果IAP呢?好办,把源生支付代码模块接入这个桥梁。

由此可见RN就是由一个bridge桥梁,连接起了JS与native的代码模块

这是一个极度模块化可扩展的桥梁框架,不是说你从facebook的源上拉下来RN的代码,RN的能力就固定一成不变了,他的模块化可扩展,让你缺啥补上啥就好了。

ReactNative 结构图


大家可以看这个结构图,整个RN的结构分为四个部分,上面提到的,RN桥的模块化可扩展性,就体现在JSBridge/OCBridge里的ModuleConfig,只要遵循RN的协议RCTBridgeModule去写的OC Module对象,使用RCT_EXPORT_MODULE()宏注册类,使用RCT_EXPORT_METHOD()宏注册方法,那么这个OC Module以及他的OC Method都会被JS与OC的ModuleConfig进行统一控制

上面是RN的代码类结构图


ReactNative 初始化代码分析

我会按着函数调用栈类似的形式梳理出一个代码流程表,对每一个调用环节进行简单标记与作用说明,在整个表梳理完毕后,我会一一把每个标记进行详细的源码分析和解释

下面的代码流程表,如果有类名+方法的,你可以直接在RN源码中定位到具体代码段

RootInit标记:所有RN都是通过init方法创建的不再赘述,URL可以是网络url,也可以是本地filepath转成URL

BatchBridgeInit标记:前边说过rootview会先持有一个RCTBridge,所有的module都是直接操作bridge所提供的接口,但是这个bridge基本上不干什么核心逻辑代码,他内部持有了一个batchbrdige,各种调用都是直接转发给RCTBatchBrdige来操作,因此batchbridge才是核心

RCTBridge在init的时候调用[self setUp]

RCTBridge在setUp的时候调用[self createBatchedBridge]

DisplaylinkInit标记:batchbridge会首先初始化一个RCTDisplayLink这个东西在业务逻辑上不会被所有的module调用,他的作用是以设备屏幕渲染的频率触发一个timer,判断是否有个别module需要按着timer去回调js,如果没有module,这个模块其实就是空跑一个displaylink,注意,此时只是初始化,并没有run这个displaylink

dispatchQueueInit标记:会初始化一个GCDqueue,后面很多操作都会被扔到这个队列里,以保证顺序执行

dispatchGroupInit标记:后面接下来进行的一些列操作,都会被添加到这个GCDgroup之中,那些被我做了group Enter标记的,当group内所有事情做完之后,会触发group Notify

groupEnterLoadSource标记:会把无论是从网络还是从本地,拉取jsbundle这个操作,放进GCDgroup之中,这样只有这个操作进行完了(还有其他group内操作执行完了,才会执行notify的任务)

loadJS标记:其实就是异步去拉取jsbundle,无论是本地读还是网络啦,[RCTJavaScriptLoader loadBundleAtURL:self.bundleURL onComplete:onSourceLoad];只有当回调完成之后会执行dispatch_group_leave,离开group

InitModule标记:这个函数是在主线程被执行的,但是刚才生成的GCD group会被当做参数传进内部,因为内部的一些逻辑是需要加入group的,这个函数内部很复杂 我会继续绘制一个代码流程表

1)RCTGetModuleClasses()
一个C函数,RCT_EXPORT_MODULE()注册宏会在+load时候把Module类都统一管理在一个static NSArray里,通过RCTGetModuleClasses()可以取出来所有的Module

2)RCTModuleData-initWithModuleClass
此处是一个for循环,循环刚才拿到的array,对每一个注册了得module都循环生成RCTModuleData实例

3)配置moduleConfig
每一个module在循环生成结束后,bridge会统一存储3分配置表,包含了所有的moduleConfig的信息,便于查找和管理

//barchbridge的ivar
  NSMutableDictionary<NSString *, RCTModuleData *> *_moduleDataByName;
  NSArray<RCTModuleData *> *_moduleDataByID;
  NSArray<Class> *_moduleClassesByID;
// Store modules
  _moduleDataByID = [moduleDataByID copy];
  _moduleDataByName = [moduleDataByName copy];
  _moduleClassesByID = [moduleClassesByID copy];

4)RCTModuleData-instance
这是一个for循环,每一个RCTModuleData都需要循环instance一下,需要说明的是,RCTModuleData与Module不是一个东西,各类Module继承自NSObject,RCTModuleData内部持有的instance实例才是各类Module,因此这个环节是初始化RCTModuleData真正各类Module实例的环节

通过RCTModuleData-setUpInstanceAndBridge来初始化创建真正的Module

//SOME CODE
_instance = [_moduleClass new];
//SOME CODE
[self setUpMethodQueue];

这里需要说明,每一个Module都会创建一个自己独有的专属的串行GCD queue,每次js抛出来的各个module的通信,都是dispatch_async,不一定从哪个线程抛出来,但可以保证每个module内的通信事件是串行顺序的

每一个module都有个bridge属性指向,rootview的bridge,方便快速调用

5)RCTJSCExecutor
RCTJSCExecutor是一个特殊的module,是核心,所以这里会单独处理,生成,初始化,并且被bridge持有,方便直接调用

RCTJSCExecutor初始化做了很多事情,需要大家仔细关注一下

创建了一个全新的NSThread,并且被持有住,绑定了一个runloop,保证这个线程不会消失,一直在loop,所有与JS的通信,一定都通过RCTJSCExecutor来进行,所以一定是在这个NSThread线程内,只不过各个模块的消息,会进行二次分发,不一定在此线程内

6)RCTModuleData-gatherConstants
每一个module都有自己的提供给js的接口配置表,这个方法就是读取这个配置表,注意!这行代码执行在主线程,但他使用dispatch_async 到mainQueue上,说明他先放过了之前的函数调用栈,等之前的函数调用栈走完,然后还是在主线程执行这个循环的gatherConstants,因此之前传进来的GCD group派上了用场,因为只有当所有module配置都读取并配置完毕后才可以进行 run js代码

下面思路从子代码流程表跳出,回到大代码流程表的标记

groupEnterJSConfig标记:代码到了这块会用到刚才创建,但一直没使用的GCD queue,并且这块还比较复杂,在这次enter group内部,又创建了一个子group,都放在这个GCD queue里执行

如果觉得绕可以这么理解他会在专属的队列里执行2件事情(后面要说的2各标记),当这2个事情执行完后触发子group notify,执行第三件事情(后面要说的第三个标记),当第三个事情执行完后leave母group,触发母group notify

dispatch_group_enter(initModulesAndLoadSource);
  dispatch_async(bridgeQueue, ^{
    dispatch_group_t setupJSExecutorAndModuleConfig = dispatch_group_create();
    // Asynchronously initialize the JS executor
    dispatch_group_async(setupJSExecutorAndModuleConfig, bridgeQueue, ^{
      RCTPerformanceLoggerStart(RCTPLJSCExecutorSetup);
      [weakSelf setUpExecutor];
      RCTPerformanceLoggerEnd(RCTPLJSCExecutorSetup);
    });
    // Asynchronously gather the module config
    dispatch_group_async(setupJSExecutorAndModuleConfig, bridgeQueue, ^{
      if (weakSelf.valid) {
        RCTPerformanceLoggerStart(RCTPLNativeModulePrepareConfig);
        config = [weakSelf moduleConfig];
        RCTPerformanceLoggerEnd(RCTPLNativeModulePrepareConfig);
      }
    });
    dispatch_group_notify(setupJSExecutorAndModuleConfig, bridgeQueue, ^{
      // We're not waiting for this to complete to leave dispatch group, since
      // injectJSONConfiguration and executeSourceCode will schedule operations
      // on the same queue anyway.
      RCTPerformanceLoggerStart(RCTPLNativeModuleInjectConfig);
      [weakSelf injectJSONConfiguration:config onComplete:^(NSError *error) {
        RCTPerformanceLoggerEnd(RCTPLNativeModuleInjectConfig);
        if (error) {
          dispatch_async(dispatch_get_main_queue(), ^{
            [weakSelf stopLoadingWithError:error];
          });
        }
      }];
      dispatch_group_leave(initModulesAndLoadSource);
    });
  });

configJSExecutor标记:再次专门处理一些JSExecutor这个RCTModuleData

1)property context懒加载,创建了一个JSContext

2)为JSContext设置了一大堆基础block回调,都是一些RN底层的回调方法

moduleConfig标记:把刚才所有配置moduleConfig信息汇总成一个string,包括moduleID,moduleName,moduleExport接口等等

moduleConfigInject标记:把刚才的moduleConfig配置信息string,通过RCTJSExecutor,在他内部的专属Thread内,注入到JS环境JSContext里,完成了配置表传给JS环境的工作

groupDone标记:GCD group内所有的工作都已完成,loadjs完毕,配置module完毕,配置JSExecutor完毕,可以放心的执行JS代码了

evaluateJS标记:通过[_javaScriptExecutor executeApplicationScript:script sourceURL:url onComplete:]来在JSExecutor专属的Thread内执行jsbundle代码

addrunloop标记:最早创建的RCTDisplayLink一直都只是创建完毕,但并没有运作,此时把这个displaylink绑在JSExecutor的Thread所在的runloop上,这样displaylink开始运作

小结:

整个RN在bridge上面,单说OC侧,各种GCD,线程,队列,displaylink,还是挺复杂的,针对各个module也都是有不同的处理,把这块梳理清楚能让我们更加清楚OC代码里面,RN的线程控制,更方便以后我们扩展编写更复杂的module模块,处理更多native的线程工作。

后面的 js call oc oc call js 我也会以同样的方式进行梳理,让大家清楚线程上是如何运作的

PS:JS代码侧其实bridge的设计也有一套,包括所有call oc messageQueue会有个队列控制之类的,我对JS不是那么熟悉和理解,JS侧的代码我就不梳理了。


上一篇 下一篇

猜你喜欢

热点阅读