react native 實現原理解析
建立HelloWord工程
熟悉完環境的搭建,按照慣例,我們來建立一個HelloWord工程,用來分析RN的實現原理。
廢話不多說,show the code
首先開啟終端,輸入
react-native init YourProjectName
此時在你的finder個人頁面能看到該工程。手動開啟iOS檔案Android則開啟Android檔案。點選工程執行,或者在工程的根目錄下輸入
react-native run-ios/android
即可執行。
初識React
import React, { Component } from 'react';
import { Text, View } from 'react-native' ;
class WhyReactNativeIsSoGreat extends Component {
render() {
return (
<View>
<Text>
If you like React on the web, you'll like React Native.
</Text>
<Text>
You just use native components like 'View' and 'Text',
instead of web components like ' div' and 'span'.
</Text>
</View>
);
}
React Natieve把一組相關的 HTML 標籤,也就是 app 內的 UI 控制元件,封裝進一個元件(Component)中
如果你想問:“為什麼 JavaScript 程式碼裡面出現了 HTML 的語法”,那麼恭喜你已經初步體會到 React 的奧妙了。這種語法被稱為 JSX,它是一種 JavaScript 語法拓展。JSX 允許我們寫 HTML 標籤或 React 標籤,它們終將被轉換成原生的 JavaScript 並建立 DOM。
在 React 框架中,除了可以用 JavaScript 寫 HTML 以外,我們甚至可以寫 CSS,這在後面的例子中可以看到。
可能現在你理解React Native還有點燒腦,但是你可以這麼理解
React 是一套可以用簡潔的語法高效繪製 DOM 的框架
所謂“簡潔的語法”,因為我們可以暫時放下 HTML 和 CSS,只關心如何用 JavaScript 構造頁面。
所謂的“高效”,是因為 React 獨創了 Virtual DOM 機制。Virtual DOM 是一個存在於記憶體中的 JavaScript 物件,它與 DOM 是一一對應的關係,也就是說只要有 Virtual DOM,我們就能渲染出 DOM。
當介面發生變化時,得益於高效的 DOM Diff 演算法,我們能夠知道 Virtual DOM 的變化,從而高效的改動 DOM,避免了重新繪製 DOM。
當然,React 並不是前端開發的全部。從之前的描述也能看出,它專注於 UI 部分,對應到 MVC 結構中就是 View 層。要想實現完整的 MVC 架構,還需要 Model 和 Controller 的結構。在前端開發時,我們可以採用 Flux 和 Redux(基於Flux) 架構,它們並非框架(Library),而是和 MVC 一樣都是一種架構設計(Architecture)。
React Native
而 React 在前端取得突破性成功以後,JavaScript 開始試圖一統三端。他們利用了移動平臺能夠執行 JavaScript (指令碼語言)程式碼的能力,並且發揮了 JavaScript 不僅僅可以傳遞配置資訊,還可以表達邏輯資訊的優點。
最終,一個基於 JavaScript,具備動態配置能力,面向前端開發者的移動端開發框架,React Native,誕生了!
看到了麼,這是一個面向前端開發者的框架。它的宗旨是讓前端開發者像用 React 寫網頁那樣,用 React Native 寫移動端應用。這就是為什麼 React Native 自稱:Learn once,Write anywhere!
React Native原理概述
首先要明白的一點是,即使使用了 React Native,我們依然需要 UIKit 等框架,呼叫的是 Objective-C 程式碼。總之,JavaScript 只是輔助,它只是提供了配置資訊和邏輯的處理結果。React Native 與 Hybrid 完全沒有關係,它只不過是以 JavaScript 的形式告訴 Objective-C 該執行什麼程式碼
我們知道 C 系列的語言,經過編譯,連結等操作後,會得到一個二進位制格式的可執行文,所謂的執行程式,其實是執行這個二進位制程式。
而 JavaScript 是一種指令碼語言,它不會經過編譯、連結等操作,而是在執行時才動態的進行詞法、語法分析,生成抽象語法樹(AST)和位元組碼,然後由直譯器負責執行或者使用 JIT 將位元組碼轉化為機器碼再執行。蘋果提供了一個叫做 JavaScript Core 的框架,這是一個 JavaScript 引擎。整個流程由 JavaScript 引擎負責完成。
JavaScript 是一種單執行緒的語言,它不具備自執行的能力,因此總是被動呼叫。很多介紹 React Native 的文章都會提到 “JavaScript 執行緒” 的概念,實際上,它表示的是 Objective-C 建立了一個單獨的執行緒,這個執行緒只用於執行 JavaScript 程式碼,而且 JavaScript 程式碼只會在這個執行緒中執行。
JavaScript 呼叫OC
由於 JavaScript Core 是一個面向 Objective-C 的框架,在 Objective-C 這一端,我們對 JavaScript 上下文知根知底,可以很容易的獲取到物件,方法等各種資訊,當然也包括呼叫 JavaScript 函式。
真正複雜的問題在於,JavaScript 不知道 Objective-C 有哪些方法可以呼叫。
React Native 解決這個問題的方案是在 Objective-C 和 JavaScript 兩端都儲存了一份配置表,裡面標記了所有 Objective-C 暴露給 JavaScript 的模組和方法。這樣,無論是哪一方呼叫另一方的方法,實際上傳遞的資料只有 ModuleId、MethodId 和 Arguments 這三個元素,它們分別表示類、方法和方法引數,當 Objective-C 接收到這三個值後,就可以通過 runtime 唯一確定要呼叫的是哪個函式,然後呼叫這個函式。
對於 Objective-C 來說,執行完 JavaScript 程式碼再執行 Objective-C 回撥毫無難度,難點依然在於 JavaScript 程式碼呼叫 Objective-C 之後,如何在 Objective-C 的程式碼中,回撥執行 JavaScript 程式碼。
目前 React Native 的做法是:在 JavaScript 呼叫 Objective-C 程式碼時,註冊要回調的 Block,並且把 BlockId 作為引數傳送給 Objective-C,Objective-C 收到引數時會建立 Block,呼叫完 Objective-C 函式後就會執行這個剛剛建立的 Block。
Objective-C 會向 Block 中傳入引數和 BlockId,然後在 Block 內部呼叫 JavaScript 的方法,隨後 JavaScript 查詢到當時註冊的 Block 並執行。
下面以JavaScript呼叫object-c為例子中的圖解,方便大家理解。
React Native原始碼解析
初始化階段
每個專案都有一個入口,然後進行初始化操作,React Native 也不例外。一個不含 Objective-C 程式碼的專案留給我們的唯一線索就是位於 AppDelegate 檔案中的程式碼:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSURL *jsCodeLocation;
jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil];
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"TabBar"
initialProperties:nil
launchOptions:launchOptions];
rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [UIViewController new];
rootViewController.view = rootView;
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];
return YES;
}
使用者能看到的一切內容都來源於這個 RootView,所有的初始化工作也都在這個方法內完成。
在這個方法內部,在建立 RootView 之前,React Native 實際上先建立了一個 Bridge 物件。它是 Objective-C 與 JavaScript 互動的橋樑,後續的方法互動完全依賴於它,而整個初始化過程的最終目的其實也就是建立這個橋樑物件。
初始化方法的核心是 setUp 方法,而 setUp 方法的主要任務則是建立 BatchedBridge。
BatchedBridge 的作用是批量讀取 JavaScript 對 Objective-C 的方法呼叫,同時它內部持有一個 JavaScriptExecutor,顧名思義,這個物件用來執行 JavaScript 程式碼。
建立 BatchedBridge 的關鍵是 start 方法,它可以分為五個步驟:
- 讀取 JavaScript 原始碼
- 初始化模組資訊
- 初始化 JavaScript 程式碼的執行器,即 RCTJSCExecutor 物件
- 生成模組列表並寫入 JavaScript 端
- 執行 JavaScript 原始碼
我們逐個分析每一步完成的操作:
讀取JavaScript原始碼
這一部分的具體程式碼實現沒有太大的討論意義。我們只要明白,JavaScript 的程式碼是在 Objective-C 提供的環境下執行的,所以第一步就是把 JavaScript 載入進記憶體中,對於一個空的專案來說,所有的 JavaScript 程式碼大約佔用 1.5 Mb 的記憶體空間。
需要說明的是,在這一步中,JSX 程式碼已經被轉化成原生的 JavaScript 程式碼
初始化模組資訊
這一步在方法 initModulesWithDispatchGroup: 中實現,主要任務是找到所有需要暴露給 JavaScript 的類。每一個需要暴露給 JavaScript 的類(也成為 Module,以下不作區分)都會標記一個巨集:RCT_EXPORT_MODULE,這個巨集的具體實現並不複雜
#define RCT_EXPORT_MODULE(js_name) \
RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString *)moduleName { return @#js_name; } \
+ (void)load { RCTRegisterModule(self); }
這樣,這個類在 load 方法中就會呼叫 RCTRegisterModule 方法註冊自己:
void RCTRegisterModule(Class moduleClass)
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
RCTModuleClasses = [NSMutableArray new];
});
[RCTModuleClasses addObject:moduleClass];
}
因此,React Native 可以通過 RCTModuleClasses 拿到所有暴露給 JavaScript 的類。下一步操作是遍歷這個陣列,然後生成 RCTModuleData 物件:
for (Class moduleClass in RCTGetModuleClasses()) {
RCTModuleData *moduleData = [[RCTModuleData alloc]initWithModuleClass:moduleClass bridge:self];
[moduleClassesByID addObject:moduleClass];
[moduleDataByID addObject:moduleData];
}
可以想見,RCTModuleData 物件是模組配置表的主要組成部分。如果把模組配置表想象成一個數組,那麼每一個元素就是一個 RCTModuleData 物件。
這個物件儲存了 Module 的名字,常量等基本資訊,最重要的屬性是一個數組,儲存了所有需要暴露給 JavaScript 的方法。
暴露給 JavaScript 的方法需要用 RCT_EXPORT_METHOD 這個巨集來標記,它的實現原理比較複雜,有興趣的讀者可以自行閱讀。簡單來說,它為函式名加上了 rct_export 字首,再通過 runtime 獲取類的函式列表,找出其中帶有指定字首的方法並放入陣列中:
- (NSArray<id<RCTBridgeMethod>> *)methods{
unsigned int methodCount;
Method *methods = class_copyMethodList(object_getClass(_moduleClass), &methodCount); // 獲取方法列表
for (unsigned int i = 0; i < methodCount; i++) {
RCTModuleMethod *moduleMethod = /* 建立 method */
[_methods addObject:moduleMethod];
}
}
return _methods;
}
因此 Objective-C 管理模組配置表的邏輯是:Bridge 持有一個數組,陣列中儲存了所有的模組的 RCTModuleData 物件,RCTModuleData又儲存了類的方法、常亮、類名等資訊。只要給定 ModuleId 和 MethodId 就可以唯一確定要呼叫的方法。
初始化JavaScript執行器(RCTJSCExecutor)
通過檢視原始碼可以看到,初始化 JavaScript 執行器的時候,會呼叫
+ (instancetype)initializedExecutorWithContextProvider:(RCTJSContextProvider *)JSContextProvider
applicationScript:(NSData *)applicationScript
sourceURL:(NSURL *)sourceURL
JSContext:(JSContext **)JSContext
error:(NSError **)error;
返回的excuter物件是已經被同步執行的。
- (void)callFunctionOnModule:(NSString *)module
method:(NSString *)method
arguments:(NSArray *)args
jsValueCallback:(RCTJavaScriptValueCallback)onComplete;
執行對應的方法。
這裡需要關注nativeRequireModuleConfig和nativeFlushQueueImmediate這兩個block。
在這兩個block中會通過bridge呼叫oc的方法。
[self executeBlockOnJavaScriptQueue:^{
if (!self.valid) {
return;
}
JSContext *context = nil;
if (self->_jscWrapper) {
RCTAssert(self->_context != nil, @"If wrapper was pre-initialized, context should be too");
context = self->_context.context;
} else {
[self->_performanceLogger markStartForTag:RCTPLJSCWrapperOpenLibrary];
self->_jscWrapper = RCTJSCWrapperCreate(self->_useCustomJSCLibrary);
[self->_performanceLogger markStopForTag:RCTPLJSCWrapperOpenLibrary];
RCTAssert(self->_context == nil, @"Didn't expect to set up twice");
context = [self->_jscWrapper->JSContext new];
self->_context = [[RCTJavaScriptContext alloc] initWithJSContext:context onThread:self->_javaScriptThread];
[[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptContextCreatedNotification
object:context];
configureCacheOnContext(context, self->_jscWrapper);
installBasicSynchronousHooksOnContext(context);
}
__weak RCTJSCExecutor *weakSelf = self;
context[@"nativeRequireModuleConfig"] = ^NSString *(NSString *moduleName) {
RCTJSCExecutor *strongSelf = weakSelf;
if (!strongSelf.valid) {
return nil;
}
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"nativeRequireModuleConfig", nil);
NSArray *config = [strongSelf->_bridge configForModuleName:moduleName];
NSString *result = config ? RCTJSONStringify(config, NULL) : nil;
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"js_call,config", @{ @"moduleName": moduleName });
return result;
};
context[@"nativeFlushQueueImmediate"] = ^(NSArray<NSArray *> *calls){
RCTJSCExecutor *strongSelf = weakSelf;
if (!strongSelf.valid || !calls) {
return;
}
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"nativeFlushQueueImmediate", nil);
[strongSelf->_bridge handleBuffer:calls batchEnded:NO];
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"js_call", nil);
};
#if RCT_PROFILE
__weak RCTBridge *weakBridge = self->_bridge;
context[@"nativeTraceBeginAsyncFlow"] = ^(__unused uint64_t tag, __unused NSString *name, int64_t cookie) {
if (RCTProfileIsProfiling()) {
[weakBridge.flowIDMapLock lock];
int64_t newCookie = [_RCTProfileBeginFlowEvent() longLongValue];
CFDictionarySetValue(weakBridge.flowIDMap, (const void *)cookie, (const void *)newCookie);
[weakBridge.flowIDMapLock unlock];
}
};
context[@"nativeTraceEndAsyncFlow"] = ^(__unused uint64_t tag, __unused NSString *name, int64_t cookie) {
if (RCTProfileIsProfiling()) {
[weakBridge.flowIDMapLock lock];
int64_t newCookie = (int64_t)CFDictionaryGetValue(weakBridge.flowIDMap, (const void *)cookie);
_RCTProfileEndFlowEvent(@(newCookie));
CFDictionaryRemoveValue(weakBridge.flowIDMap, (const void *)cookie);
[weakBridge.flowIDMapLock unlock];
}
};
#endif
#if RCT_DEV
RCTInstallJSCProfiler(self->_bridge, context.JSGlobalContextRef);
// Inject handler used by HMR
context[@"nativeInjectHMRUpdate"] = ^(NSString *sourceCode, NSString *sourceCodeURL) {
RCTJSCExecutor *strongSelf = weakSelf;
if (!strongSelf.valid) {
return;
}
RCTJSCWrapper *jscWrapper = strongSelf->_jscWrapper;
JSStringRef execJSString = jscWrapper->JSStringCreateWithUTF8CString(sourceCode.UTF8String);
JSStringRef jsURL = jscWrapper->JSStringCreateWithUTF8CString(sourceCodeURL.UTF8String);
jscWrapper->JSEvaluateScript(strongSelf->_context.context.JSGlobalContextRef, execJSString, NULL, jsURL, 0, NULL);
jscWrapper->JSStringRelease(jsURL);
jscWrapper->JSStringRelease(execJSString);
};
#endif
}];
}
生成模組配置表並寫入JavaScript端
複習一下 nativeRequireModuleConfig 這個 Block,它可以接受 ModuleName 並且生成詳細的模組資訊,但在前文中我們沒有提到 JavaScript 是如何知道 Objective-C 要暴露哪些類的(目前只是 Objective-C 自己知道)。
這一步的操作就是為了讓 JavaScript 獲取所有模組的名字
- (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);
}
執行JavaScript程式碼
這一步也沒什麼技術難度可以,程式碼已經載入進了記憶體,該做的配置也已經完成,只要把 JavaScript 程式碼執行一遍即可。
執行程式碼時,第三步中所說的那些 Block 就會被執行,從而向 JavaScript 端寫入配置資訊。
至此,JavaScript 和 Objective-C 都具備了向對方互動的能力,準備工作也就全部完成了。
整體流程如下
方法呼叫
如前文所述,在 React Native 中,Objective-C 和 JavaScript 的互動都是通過傳遞 ModuleId、MethodId 和 Arguments 進行的。以下是分情況討論
呼叫JavaScript
也許你在其他文章中曾經多次聽說 JavaScript 程式碼總是在一個單獨的執行緒上面呼叫,它的實際含義是 Objective-C 會在單獨的執行緒上執行 JavaScript 程式碼
- (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block
{
if ([NSThread currentThread] != _javaScriptThread) {
[self performSelector:@selector(executeBlockOnJavaScriptQueue:)
onThread:_javaScriptThread withObject:block waitUntilDone:NO];
} else {
block();
}
}
呼叫JavaScript的核心程式碼如下
- (void)_executeJSCall:(NSString *)method
arguments:(NSArray *)arguments
callback:(RCTJavaScriptCallback)onComplete{
[self executeBlockOnJavaScriptQueue:^{
// 獲取 contextJSRef、methodJSRef、moduleJSRef
resultJSRef = JSObjectCallAsFunction(contextJSRef, (JSObjectRef)methodJSRef, (JSObjectRef)moduleJSRef, arguments.count, jsArgs, &errorJSRef);
objcValue = /*resultJSRef 轉換成 Objective-C 型別*/
onComplete(objcValue, nil);
}];
}
需要注意的是,這個函式名是我們要呼叫 JavaScript 的中轉函式名,比如 callFunctionReturnFlushedQueue。也就是說它的作用其實是處理引數,而非真正要呼叫的 JavaScript 函式。
在實際使用的時候,我們可以這樣發起對 JavaScript 的呼叫:
[_bridge.eventDispatcher sendAppEventWithName:@"greeted"
body:@{ @"name": @"nmae"}];
這裡的 Name 和 Body 引數分別表示要呼叫的 JavaScript 的函式名和引數。
JavaScript呼叫OC
在呼叫 Objective-C 程式碼時,如前文所述,JavaScript 會解析出方法的 ModuleId、MethodId 和 Arguments 並放入到 MessageQueue 中,等待 Objective-C 主動拿走,或者超時後主動傳送給 Objective-C。
Objective-C 負責處理呼叫的方法是 handleBuffer,它的引數是一個含有四個元素的陣列,每個元素也都是一個數組,分別存放了 ModuleId、MethodId、Params,第四個元素目測用處不大。
函式內部在每一次方呼叫中呼叫 _handleRequestNumber:moduleID:methodID:params 方法。,通過查詢模組配置表找出要呼叫的方法,並通過 runtime 動態的呼叫:
演示JavaScript呼叫OC方法
//.h檔案
#import <Foundation/Foundation.h>
#import "RCTBridge.h"
#import "RCTLog.h"
#import "EncryptUtil.h"
#import "RSA.h"
@interface CryptoExport : NSObject<RCTBridgeModule>
@end
//.m檔案
#import "CryptoExport.h"
@implementation CryptoExport
RCT_EXPORT_MODULE()//必須定義的巨集
RCT_EXPORT_METHOD(rsaEncryptValue:(NSString *)src withKey:(NSString *)rsaKey successCallback:(RCTResponseSenderBlock)successCallback){
NSString *rsaValue = [RSA encryptString:src publicKey:rsaKey];
successCallback(@[rsaValue]);
}
@end
每個oc的方法前必須加上RCT_EXPORT_METHOD巨集,用來註冊模組表。
在JavaScript中的調動如下
NativeModules.CryptoExport.rsaEncryptValue(value, rsaKey,function (rsaValue) {
console.log(rsaValue)
});
RN優缺點分析
經過一長篇的討論,其實 React Native 的優缺點已經不難分析了,這裡簡單總結一下:
優點
複用了 React 的思想,有利於前端開發者涉足移動端。
能夠利用 JavaScript 動態更新的特性,快速迭代。
相比於原生平臺,開發速度更快,相比於 Hybrid 框架,效能更好。
缺點
做不到 Write once, Run everywhere,也就是說開發者依然需要為 iOS 和 Android 平臺提供兩套不同的程式碼,比如參考官方文件可以發現不少元件和API都區分了 Android 和 iOS 版本。即使是共用元件,也會有平臺獨享的函式。
不能做到完全遮蔽 iOS 端或 Android 的細節,前端開發者必須對原生平臺有所瞭解。加重了學習成本。對於移動端開發者來說,完全不具備用 React Native 開發的能力。
由於 Objective-C 與 JavaScript 之間切換存在固定的時間開銷,所以效能必定不及原生。比如目前的官方版本無法做到 UItableview(ListView) 的檢視重用,因為滑動過程中,檢視重用需要在非同步執行緒中執行,速度太慢。這也就導致隨著 Cell 數量的增加,佔用的記憶體也線性增加。
綜上,我對 React Native 的定位是:
利用指令碼語言進行原生平臺開發的一次成功嘗試,降低了前端開發者入門移動端的門檻,一定業務場景下具有獨特的優勢,幾乎不可能取代原生平臺開發。