React Native原理

本文基于iOS平臺來分析React Native v0.68.1的原理。閱讀本文章需要一定的React Native開發(fā)基礎(chǔ)。

React Native的作用

React Native來源于React。React是Facebook開源的用來構(gòu)建用戶界面的一個JavaScript庫,在web頁面開發(fā)領(lǐng)域取得了巨大的成功。在擁有了大量優(yōu)秀的React開發(fā)者后,F(xiàn)acebook想,能不能使用React來開發(fā)手機(jī)app?于是就有了React Native。

所以,如果說React是用來在web瀏覽器上做用戶界面的JavaScript庫,那么React Native就是用來在iOS和Android上做用戶界面的JavaScript庫。

當(dāng)然,通過JavaScript語言自身的能力,手機(jī)app的業(yè)務(wù)邏輯也完全可以通過React Native完成。

所以通過React Native,你可以:

  1. 一套代碼完成iOS和Android的開發(fā);
  2. 開發(fā)app中的某些頁面(Hybrid App)。

React Native的基礎(chǔ)

現(xiàn)在我們知道,React Native來源于React,是一個JavaScript庫。所以React Native的基礎(chǔ),就是iOS和Android需要有執(zhí)行JavaScript代碼的能力。

iOS從iOS7開始開放JavaScriptCore框架。通過JavaScriptCore,Objective-C和JavaScript之間就能相互調(diào)用了:

#import <JavaScriptCore/JavaScriptCore.h>
...
// 初始化JavaScriptCore上下文
JSContext *ctx = [[JSContext alloc] init];

// 執(zhí)行JavaScript代碼:
[ctx evaluateScript:@"var sum = 1 + 1;"];
// 獲取上面的執(zhí)行結(jié)果sum:
JSValue *sum = ctx[@"sum"];
NSLog(@"sum = %d", [sum toInt32]); // sum = 2

// 將Objective-C的block傳入JavaScript
ctx[@"add"] = ^(int a, int b) {
    return a + b;
};
// 在JavaScript中調(diào)用Objective-C中的block:
[ctx evaluateScript:@"sum = add(3, 4);"];
// 獲取上面的執(zhí)行結(jié)果sum:
sum = ctx[@"sum"];
NSLog(@"sum = %d", [sum toInt32]); // sum = 7

目前Android也是使用JavaScriptCore來實(shí)現(xiàn)Java與JavaScript的相互調(diào)用。但是Android使用JavaScriptCore存在性能問題。為了解決這個問題,F(xiàn)acebook引入了新的JavaScript引擎Hermes。但是HermesReact Native v0.68.1中還是可選狀態(tài)。

React Native的工作原理

有了JavaScript和Objective-C能夠互相調(diào)用的基礎(chǔ)后,React Native的工作原理很簡單:

  1. JavaScript調(diào)用Objective-C構(gòu)建用戶界面。iOS并不支持使用JavaScript來構(gòu)建用戶界面。而Objective-C有構(gòu)建用戶界面的能力(通過UIKit)。所以JavaScript是通過調(diào)用Objective-C間接完成用戶界面的構(gòu)建;

  2. Objective-C把系統(tǒng)事件傳遞給JavaScript進(jìn)行處理。系統(tǒng)事件包括觸摸屏幕事件,定時器事件等。例如用戶點(diǎn)擊了一個按鈕,那么Objective-C會把這個觸摸事件傳遞給JavaScript進(jìn)行處理。

這就是React Native的工作原理。當(dāng)然JavaScript和Objective-C相互調(diào)用的內(nèi)容不止上面這些,除此之外,JavaScript還可以通過調(diào)用Objective-C獲取iOS的硬件、數(shù)據(jù)庫、網(wǎng)絡(luò)等能力;Objective-C還會通過調(diào)用JavaScript進(jìn)行初始化等。

所以React Native最終是通過Objective-C(UIKit)來構(gòu)建用戶界面的,因此我們說React Native的性能接近于原生。那為什么是“接近”而不是“等于”呢?是因?yàn)樵谀壳暗募軜?gòu)中,JavaScript與Objective-C的相互調(diào)用是異步的,可能存在幾毫秒的延遲。所以React Native的性能只能說是接近原生。

React Native v0.68.1中,F(xiàn)acebook發(fā)布了新的架構(gòu),該架構(gòu)目前還是可選階段。等到該架構(gòu)正式使用后,相信React Native的性能會進(jìn)一步提升。

