React Native 剖析

React Native剖析

React Native 现在是异常的火爆,我司最近也完成了一个 React Native 编写的项目,现在已经提测审核。大家关心的苹果会不会拒绝RN ,会不会拒绝 CodePush ,我们会用实际行动告诉大家。

本文会介绍 React Native 的工作原理,让移动开发者从代码上了解框架。

React

React 是 facebook 出品一个前端框架,是目前最火的框架。以组件的形式组织项目结构,代替市面上的 MVC 框架。React 的出现解决了前端许多的痛点:

  • 组件化、模块化:React 天生组件化,将页面拆分成各个组件,项目的开发效率显著提升,而且极大的降低了项目维护成本。
  • 开发效率:React 的页面就是组件的组合,哪个组件出现问题就治哪里,一贴下药。再加上 ES6 语法使用,使得项目有超高的可读性,容易理解。
  • 运行效率: React实现了 Virtual DOM ,高效的算法带来高效页面渲染,使得性能更优越。
  • 可维护性:React 的组件化配合 Redux 的单向数据流,使得问题定位清晰明显。
  • JSX: 一种语法糖,可以将 HTML 写在 JS 文件里,方便、简单。

React 更像的是 MVC 中的 View 层,Model 和 Controller 的角色则由 Redux 代替( React 跟MVC 并无关系,仅仅是为了方便理解),单向数据流使得业务逻辑清晰明了。所以现在的
React 项目的常用体系是 React + Redux + webpack + ES6 + react-router 。我们的 Reat Native 项目使用的是 React + Redux + ES6 。

React Native

简单交代了 React 的背景,下面到了咱们的主角 —— React Native 。它可以看作是 React 的亲儿子,把全身的本领都传授了下去,儿子也挺争气,在 iOS 端跟 Android 端也有跨越性的突破。于是乎它就有了“跨平台”、“Javascript编写Native项目”史诗级的技能,他的表叔
Microsoft 对它也是疼爱有加,怕他挨 Native 欺负,给它做了一个叫CodePush 的装备,随时升级加修复,“热更新”这个标签又贴到了它的身上。现在的它是集万千宠爱于一身,要风得风,要雨得雨。我们现在就来扒一扒,看它究竟是何方神圣。

原理概述

引用React Native 从入门到原理中的一段话。

首先要明白的一点是,即使使用了 React Native,我们依然需要 UIKit 等框架,调用的是 Objective-C 代码。总之,JavaScript 只是辅助,它只是提供了配置信息和逻辑的处理结果。React Native 与 Hybrid 完全没有关系,它只不过是以 JavaScript 的形式告诉 Objective-C 该执行什么代码。

工作流程.png

直白的说,Javascript 在上层完成逻辑处理,然后通过 Javascript 引擎,实现 Javascript 与 Objective-C 交互,调用 Objective-C 中的原生UI组件,来实现页面的渲染。

JavaScript 是一种单线程的语言,它不具备自运行的能力,因此总是被动调用。很多介绍 React Native 的文章都会提到 “JavaScript 线程” 的概念,实际上,它表示的是 Objective-C 创建了一个单独的线程,这个线程只用于执行 JavaScript 代码,而且 JavaScript 代码只会在这个线程中执行。

React Native初始化

在 AppDelegatedidFinishLaunchingWithOptions方法中,我们找到了RN的入口。

    RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                  moduleName:@"demo"
                                           initialProperties:nil
                                               launchOptions:launchOptions];

首先创建了一个根控制器的View,然后将RN创建的view添加到窗口上显示。这个方法分为两步。在initWithBundleURL:moduleProvider:launchOptions这个方法内,React Native创建了一个实现 Objective-C 与 Javascript 交互的全局bridge,后续的所有交互全部都是通过这个桥接实现的。
第二步initWithBridge:moduleName:initialProperties方法中返回了刚才的RCTRootView

第一步初始化的核心方法是setUp。这个方法主要是创建了加载
main.jsbundle 的地址和创建了BatchedBridge。这个BatchedBridge才是真正的主角,它的主要作用就是读取 Javascript 对 Objective-C 的方法调用,而且它的内部持有一个
JavascriptExecutor 对象,用来执行 Javascript 代码。
- (void)setUp
{
...
[self createBatchedBridge];
[self.batchedBridge start];
...
}

RCTBatchedBridge

