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已經過審,大家放心可用~

發佈了43 篇原創文章 · 獲贊 56 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章