React Native的開發(fā)流程

捋一捋React Native的開發(fā)流程,可以幫助我們更好地了解React Native的原理。

創(chuàng)建React Native工程

首先需要創(chuàng)建React Native工程,你可以在已有的原生項(xiàng)目中引入React Native(Hybrid App),也可以從零開始創(chuàng)建一個React Native工程。

一個最基本的React Native的工程目錄是這樣的:

工程目錄

其中,android目錄是Android Studio的工程目錄,存放的是Android的原生代碼;ios目錄是Xcode的工程目錄,存放的是iOS的原生代碼;node_modules目錄、package-lock.jsonpackage.json這三個文件是通過npm導(dǎo)入JavaScript庫需要的文件,至少需要導(dǎo)入reactreact-native兩個JavaScript庫;index.js是開始編寫我們自己的JavaScript代碼的文件。

編寫JavaScript代碼

可以做一個Hybrid App作為例子。我們使用React Native來做一個最簡單的用戶界面:一個紅色的方塊。那么我可以在index.js文件中編寫如下代碼:

// 從react和react-native兩個JavaScript庫中導(dǎo)入所需要的對象
import React from 'react'
import { AppRegistry, View } from 'react-native'

// 實(shí)現(xiàn)紅色方塊
const RedBlock = props => {
    return <View style={{flex: 1, backgroundColor: 'red'}} />;
};

// 注冊紅色方塊
AppRegistry.registerComponent('RedBlock', () => RedBlock);

這樣JavaScript部分的代碼就完成了。

打包JavaScript代碼

我們的JavaScript代碼分散在index.jsnode_modules文件夾內(nèi)的各個文件內(nèi),React Native需要把它們打包成一個文件來執(zhí)行。

在React Native工程根目錄下,可以通過終端命令npx react-native bundle進(jìn)行打包(可以通過npx react-native bundle -h命令查看其用法):

npx react-native bundle --entry-file index.js --platform ios --dev false --bundle-output ReactNative.jsbundle --assets-dest ./ 

就可以把所有的JavaScript代碼打包成一個文件了。在當(dāng)前的項(xiàng)目中,全部的JavaScript代碼有400多kb大小,400多行代碼。

提示:在實(shí)際開發(fā)時,不需要這樣打包。開發(fā)的時候會通過node啟動一個http服務(wù)器,JavaScript代碼是從這個http服務(wù)器拉取的。每當(dāng)你修改了JavaScript代碼后,會重新從http服務(wù)器拉取最新的JavaScript代碼,從而實(shí)現(xiàn)熱調(diào)試功能。

JavaScript代碼打包好后,把它拖進(jìn)Xcode工程內(nèi),準(zhǔn)備讓Objective-C調(diào)用。

編寫原生代碼

一般情況下我們并不需要編寫原生代碼,因?yàn)檫@意味著我們需要懂得原生開發(fā),這不是React Native愿意看到的。React Native希望我們可以使用JavaScript完成整個app的開發(fā)。

但是我們這個項(xiàng)目是一個Hybrid App,所以還是需要寫一點(diǎn)原生代碼的。其實(shí)很簡單,所有的魔法都是從一個RCTRootView開始的:

#import "ViewController.h"
#import <React/RCTRootView.h>

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 我們打包的JavaScript代碼的URL
    NSURL *bundleURL = [[NSBundle mainBundle] URLForResource:@"ReactNative" withExtension:@"jsbundle"];
    
    // 創(chuàng)建RCTRootView
    RCTRootView *redblock = [[RCTRootView alloc] initWithBundleURL:bundleURL moduleName:@"RedBlock" initialProperties:nil launchOptions:nil];
    redblock.frame = CGRectMake(100,100,100,100);
    [self.view addSubview:redblock];
}

@end

創(chuàng)建RCTRootView需要bundleURL和moduleName兩個參數(shù)。bundleURL就是我們上一步打包的JavaScript代碼的URL,用來取得我們的JavaScript代碼。moduleName就是我們上面在index.js中使用AppRegistry.registerComponent注冊的紅色方塊組件名字。

運(yùn)行Xcode項(xiàng)目,就能等到我們的成果:

紅色方塊

這就是一個React Native項(xiàng)目開發(fā)的流程。

RCTRootView揭秘