RCTBatchedBridge中最重要的就是Start方法。该方法主要包含以下几步:

  1. 读取Javascript代码
  2. 初始化需要暴露给js调用的Native模块
  3. 异步初始化 JS executor
  4. 异步初始化模块配置列表
  5. 将配置表传入JS端
  6. 调用JS代码

下面我们详细的解析下每个步骤

  • 异步加载 JSBundle 。

      [self loadSource:^(NSError *error, NSData *source, __unused   int64_t sourceLength) {
         if (error) {
           RCTLogWarn(@"Failed to load source: %@", error);
           dispatch_async(dispatch_get_main_queue(), ^{
            [weakSelf stopLoadingWithError:error];
           });
          }
        sourceCode = source;
        dispatch_group_leave(initModulesAndLoadSource);
      } onProgress:^(RCTLoadingProgress *progressData) {
      #ifdef RCT_DEV
         RCTDevLoadingView *loadingView = [weakSelf moduleForClass:[RCTDevLoadingView class]];
        [loadingView updateProgress:progressData];
      #endif
      }];
    
  • 初始化Native模块化信息。

      // Synchronously initialize all native modules that cannot be loaded lazily
      [self initModulesWithDispatchGroup:initModulesAndLoadSource];
    

    这个方法很复杂,咱们一步步来。

    1. 首先创建了2个数组跟一字典,分别存贮 module 的类、module 数据跟两者组合的键值对,这样就形成了三份配置表。

      NSMutableArray<Class> *moduleClassesByID = [NSMutableArray new];
      NSMutableArray<RCTModuleData *> *moduleDataByID = [NSMutableArray new];
      NSMutableDictionary<NSString *, RCTModuleData *> *moduleDataByName = [NSMutableDictionary new];
      
    2. 遍历需要暴露给 Javascript 的类(也就是下文的 module ),将所有的 module 加入配置表中。每一个 module 在实例化的时候都会开一个自己的队列,保证每个模块内部的通信都是串行执行。感兴趣的同学们可以阅读源码,这个段写的非常好,处理了很多像“死锁”这样的多线程操作。

         for (Class moduleClass in RCTGetModuleClasses()) {
             NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass);
      
             // Check for module name collisions
             RCTModuleData *moduleData = moduleDataByName[moduleName];
      
              ...
      
             moduleData = [[RCTModuleData alloc] initWithModuleClass:moduleClass
                                                  bridge:self];
             moduleDataByName[moduleName] = moduleData;
             [moduleClassesByID addObject:moduleClass];
             [moduleDataByID addObject:moduleData];
         }
      
RCTRegisterModule

每个 module 都使用了RCTRegisterModule这个宏,在这个类的 load 方法的时候注册了自己的 moduleName 到 RCTModuleClasses这个数组中。这样,RN 遍历这个数组就能找到所有注册的 module 了。
RCTBridgeModule.h这个文件中我们可以看到实现。
#define RCT_EXPORT_MODULE(js_name) 
RCT_EXTERN void RCTRegisterModule(Class); 
+ (NSString *)moduleName { return @#js_name; } 
+ (void)load { RCTRegisterModule(self); }

      void RCTRegisterModule(Class moduleClass)
      {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
          RCTModuleClasses = [NSMutableArray new];
        });

        RCTAssert([moduleClass conformsToProtocol:@protocol(RCTBridgeModule)],
        @"%@ does not conform to the RCTBridgeModule protocol",
        moduleClass);

        // Register module
        [RCTModuleClasses addObject:moduleClass];
      }
`moduleName`方法,返回 `@#js_name`。`@# `是把宏参数 

js_name 转为字符串,若字符串为空则返回的就是空。在 RCTBridgeModuleNameForClass() 获取模块名的方法里,如果 moduleName 长度为0,那么就会调用 NSStringFromClass() 方法获取类名。

