美文网首页RN iOS开发相关
ReactNative-源码解析

ReactNative-源码解析

作者: 5ea1aaa189a6 | 来源:发表于2018-09-07 23:05 被阅读301次

    前言

    最近由于工作的关系需要研究一下ReactNative的源码。下面简单说说作为一个iOS开发者看完代码的感受,以及介绍rn代码中一些比较关键的点。

    注意:以下内容基于ReactNative 0.44.3

    第一印象

    按着官网的教程很容易的就创建了一个rn demo。迫不及待用xcode打开ios目录下的.xcodeproj文件。第一感受就是庞大,复杂。说实在的,rn是我看到过的最庞大的项目之一了。

    项目结构.png

    可以看到整个项目依赖了若干个工程,粗略的过了一遍,最为核心的部分是React这个工程,主要实现native到js的通信,渲染布局以及一些通用组件。网上的大多数资料也都是围绕这个工程展开的。

    AppDelegate.m中可以看到,rn 是从创建一个RCTRootView开始运作的,而创建RCTRootView的第一步就是需要创建一个RCTBridge对象。实际上,他们也是整个rn项目中最为关键的两个部分,他们的职责分别是:

    • RCTBridge:native与js交互的桥梁,实现native与js的互相调用
    • RCTRootView:native组件的视图容器。以及rn 程序启动的入口。

    RCTBridge

    RCTBridge只是jsBridge对外暴露的壳,实际上,RCTBridge的核心实现有两种,分别是BatchedBridge和CxxBridge,Facebook官方表示后续BatchedBridge将会逐步被CxxBridge取代。但是替换的原因上网找了一圈也没找到,简单看过CxxBridge的实现,似乎线程模型有所改变,C++实现效率上应该也会高一些(具体原因欢迎知道的同学下方评论告知,万分感谢)上面说到,RCTBridge的主要作用是承载native和js的交互,这里放一张经典的 rn 通信模型:

    通信图.png

    接下来我们就来分阶段的看看rn通信的具体流程~

    BatchedBridge的初始化

    以下是BatchedBridge的初始化时序图,初始化过程中的一些耗时操作均是在com.facebook.react.RCTBridgeQueue这个并发队列中进行的。

    RCTBridge初始化

    JsBundle 加载

    JsBundle的加载流程比较简单,开发者可以选择实现RCTBridge delegate的loadSourceForBridge:onProgress:onComplete:方法或loadSourceForBridge:withBlock:来自定义bundle的加载流程。利用这个代理我们可以实现jsBundle的服务端加载等离线包逻辑。

    rn默认的包加载逻辑实现比较简单,优先通过传入的bundle路径去同步读取文件数据,如果读取不到就会创建一个异步task进行网络请求和包加载。需要注意的是jsBundle的包类型有3种:

    • StringBundle:普通的字符串jsBundle
    • RAMBundle:random access jsBundle,实际上就是在js执行的时候按需加载各个部分的js代码,具体实现可以参考[RCTJSExcutor registerNativeRequire]
    • BCBundle:字节码 bundle,应该是js编译过后得到的二进制数据。

    Native模块接口暴露及注册

    rn定义并实现了native向js 暴露模块和方法的协议。当js调用native module时,jsBridge会提供一个native模块对应的js对象,这个对象会包含native模块对js暴露的方法。而js对native模块的操作就是对这个js对象的操作。

    模块初始化的过程是从[RCTBatchedBridge initModulesWithDispatchGroup:]方法开始的,这个方法中完成了与native模块一一对应的RCTModuleData对象的实例化,下面是module实例化的关键循环。

    for (Class moduleClass in RCTGetModuleClasses()) {
        // 略去非关键部分
        // Instantiate moduleData (TODO: can we defer this until config generation?)
        moduleData = [[RCTModuleData alloc] initWithModuleClass:moduleClass
                                                         bridge:self];
        moduleDataByName[moduleName] = moduleData;
        [moduleClassesByID addObject:moduleClass];
        [moduleDataByID addObject:moduleData];
      }
    

    循环内容是RCTGetModuleClasses()的返回值,查看代码发现这实际上是一个静态数组RCTModuleClasses,而对这个数组进行操作的的方法只有一个RCTRegisterModule(Class moduleClass)。再进一步的,通过搜索代码发现,这个函数唯一的调用是在RCT_EXPORT_MODULE宏定义内。这不就就是rn模块暴露的宏么。

    #define RCT_EXPORT_MODULE(js_name) \
    RCT_EXTERN void RCTRegisterModule(Class); \
    + (NSString *)moduleName { return @#js_name; } \
    + (void)load { RCTRegisterModule(self); }
    

    可以看到,这个宏会自动为类添加moduleName方法,并将模块名作为返回值,同时会在类的load方法中去注册自身。这样一来,每一个需要暴露给js的模块就在不经意间向Bridge完成模块的注册。同理,在rn中native模块的方法,属性,常理等信息的暴露都是通过rn提供的宏。例如模块的方法暴露是通过RCT_EXPORT_METHOD

    JSCExecutor 初始化

    JSCExecutor 即js代码的运行环境。rn主要是使用JSCore作为Js的执行引擎,不过bridge这层对JsCore并没有直接依赖,而是通过宏来进行了解耦合,并且支持自定义JsExecutor(如使用v8作为Js执行引擎实现自定义的JSCExecutor)。JSCExecutor的初始化入口为[JSCExecutor setup],总的来说做了两件事:

    • 创建js执行的上下文环境
    • 向js上下文中注入js与native通信的方法

    JSCExecutor初始化过程中向JsContext中注入了最基础的几个native方法用于js与native的通讯:

    • nativeRequireModuleConfig:js获取native module配置表
    • nativeFlushQueueImmediate : js触发native进行队列消息处理
    • nativeCallSyncHook:同步调用

    这几个方法的具体调用时机会在下面详细介绍。

    创建Module配置表

    与JSCExecutor初始化同时进行的还有module配置表的创建过程,这一步的主要目的是将所有native module的信息收集起来,并且生成配置表。最后注入到JSCExecutor当中。配置表的创建入口是[RCTBatchedBridge moduleConfig],可以看到方法逻辑就是简单的将moduleData.config添加到数组中并返回,而这个config就是其中关键,链接到[RCTModuleData config]。这个方法就是收集native module的关键方法,module信息的收集主要包括:

    • 搜集常量信息,通过constantsToExport 方法读取。
    • 搜集method信息(也就是前面说到模块通过宏暴露方法信息)并实例化对应的RCTModuleMethod对象
    • 对method进行分类,这里有两类,一类为promise方法,一类是同步方法。区分办法很简单,promise方法需要使用到RCTPromiseResolveBlockRCTPromiseRejectBlock的block,所以只需要检查方法定义是否包含RCTPromise字段就行了。

    JsModule 初始化:

    在JSExcutor和native module配置表都准备完毕之后,配置表会被注入到了JsExcutor当中,具体执行的逻辑在 [RCTBatchedBridge injectJSONConfiguration:onComplete:] 也就是说main.jsbundle的代码被执行时,js的上下文中已经包含了module配置表信息,其中每一个module的结构都遵循下面的规律:

    [moduleName,constants,methods,promiseMethods,syncMethods]

    具体结构如下(可以在Chrome或是Safari的interceptor查看变量__fbBatchedBridgeConfig):

    BridgeConfig.png

    在工程目录下node_modules/react-native/Libraries/BatchedBridge下的NativeModule.js可以找到Js上下文环境中初始化module的代码,同文件夹下还包含了与native bridge通信的另外两个关键的js文件:MessageQueue.jsBatchedBridge.js。这3个js文件在执行main.jsbundle时会被执行,他们负责创建js端的bridge和初始化js module。来看下js module的创建逻辑:

    // NativeModule.js 中的genModule方法,非重要部分已略去
    // module初始化
    function genModule(config: ?ModuleConfig, moduleID: number): ?{name: string, module?: Object} {
      // 解析配置信息  
      const [moduleName, constants, methods, promiseMethods, syncMethods] = config;
      
      const module = {};
      methods && methods.forEach((methodName, methodID) => {
        const isPromise = promiseMethods && arrayContains(promiseMethods, methodID);
        const isSync = syncMethods && arrayContains(syncMethods, methodID);
        const methodType = isPromise ? 'promise' : isSync ? 'sync' : 'async';
        module[methodName] = genMethod(moduleID, methodID, methodType);
      });
      Object.assign(module, constants);
    
      return { name: moduleName, module };
    }
    
    // method 初始化
    function genMethod(moduleID: number, methodID: number, type: MethodType) {
      let fn = null;
      if (type === 'promise') {
        fn = function(...args: Array<any>) {
          return new Promise((resolve, reject) => {
            // promise 是直接走BatchedBridge.enqueueNativeCall的
            BatchedBridge.enqueueNativeCall(moduleID, methodID, args,
              (data) => resolve(data),
              (errorData) => reject(createErrorFromErrorData(errorData)));
          });
        };
      } else if (type === 'sync') {
        fn = function(...args: Array<any>) {
           // sync方法直接调用初始化时jsc注入nativeCallSyncHook方法
          return global.nativeCallSyncHook(moduleID, methodID, args);
        };
      } else {
        fn = function(...args: Array<any>) {
          // 其余均认为是异步调用走BatchedBridge.enqueueNativeCall
          BatchedBridge.enqueueNativeCall(moduleID, methodID, args, onFail, onSuccess);
        };
      }
      fn.type = type;
      return fn;
    }
    
    

    这里面比较关键的Js module的方法声明,可以看到:

    • promise方法的声明调用了BatchedBridge.enqueueNativeCall
    • sync方法的声明调用的是global.nativeCallSyncHook
    • 其余方法调用均为认为是异步也是通过BatchedBridge.enqueueNativeCall

    Native与Js的数据通信

    到目前为止,js端和native端的module都已经准备完成了,接下来bridge将开始处理js和native相互调用,这一部分算是RCTBridge的核心部分了,下面是通讯时序图:

    数据通信时序图

    Js 调用Native:

    以js调用UIManager模块的measureLayout方法为例,js调用native的调用栈如下:

    ----------------------------------以下为Js端调用栈------------------------------------------
    UIManager.measureLayout(params,onSuccess,onFail)
    BatchedBridge.enqueueNativeCall(moduleID, methodID, args, onFail, onSuccess)      
    MessageQueue.enqueueNativeCall(moduleID, methodID, args, onFail, onSuccess) // 保存callback 
    MessageQueue_queue[PARAMS].push(params) //调用入队,如果距上一次刷新消息队列的时间间隔达到阈值,则触发更新
    global.nativeFlushQueueImmediate(queue)                                 
    ----------------------------------以下为Native端调用栈--------------------------------------
    context[@"nativeFlushQueueImmediate"] block invoke 
    [RCTBatchedBridge handleBuffer:batchEnded:]
    [RCTBatchedBridge handleBuffer:] // 批量处理队列中的调用消息,把调用分发到各个module对应的queue中处理
    [RCTBatchedBridge callNativeModule:method:params:]  // 找到nativeModule对应的方法并执行
    [RCTBridgeMethod invokeWithBridge:module:arguments:] // 开始执行module对应的方法
    [RCTBridgeMethod processMethodSignature]  // 初始化这次调用的invocation对象 这个方法大量使用到runtime
    [UIManager measureLayout] //目标方法真正被执行
    RCTPromiseResolveBlock block invoke  // 方法逻辑执行完毕后回调被执行
    [RCTBatchedBridge enqueueCallback:args:]  // 通过bridge回调结果
    [RCTBatchedBridge _actuallyInvokeCallback:arguments:] 
    [RCTJavaScriptExecutor invokeCallbackID:arguments:callback:] //执行invokeCallbackAndReturnFlushedQueue
    [RCTJavaScriptExecutor _executeJSCall:arguments:unwrapResult:callback:]  // 使用jscontext触发js端处理回调
    ----------------------------------以下为Js端调用栈------------------------------------------
    MessageQueue.invokeCallbackAndReturnFlushedQueue()
    MessageQueue.__invokeCallback()   //触发js端保存的callbck回调
    

    Native 调用Js:

    以native调用AppRegistry模块的的runApplication方法为例子,native调用js的调用栈如下:

    ----------------------------------以下为Native端调用栈--------------------------------------
    [RCTBatchedBridge enqueueJSCall:method:args:completion:]  //开始Js模块调用
    [RCTBatchedBridge _actuallyInvokeAndProcessModule:method:arguments:]  //执行模块方法
    [RCTJavaScriptExecutor callFunctionOnModule:method:arguments:callback:] 
    [RCTJavaScriptExecutor _callFunctionOnModule:method:arguments:returnValue:unwrapResult:callback:]
    [RCTJavaScriptExecutor _executeJSCall:arguments:unwrapResult:callback:] //区分是否有返回值,调用不同的方法
    ----------------------------------以下为Js端调用栈------------------------------------------
    MessageQueue.callFunctionReturnResultAndFlushedQueue()  //js端处理调用消息
    MessageQueue.__callFunction()  // 找到对应js module,执行方法并回调结果
    ----------------------------------以下为Native端调用栈--------------------------------------
    onComplete block invoke 
    

    就0.44.3的rn bridge的通信模型其实是native端和js端分别维护了一个消息队列,各端的调用以及callback消息都会被存储在队列中。有意思的是,两端的数据通信并不是完全分离的调用,在native端对js端的一次调用中,js端callback的同时还会携带上js队列中的数据,而native在收到回调的时候,不仅会将结果返回给调用方,还会顺带处理js端发过来的其他消息。这样消息调用的循环就建立了起来。

    同步调用:

    同步调用相对于异步而言就简单了不少,nativeCallSyncHook的实现实际上就是通过runtime调用native模块。并且同步调用并不会切换到module的queue,而是直接在js线程进行处理,达到阻塞js端的效果。

    RCTRootView

    RCTRootView是rn渲染的关键,上面已经讲到RCTJsBridge实现了native module与js module之间的相互调用。在这个基础之上,我们写的React代码将最终被渲染成Native布局,接下来看看具体实现。

    React.js与ReactNative的桥接

    用过react.js的同学应该都听说过vDom,如果没听过,请先研究下这篇文章:virtual-dom原理与简单实现

    目前react将核心渲染部分抽离,定义了一套渲染需要的API标准(详见ReactFiberHostConfig),理论上所有的平台只要实现react标准的API就能够对接上react的vDom实现。例如:

    重点关注一下ReactNativeHostConfig.js的实现,我在其中发现了这样一段代码:

    // Modules provided by RN:
    import UIManager from 'UIManager';
    

    通读了下代码文件,这个js文件里面基本上是对React API标准的实现。其中大量使用到了UIMananger这个类,而且在上面的代码注释中写道,这个Modules由ReactNative提供。也就是说,React的dom diff的结果最终是通过UIManager作用到ReactNative上的。

    UIManager

    RCTRootView的初始化过程中会注册3个通知,比较重要的是RCTJavaScriptDidLoadNotification,在js load结束后,会创建RCTRootContentView ,并且执行runApplication方法。其实就是创建承载内容视图的view并开始运行app的逻辑。而在RCTRootContentView的实例化方法中,我们又一次见到了rn渲染的关键module:UIMananger。

    UIManager的初始化过程会获取所有继承至RCTViewManager的所有module,并将其保存在_componentDataByName的字典中。既然是module自然有对js提供的method:

    注意:Native module的初始化逻辑基本都是卸载setBridge: 方法当中的,setBridge会在module实例化的时候由BatchedBridge调用。

    image.png

    不难发现,UIManager提供的这些方法都是用于操作view的,譬如创建和移除view,调整view的关系,设置view的属性等。而这些其实就是dom API的oc实现。rn通过UIManager实现了dom API的子集。有了这些API,js端就能够像操作dom一样操作native view tree。

    下面分析下最常用的两个UIManager的API接口实现:

    RCT_EXPORT_METHOD(createView:viewName:rootTag:props:大致逻辑如下:

    createView流程图

    RCT_EXPORT_METHOD(updateView:viewName:props:)大致逻辑如下:

    updateView流程图

    看到这里很自然会产生这样的疑问:

    • 什么是shadowView?
    • UIBlock什么时候被执行?

    ShadowView tree

    上面个讲到的两个API都同时操作了view和shadowView,而在简单查看了所有的UIMananger暴露的API接口后,我发现所有的API都会对view和shadowView进行操作。到底什么是shadowView呢?在RCTShadowView.h的注释中,我找到了答案:

    /**

    • ShadowView tree mirrors RCT view tree. Every node is highly stateful.

      1. A node is in one of three lifecycles: uninitialized, computed, dirtied.
      1. RCTBridge may call any of the padding/margin/width/height/top/left setters. A setter would dirty
    • the node and all of its ancestors.

      1. At the end of each Bridge transaction, we call collectUpdatedFrames:widthConstraint:heightConstraint
    • at the root node to recursively lay out the entire hierarchy.

      1. If a node is "computed" and the constraint passed from above is identical to the constraint used to
    • perform the last computation, we skip laying out the subtree entirely.
      */
      @interface RCTShadowView : NSObject <RCTComponent>

    简单翻译一下:

    shadowView tree 和RCT view tree一一对应,所有节点都是有状态的

    1.每个节点有3种生命周期状态,uninitialized(未初始化),computed(计算完成),dirtied(未计算)

    2.Bridge将可以随意调用shadow view的setter方法设置属性,这会导致节点变成一个dirtied节点

    3.每当bridge的批处理结束,就会调用collectUpdatedFrames:widthConstraint:heightConstraint。从而触发从根节点开始的递归布局计算

    4.如果一个节点本身处在computed状态,并且父节点的约束内容和上次相同,则会略过这个节点

    接着看RCTShadowView的代码,发现shadowView持有一个YGNode,YGNode是啥?上两篇资料:

    Yoga 官网

    如何评价 Facebook开源的 YOGA?

    一句话总结,Yoga是跨平台的FlexBox实现,主要用来实现视图布局。其实不难理解,我们写的js代码通常使用css来进行布局描述,iOS系统显然无法处理css描述的布局信息,这中间就需要Yoga这样的布局框架来进行转换。所以在rn中,每一个shadowView都持有一个YGNode,用于进行布局描述,并且在必要的时候转换成iOS系统能够理解的布局数据(iOS即是frame)

    Js布局信息=>shadowView

    上面简单介绍了下shadowView tree。接下来看看js传到native的布局信息是如何作用到shadowView上的。继续来看UImanager的updateView接口实现,关键代码在于setProps:forShadowView:,整个更新过程总结下来做了下面几件事:

    • 设置shadowView的属性

    • 循环每一个需要设置的属性

    • 获取设置属性需要的block,如果没有则创建一个并保存

    • 执行设置属性的block

    • 将一个设置view属性的block存入UIBlocks中,block逻辑和设置shadowView的相同

    这里有一个疑问,既然属性都会直接设置在view上,那么为什么还需要shadowView呢,后来我在RCTViewManager的实现中发现了秘密。

    RCTViewManager中定义了很多向Js暴露的属性(和module暴露方法一样,rn为暴露属性提供了宏RCT_EXPORT_VIEW_PROPERTYRCT_EXPORT_SHADOW_PROPERTY)而js端也是通过设置这些属性来控制native view的布局的。这些属性中所有的和布局相关的属性如top,left全部是shadowProperty。而与布局无关的属性如shadowColor,borderColor 等属性都属性ViewProperty。也就是说,布局相关的属性都存储在shadowView中,反之存储在view中。这也和shadowView中持有YGNode的相互吻合。另外我们在createPropBlock:isShadowView:中可以可以看到,当属性值在当前的view上无法找到时,会直接返回一个空的block。也就是说js,虽然在UIManager的dom API中同时操作了shadowView和view,但实际上只有对shadowView生效的属性才会被设置在shadowView上(即布局属性)view也一样。所以updateView的实际逻辑其实是:

    • 设置shadowView的属性

    • 循环每一个需要设置的属性

    • 如果属性是shadowView持有的,那么对shadowView进行设置,如果没有则跳过

    • 将一个设置view属性的block存入UIBlocks中block逻辑和设置shadowView的相同

    ShadowView布局信息=> view

    上面说到,UIMananger的API会将布局相关的属性保存在shadowView中,那么这些布局信息如和作用到view上呢。关键就在于[UIManager _layoutAndMount]

    // 提供给所有的Component在布局前只是逻辑的机会
    for (RCTComponentData *componentData in _componentDataByName.allValues) {
        RCTViewManagerUIBlock uiBlock = [componentData uiBlockToAmendWithShadowViewRegistry:_shadowViewRegistry]; 
        [self addUIBlock:uiBlock];
     }
    
    // 进行layout
    for (NSNumber *reactTag in _rootViewTags) {
        RCTRootShadowView *rootView = (RCTRootShadowView *)_shadowViewRegistry[reactTag];
        [self addUIBlock:[self uiBlockWithLayoutUpdateForRootView:rootView]]; // 添加一个用于layout的UIblock
        [self _amendPendingUIBlocksWithStylePropagationUpdateForShadowView:rootView]; // 添加用于更新view背景色属性的UIBlock
    }
    
    // 主线程通知节点bridge处理完毕
    [self addUIBlock:^(RCTUIManager *uiManager, __unused NSDictionary<NSNumber *, UIView *> *viewRegistry) {
        for (id<RCTComponent> node in uiManager->_bridgeTransactionListeners) {
          [node reactBridgeDidFinishTransaction];
        }
      }];
    
    for (id<RCTUIManagerObserver> observer in _uiManagerObservers) {
        [observer uiManagerWillFlushUIBlocks:self];
      }
    
    // 开始执行UIBlocks队列中的所有block
    [self flushUIBlocks];
    

    具体的调用逻辑如下:

    • UIManager调用uiBlockWithLayoutUpdateForRootView获取更新布局的UIBlock
    • 从rootShadowView开始递归收集所有子view的布局数据变化,并生成布局产生变化的数组,关键逻辑在[RCTRootShadowView collectViewsWithUpdatedFrames]
    • 生成UIBlock处理需要重置布局的view,block中区分了是否进行动画等逻辑,最终逻辑是设置view的frame。
    • 递归搜集从rootView到所有子节点的背景色属性设置的block。如果view本身没有背景色并且父节点有则继承父节点。关键逻辑在processUpdatedProperties:parentProperties:

    另外值得注意的一点是,所有的frame计算操作都是在UIManangerQueue队列中进行的,而非主线程,主线程做的只是最终设置计算好的frame属性,这样能够主线程的绘制效率

    结语

    移动端跨平台解决方案从最初的H5,到后来的Hybrid,再到如今的rn,weex,还有最近大火的Flutter。人们依旧在努力的寻找跨平台的最优解。rn开源至今的这三年也是快速发展迭代,到今天已经是一个庞然大物了。就我个人而言,rn是一个十分优秀的开源框架,虽然我不是一个rn的使用者,但其中涉及到的设计思以及大量的技术细节依然值得学习和借鉴。

    相关文章

      网友评论

        本文标题:ReactNative-源码解析

        本文链接:https://www.haomeiwen.com/subject/yyihgftx.html