RCTRootView是React Native的魔法發(fā)生的地方。所以,要想了解React Native,就要從RCTRootView入手??梢韵瓤纯?/code>RCTRootView`指定的構(gòu)造方法:

- (instancetype)initWithFrame:(CGRect)frame bridge:(RCTBridge *)bridge moduleName:(NSString *)moduleName initialProperties:(nullable NSDictionary *)initialProperties;

其中最重要的參數(shù)是bridgemoduleName,moduleName前面提到過,它是JavaScript代碼中使用AppRegistry.registerComponent注冊的組件名。我們的重點(diǎn)是bridge,它是React Native中最核心的東西。bridge顧名思義就是“橋”的意思,它發(fā)揮的作用也恰恰和橋一樣:它是JavaScript和Objective-C之間的一座橋,用來實(shí)現(xiàn)JavaScript和Objective-C之間的相互調(diào)用。

這里不深究bridge。但是我們需要知道,bridge創(chuàng)建時,需要去加載和執(zhí)行JavaScript代碼。沒錯,就是上面打包的400多kb,400多行的JavaScript代碼。bridge會創(chuàng)建一個JavaScriptCore上下文,然后從頭到尾執(zhí)行這份JavaScript代碼,執(zhí)行完畢后,React Native中的各種變量就存在這個上下文中了(例如AppRegistry對象),就準(zhǔn)備好隨時被Objective-C調(diào)用。

RCTRootView的源碼比較簡單,它主要做的事情是:

  1. 創(chuàng)建并持有bridge;
  2. 監(jiān)聽JavaScript的執(zhí)行情況;
  3. 當(dāng)監(jiān)聽到JavaScript代碼執(zhí)行完畢后,通過bridge調(diào)用JavaScript中的AppRegistry模塊的runApplication方法,隨后就開始進(jìn)行用戶界面構(gòu)建了。

bridge淺析

React Native目前是通過bridge來實(shí)現(xiàn)JavaScript和Objective-C之間相互調(diào)用的,所以bridge是React Native的核心。但是bridge比較復(fù)雜,而且新的架構(gòu)已經(jīng)發(fā)布了,目前的bridge(React Naitve 0.68.1)將會被淘汰,所以這里只會簡單分析一下當(dāng)前bridge的特點(diǎn)。

批量處理的bridge

RCTRootView的bridge是一個RCTBridge對象。RCTBridge的實(shí)際功能是由其內(nèi)部變量batchedBridge實(shí)現(xiàn)的(目前batchedBridge是一個RCTCxxBridge對象)。batchedBridge,可以翻譯為“批量處理的橋”,通過閱讀源碼可以知道,在當(dāng)前的bridge設(shè)計(jì)中,JavaScript和Objective-C的相互調(diào)用是批量處理的。

批量處理,就是說JavaScript和Objective-C的相互調(diào)用不會馬上執(zhí)行,而是攢夠一批,然后再一起處理。例如當(dāng)用戶的手指在屏幕上滑動時,會在短時間內(nèi)產(chǎn)生大量的觸摸事件,根據(jù)當(dāng)前的bridge的設(shè)計(jì),這些觸摸事件是成批傳遞給JavaScript的,而不是每一個觸摸事件單獨(dú)實(shí)時進(jìn)行傳遞。

加載并執(zhí)行JavaScript代碼

bridge在創(chuàng)建時,最重要的任務(wù)是加載并執(zhí)行JavaScript代碼。JavaScript代碼就是前面我們說的通過npx react-native bundle命令打包的JavaScript代碼,在前面我們的RedBlock項(xiàng)目中,打包后的代碼大概是400多kb,共400多行代碼。這400多行代碼執(zhí)行完畢后,React Native的環(huán)境就準(zhǔn)備就緒了(React Native需要的各種變量已創(chuàng)建完畢),就可以準(zhǔn)備好被Objective-C調(diào)用。

JavaScript是一種沒有多線程能力的語言。在當(dāng)前bridge的設(shè)計(jì)中,會創(chuàng)建一個名為"com.facebook.react.JavaScript"的線程,然后讓所有JavaScript都在這個線程中執(zhí)行。

Objective-C和JavaScript相互調(diào)用的方式

JavaScript代碼被執(zhí)行后,會創(chuàng)建一個MessageQueue變量。MessageQueue是bridge的重要組成部分,因?yàn)镴avaScript和Objective-C相互調(diào)用的兩個重要方法都在MessageQueue里。

首先,Objective-C對JavaScript的調(diào)用,都是通過調(diào)用MessageQueue的callFunctionReturnFlushedQueue方法來實(shí)現(xiàn)的:

  callFunctionReturnFlushedQueue(
    module: string,
    method: string,
    args: mixed[],
  ): null | [Array<number>, Array<number>, Array<mixed>, number] {
    this.__guard(() => {
      this.__callFunction(module, method, args);
    });

    return this.flushedQueue();
  }

callFunctionReturnFlushedQueue方法需要傳入module、methodargs三個參數(shù),通過這三個參數(shù)就可以確定Objective-C需要調(diào)用JavaScript的哪個模塊(類),哪個方法和傳入的參數(shù)。

而JavaScript對Objetive-C的調(diào)用也和callFunctionReturnFlushedQueue方法有關(guān),因?yàn)樵摲椒▓?zhí)行后會返回一個flushedQueue,這個flushedQueue存放的是JavaScript需要調(diào)用的Objective-C的內(nèi)容。Objective-C拿到這個flushedQueue后,就會執(zhí)行JavaScript需要調(diào)用的內(nèi)容。

而這個flushedQueue的內(nèi)容,是通過MessageQueueenqueueNativeCall方法來添加的:

// 為了方便理解,已剔除調(diào)試和打印代碼
enqueueNativeCall(
    moduleID: number,
    methodID: number,
    params: mixed[],
    onFail: ?(...mixed[]) => void,
    onSucc: ?(...mixed[]) => void,
  ) {
  // 處理回調(diào)方法
    this.processCallbacks(moduleID, methodID, params, onFail, onSucc);
    // 將要調(diào)用的Objective-C內(nèi)容保存到隊(duì)列,體現(xiàn)了批處理的特點(diǎn)
    this._queue[MODULE_IDS].push(moduleID);
    this._queue[METHOD_IDS].push(methodID);
    this._queue[PARAMS].push(params);

    // 如果距離上一次調(diào)用超過MIN_TIME_BETWEEN_FLUSHES_MS(5ms)
    // 立即讓Objective-C執(zhí)行隊(duì)列內(nèi)容
    // 所以JavaScript對Objective-C的調(diào)用最多是5ms一批
    const now = Date.now();
    if (
      global.nativeFlushQueueImmediate &&
      now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS
    ) {
      const queue = this._queue;
      this._queue = [[], [], [], this._callID];
      this._lastFlush = now;
      global.nativeFlushQueueImmediate(queue);
    }
  }

enqueueNativeCall方法需要傳入moduleID、methodIDparams等參數(shù),代表著要調(diào)用的Objective-C哪個模塊(類),哪個方法,和傳入?yún)?shù)。

所以JavaScript和Objective-C的相互調(diào)用,是通過MessageQueue的callFunctionReturnFlushedQueueenqueueNativeCall兩個方法來實(shí)現(xiàn)的。

知道Objetive-C和JavaScript相互調(diào)用的渠道后,很容易將相互調(diào)用的內(nèi)容打印出來。在上面的項(xiàng)目中,為了構(gòu)建紅色方塊用戶界面,JavaScript和Objetive-C相互調(diào)用的情況如下(已省略部分無關(guān)調(diào)用):

OC->JS AppRegistry runApplication 
JS->OC UIManager createView
JS->OC UIManager createView
JS->OC UIManager setChildren
JS->OC UIManager createView
JS->OC UIManager setChildren
JS->OC UIManager setChildren

可以看到,Objective-C首先調(diào)用JavaScript的AppRegistry模塊的runApplication方法。然后,JavaScript通過調(diào)用Objective-C的UIManager模塊的createViewsetChildren方法來創(chuàng)建用戶界面。

如果你用手指在React Native構(gòu)建的用戶界面上滑動,你會發(fā)現(xiàn):

OC->JS RCTEventEmitter receiveTouches
OC->JS RCTEventEmitter receiveTouches
OC->JS RCTEventEmitter receiveTouches
...

Objective-C通過調(diào)用JavaScript的RCTEventEmitter模塊的receiveTouches方法,將大量的觸摸事件傳遞給JavaScript進(jìn)行處理。

跨線程調(diào)用

上面提到,JavaScript是在自己專有的線程中運(yùn)行的。所以O(shè)bjective-C和JavaScript的相互調(diào)用,是要跨線程的。

每個可以被調(diào)用的Objective-C模塊都是RCTBridgeModule,RCTBridgeModule會指定自己的methodQueue,所以JavaScript調(diào)用Objective-C時也是跨線程調(diào)用的。

例如,“點(diǎn)擊一個按鈕,按鈕產(chǎn)生一個被點(diǎn)擊的動畫效果”,這個動作需要跨越的線程有:

主線程---->JavaScript線程---->動畫模塊線程---->主線程
 (傳遞觸摸事件)    (調(diào)用OC動畫模塊)  (啟動動畫) 

性能損耗

React Native的性能是接近原生的。到底有多接近原生,要看連接JavaScript和Objective-C的bridge的性能。

目前的bridge存在如下幾個地方的性能損耗:

  1. JavaScript代碼的加載和執(zhí)行。目前400kb左右的JavaScript代碼加載和執(zhí)行大概需要100ms左右??傊甁avaScript代碼越多,執(zhí)行的越久。100ms是用戶可感知的等待了;
  2. 批處理機(jī)制。JavaScript和Objective-C之間的相互調(diào)用都是批量處理的。例如在MessageQueue的enqueueNativeCall方法的實(shí)現(xiàn)中,一個調(diào)用最多有5ms的等待;
  3. 跨線程調(diào)用。上面一節(jié)提到,點(diǎn)擊一個按鈕這樣簡單的動作,都需要跨越三個線程的調(diào)用。

除了bridge的性能損耗外,React Native還有一個問題:所有的用戶界面都是在一個RCTRootView上面構(gòu)建的,這意味著這一個RCTRootView上面渲染的視圖可能會非常多,這也是會影響性能的因素。我們知道,在原生開發(fā)的時候,一個app會有很多個UIViewController,每個UIViewController承載不同的用戶界面。但是在React Native中,RCTRootView承載著所有的用戶界面(就算使用React Navigation進(jìn)行所謂的頁面跳轉(zhuǎn),實(shí)際還是在一個RCTRootView上面渲染內(nèi)容)。

React Native的前景

至此,React Native的原理分析完畢。React Native的原理就是JavaScript和Objetive-C的相互調(diào)用,相互調(diào)用的效率決定其性能。

React Native是Facebook開源的一個項(xiàng)目,F(xiàn)acebook自家的產(chǎn)品,包括Facebook、Instagram和Oculus都在使用。另外的一些大廠,例如騰訊QQ、京東和特斯拉也在使用。可以說只要Facebook沒有倒閉,React Native就會一直維護(hù)和更新,會越來越好。

React Native最大的優(yōu)勢當(dāng)然是跨平臺,一套代碼解決Android和iOS兩個平臺app的開發(fā),這簡直太棒了!另外,React Native使用Flexbox布局,并支持熱調(diào)試,開發(fā)用戶界面的效率非常高。最后,React Native支持熱更新,可以繞過上架審核的過程,快速部署新內(nèi)容。

但是目前在國內(nèi),Google開源的flutter其實(shí)比React Native更受歡迎,因?yàn)閾?jù)說flutter的性能更好?但是我認(rèn)為它們的差距其實(shí)可以忽略不計(jì),它們都只是一個工具,真正重要的是使用工具的人:你能把這個工具用的多好?

最后,如果你是一個JavaScript全棧工程師,恰好你又懂React,那么React Native豈不是美滋滋?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 原文鏈接 一、JavaScriptCore 講React Native之前,了解JavaScriptCore會有幫...
    peaktan閱讀 6,267評論 2 27
  • 本文主要大致介紹 React-Native 框架的底層原理,以及新架構(gòu)的演變。 文章骨架主體來自React Nat...
    Tenloy閱讀 6,630評論 0 4
  • 1.OC在 RCTRootView 建立 Bridge 2.使用 setUp 初始化 主要調(diào)用 setUp 創(chuàng)建 ...
    338d708389ae閱讀 758評論 0 0
  • React Native 是最近非?;鸬囊粋€話題,介紹如何利用 React Native 進(jìn)行開發(fā)的文章和書籍多如...
    零度_不結(jié)冰閱讀 780評論 0 1
  • 兩年前建立了這個文件夾,希望能入手rn多更新一點(diǎn)的,但是只更新了兩篇就斷了,因?yàn)楫?dāng)前公司規(guī)定,年底之前我必須要做一...
    RunningTeemo閱讀 872評論 0 0

友情鏈接更多精彩內(nèi)容