Tip:@#是将传入单字符参数名转换成字符。传送门--[define宏定义的#,##,@#及符号](http://blog.csdn.net/xdsoft365/article/details/5911596)。
  • 初始化 JavaScript 代码的执行器,即 RCTJSCExecutor 对象
    上一步是module加入到配置表中,有一个非常特殊的类叫 RCTJSCExecutor ,它需要创建一个实例并保存在一个RCTModuleData的实例中并且被RCTBatchedBridge持有。
    if (!_javaScriptExecutor) {
    id<RCTJavaScriptExecutor> executorModule = [self.executorClass new];
    RCTModuleData *moduleData = [[RCTModuleData alloc] initWithModuleInstance:executorModule
    bridge:self];
    moduleDataByName[moduleData.name] = moduleData;
    [moduleClassesByID addObject:self.executorClass];
    [moduleDataByID addObject:moduleData];
    // NOTE: _javaScriptExecutor is a weak reference
    _javaScriptExecutor = executorModule;
    }

    被持有后,初始化RCTJSCExecutor

    // Asynchronously initialize the JS executor
    dispatch_group_async(setupJSExecutorAndModuleConfig, bridgeQueue, ^{
    [performanceLogger markStartForTag:RCTPLJSCExecutorSetup];
    [weakSelf setUpExecutor];
    [performanceLogger markStopForTag:RCTPLJSCExecutorSetup];
    });
    

    前端的同学们肯定都知道js是单线程的,所有的js代码都是在一个单独的线程上调用的。
    RCTModuleClassessetUp方法中,开了一条专门为 Javascript
    运行的巴拿马运河。
    - (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block
    {
    if ([NSThread currentThread] != _javaScriptThread) {
    [self performSelector:@selector(executeBlockOnJavaScriptQueue:)
    onThread:_javaScriptThread withObject:block waitUntilDone:NO];
    } else {
    block();
    }
    }

    原理中的原理来了就是大家嘴中常说的“RN就是JS调OC啊”。

    [self executeBlockOnJavaScriptQueue:^{
    ...
    self->_context = [[RCTJavaScriptContext alloc] initWithJSContext:context onThread:self->_javaScriptThread];
    [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptContextCreatedNotification
                                                        object:context];
    ... 
    
    __weak RCTJSCExecutor *weakSelf = self;
    context[@"nativeRequireModuleConfig"] = ^NSArray *(NSString *moduleName) {
    RCTJSCExecutor *strongSelf = weakSelf;
    if (!strongSelf.valid) {
      return nil;
    }
    ...
    };
    

    在JSThread内创建了一个JSContext,并且为Contextz设置了不同的block,如上图。这些block会集中讲一下。

  • 生成配置表
    上一步已经获得了module的数据,这个一步则将数据转成json。

    - (NSString *)moduleConfig
     {
       NSMutableArray<NSArray *> *config = [NSMutableArray new];
       for (RCTModuleData *moduleData in _moduleDataByID) {
       if (self.executorClass == [RCTJSCExecutor class]) {
         [config addObject:@[moduleData.name]];
       } else {
         [config addObject:RCTNullIfNil(moduleData.config)];
       }
     }
    
       return RCTJSONStringify(@{
       @"remoteModuleConfig": config,
       }, NULL);
     }
    
  • 将配置表传入JS端
    在生成Native配置表跟初始化RCTJSCExecutor完成后,就要将配置表传入JS端了,使两端拥有同一份配置表。

    - (void)injectJSONConfiguration:(NSString *)configJSON
                   onComplete:(void (^)(NSError *))onComplete
    {
      ...
    [_javaScriptExecutor injectJSONText:configJSON
                asGlobalObjectNamed:@"__fbBatchedBridgeConfig"
                           callback:onComplete];
    }
    

    JS拥有了变量名为__fbBatchedBridgeConfig的全局变量。在JS端获得配置表后就开始执行js内部业务逻辑了。 JS 端如何使用这个配置我们下一篇会介绍。

  • 执行 js 代码
    在所有的准备工作都完成后,就开始通过executeSourceCode执行
    js 代码了。

     dispatch_group_notify(initModulesAndLoadSource, bridgeQueue, ^{
       RCTBatchedBridge *strongSelf = weakSelf;
       if (sourceCode && strongSelf.loading) {
         [strongSelf executeSourceCode:sourceCode];
       }
     });
    

executeSourceCode这个方法里,运行指定 url 的 js 代码。dev 模式下运行的是你的 bundleURL 下的代码,release 环境下运行的是你已经打包好的 jsbundle ,如果你使用了 codePush 对应的运行的是你 codePush 下发的 jsbundle。executeSourceCode开启了一个 NSRunLoop 不断的打印引入 native 模块中的 log 。

小结

本文一步步剖析了 ReactNative 这个伟大的框架,当然我这只是蜻蜓点水, ReactNative 还有许多值得我们学习的东西没有指出来,希望大家都去看看源码提高自己。后续,我会继续写文讲解下ReactNative如何将一个View页面展示出来和其他的一些小细节的东西。

PS

今天是2017-06-02,我们的APP已经过审,大家放心可用~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值