RN iOS开发相关

ReactNative-源码解析

2018-09-07  本文已影响301人  5ea1aaa189a6

前言

最近由于工作的关系需要研究一下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

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种:

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],总的来说做了两件事:

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

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

创建Module配置表

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

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的方法声明,可以看到:

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 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:,整个更新过程总结下来做了下面几件事:

这里有一个疑问,既然属性都会直接设置在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布局信息=> 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];

具体的调用逻辑如下:

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

结语

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

上一篇下一篇

猜你喜欢

热点阅读