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已经过审,大家放心可用~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容