淺談React Native與實現機制

1. React Native簡單介紹

目前App開發的主流方式有三種: Native開發,Hybird開發以及Web開發

原生Native開發

主要采用Object-C/Swift方式進行原生開發。運行效率高,流暢,用戶體驗好,可以做各種復雜的動畫效果。平臺獨立性,代碼無法在其他平臺上運行,無法做到跨平臺。更新審核周期比較長,不利于App問題的快速修復

Hybird開發

以原生開發為主。
更新頻繁,活動頁面,運營頁面等采用H5方式接入。定義好原生功能與H5之間的協議,攔截特定的URL Schema進行原生功能的調用,App調用H5提供的js方法,給H5傳值和通知H5

Web開發

是Web App,以Web為主,通過js或者插件方式調用原生功能,如撥打電話,位置服務等。
一套Web代碼可以分別在各個平臺上運行。受限制與UIWebView,app的性能和體驗都無法與純原生app相提并論。比較有代表性的:采用cordova和ionic進行web app開發,通過開發原生插件功能供Web端調用

React Native的出現

不同的開發方式都在解決如下的幾個問題

  • 使得APP的體驗效果和原生應用一樣好
  • 跨平臺,提高項目代碼的重用性
  • 應對廣告或者活動更新,能夠進行熱替換而不用進行APP新發布

因此Facebook在2015年發布了React Native框架,旨在幫助前端程序員解決如上的棘手問題,在發布當初,相比于其他Hybird框架,React Native有如下的特點

  • 基于組件開發,提供代碼的復用率。
  • 各個平臺功能代碼可以進行復用,官方數據表明:iOS和android功能代碼可以達到90%以上的復用。
  • 不用Webview,徹底擺脫了Webview的限制:交互和性能問題。
  • 相對其它Hybrid 方案,React Native性能更好,用戶體驗更接近原生。
  • 減少編譯時間,提高開發效率。
  • 可以采用熱更新方式進行app功能升級和問題修復,提高app的迭代率和開發效率
React Native實例
  • 在JS語言中嵌入了HTML和CSS的元素,這種被擴展了的JavaScript語言稱為jsx
  • React Native框架中,JavaScript內存中維護了一個Virtual DOM,JSX內容在Virtual DOM中被轉化翻譯成真實的DOM樹,Virtual DOM與真實顯示的DOM保持一一對應的關系
  • 當界面發生變化時,得益于高效的 DOM Diff 算法,我們能夠知道 Virtual DOM 的變化,從而高效的改動 DOM,避免了重新繪制 DOM
JSX實例
var MyComponent = React.createClass({
  handleClick: function() {
    this.refs.myTextInput.focus();
  },
  render: function() {
    return (
      <div>
        <input type="text" ref="myTextInput" />
        <input type="button" value="Focus the text input" onClick={this.handleClick} />
      </div>
    );
  }
});

ReactDOM.render(
  <MyComponent />,
  document.getElementById('example')
);

2. 動態配置

在當下移動端App越來越人性化的趨勢下,App的更新迭代速度很快,但是限制于App Store和安卓市場的應用版本更新限制,如果每次界面上有部分需要更新,例如廣告更換,頁面布局調整等,都需要通過發布一個新的版本來實現,著實對應用商和用戶來說都是不合理的,因此如果有一種方式可以動態的配置移動端界面便是極好的。

很多時候,我們都是利用 JSON 文件實現動態配置的效果,它的核心流程如下

JSON實現動態配置
  1. 通過 HTTP 請求獲取 JSON 格式的配置文件。
  2. 配置文件中標記了每一個元素的屬性,比如位置,顏色,圖片 URL 等。
  3. 解析完 JSON 后,我們調用 Objective-C 的代碼,完成 UI 控件的渲染。

通過這種方法,我們實現了在后臺配置 app 的展示樣式。從本質上來說,移動端和服務端約定了一套協議,但是協議內容嚴重依賴于應用內要展示的內容,不利于拓展。也就是說,如果業務要求頻繁的增加或修改頁面,這套協議很難應付。

然而這種通過JSON通信,配置一些可選項的方式在很多情況下都不能夠滿足現在快速迭代開發的App,如果想要改變一些業務邏輯或者進行一些復雜度比較高的修改操作,則客戶端只讀取JSON配置項是做不到的,無法調用處理業務邏輯的方法等,不具備調試功能。

各種移動平臺支持JavaScript

然而,基于現在移動設備都支持JavaScript代碼的執行這一條件(例如iOS上內置了JavaScript Core來執行JavaScript代碼),React Native的推出發揮了這一優點,通過JavaScript代碼,不僅僅只是傳遞簡單的配置信息,更可以進行業務邏輯的處理。

Learn once, write everywhere

和其他Hybird框架所宣傳的"Write once, run everywhere"不同,React Native其實不能真正意義上稱為"跨平臺"框架,因為它的本質是使用了各個移動平臺都支持JavaScript語言,React Native幫我們做好了將JavaScript代碼轉化成Object-C或者Java語言,并且幫我們處理好了各種回調問題,因此表面上我們只需要編寫JavaScript語言,即可在不同的平臺上展現應用,這也是React Native的開發初衷: 分別開發安卓和iOS而不用寫一行原生代碼

3. 通信機制

iOS -- JavaScript / Objective-C

我們雖然使用的是React Native框架,但還是需要依賴UIKit等框架,從而調用Objective-C代碼以在iOS平臺上執行,JavaScript其實只是為我們提供了編寫業務邏輯和前端界面的輔助工具,React Native在iOS上能夠運行的實質是利用JS代碼調用OC代碼執行,我們需要關注的重點就是JS與OC之間的通信機制,包括JS是如何去調用OC代碼,又如何實現回調功能,這是React Native的核心功能之一。

我們都知道,JS作為一種腳本語言,是不需要像C語言那樣被編譯鏈接然后執行,在執行腳本語言時,會在運行時動態地進行詞法和語法的分析,然后生成一課抽象語法樹和對應的字節碼,由JS解釋器等將字節碼轉化成對應的機器碼,而整個流程都是由JS引擎來加以完成。

JavaScript Core

在iOS平臺下,React Native利用了iOS提供的JavaScript Core作為JS解析器,然而RN并沒有完全使用JS Core中提供的JS-OC互調的特性,而是自己實現了一套通用的方案,以便兼容不同的版本

OC調用JS

OC向JS傳信息有現成接口,stringByEvaluatingJavaScriptFromString方法可以直接在當前context上執行一段JS腳本,并且可以獲取執行后的返回值,這個返回值就相當于JS向OC傳遞信息。

JSContext *context = [[JSContext alloc] init];
JSValue *jsVal = [context evaluateScript:@"21+7"];
int iVal = [jsVal toInt32];

在上述例子中,JSContext 代表了當前JS的執行環境,evaluateScript 方法則會去執行后面跟著的js腳本內容,返回值會存放在 JSValue 中,從而完成OC調用JS并獲取JS返回信息。

JS調用OC

React Native基于上述OC調用JS的方法,經過一些封裝在OC里面定義了一個模塊方法,JS可以直接調用這個模塊方法并且可以注冊回調函數。

//Objective-C
@implement RCTSQLManager
- (void)query:(NSString *)queryData successCallback:(RCTResponseSenderBlOCk)responseSender
{
     RCT_EXPORT();
     NSString *ret = @"ret"
     responseSender(ret);
}
@end
//JavaScript
RCTSQLManager.query("SELECT * FROM table", function(result) {
     //result == "ret";
});

如上圖所示,在OC內部定義了一個模塊RCTSQLManager,并且在模塊內部定義了方法 -query: successCallback;我們在JS中可以直接調用RCTSQLManager的query方法并且注冊回調函數。

模塊配置表
  1. 取出所有可被調用的模塊,每個可被調用模塊類都實現了RCTBridgeModule接口,可以通過runtime接口objc_getClassListobjc_copyClassList取出項目里所有類,然后逐個判斷是否實現了RCTBridgeModule接口,就可以找到所有模塊類。

  2. 取出模塊中所有可被調用的方法,模塊方法里有句代碼:RCT_EXPORT(),模塊里的方法加上這個宏就可以實現暴露給JS,無需其他規則,這個宏的作用是用編譯屬性__attribute__給二進制文件新建一個section,屬于__DATA數據段,名字為RCTExport,并在這個段里加入當前方法名。編譯器在編譯時會找到__attribute__進行處理,為生成的可執行文件加入相應的內容。

#define RCT_EXPORT(JS_name) __attribute__((used, section("__DATA,RCTExport" \
))) static const char *__rct_export_entry__[] = { __func__, #JS_name }
  1. 在讀取完所有可被調用模塊和可被調用方法后,OC告訴JS有哪些模塊,哪些方法是可以被JS調用的,在這里的實現機制是OC生成一份模塊配置表然后OC端和JS端分別持有這一份配置表,表的內容大致如下,可以看到每個模塊都有對應的編號,每個方法也有對應的編號,在JS調用OC時,通過傳遞對應的ModuleID和MethodID即可匹配OC模塊及方法。
{
    "remoteModuleConfig": {
        "RCTSQLManager": {
            "methods": {
                "query": {
                    "type": "remote",
                    "methodID": 0
                }
            },
            "moduleID": 4
        },
        ...
     },
}
React Native初始化分析

每個應用有一個唯一的 rootWindow,每一個UIWindow有一個唯一的rootView,在React Native 中,對應的就是RCTRootView,它持有一個RCTBridge,RCTBridge的職能是通訊橋,負責各個模塊之間和js之間的通訊, RCTBatchedBridge繼承RCTBridge,它有一個唯一的但是可變的currentBridge,實際上RCTBridge是唯一的, RCTBatchedBridge是唯一的,通訊時,實際上RCTBatchedBridge承擔一個適配的職責。

因此,實際上在創建一個RootView之前,React Native都會預先創建好一個RCTBridge,而RCTBridgesetUp方法主要是為了初始化BatchedBridgeBatchedBridge主要是用來批量讀取JavaScript對Objective-C的調用,BatchedBridge內部還依賴一個JSCExecutor,用于執行JS代碼,下面我們簡單地了解一下BatchedBridge初始化過程中都做了哪些工作。

1. 讀取 JavaScript 源碼

這個過程將應用的js代碼加載到內存,供接下來在OC中調用執行JS代碼

2. 初始化模塊信息

這一步主要是發現所有需要暴露給JavaScript的模塊及模塊中需要暴露的方法,每一個需要暴露的模塊都會被加上 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方法,將自身注冊到RCTModuleClasses

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

  [RCTModuleClasses addObject:moduleClass];
}

因此我們可以從RCTModuleClasses中獲取出所有模塊信息,每一條模塊信息都被存儲與RCTModuleData對象中

for (Class moduleClass in RCTGetModuleClasses()) {
    RCTModuleData *moduleData = [[RCTModuleData alloc]initWithModuleClass:moduleClass                                                                      bridge:self];
    [moduleClassesByID addObject:moduleClass];
    [moduleDataByID addObject:moduleData];
}
3. 初始化 JavaScript 代碼的執行器,即 RCTJSCExecutor 對象

在這一步操作中,通過addSynchronousHookWithName這一方法向JavaScript的添加了若干的Block對象作為全局變量,以供第5步過程中在執行JavaScript源碼時處理這些Block對象

4. 生成模塊列表并寫入 JavaScript 端

這一步操作是將OC端生成的模塊列表信息注入到JavaScript端中,以便雙方都持有一份模塊列表信息。

- (NSString *)moduleConfig{
    NSMutableArray<NSArray *> *config = [NSMutableArray new];
    for (RCTModuleData *moduleData in _moduleDataByID) {
      [config addObject:@[moduleData.name]];
    }
}

可以看到,Objective-C將config信息存儲到了JavaScript的全局變量中,名稱為__fbBatchedBridgeConfig

5. 執行 JavaScript 源碼

在所有的初始化都完成后,只需要運行js代碼即可,運行過程中也會執行第3步過程添加進全局變量的Block對象

方法調用流程
  1. JS調用模塊方法。

  2. 在JS端有一個JS Bridge專門負責處理JS與OC交互部分,同理在OC端也有一個OC Bridge,JS Bridge將調用的模塊方法記錄并轉化成相應的ModuleName,MethodName和args。

  3. 然后在MessageQueue中將調用模塊方法時的回調函數注冊一個CallBack ID,將ID和回調函數存儲在一個成員變量的列表中,并將第2步中的ModuleName和MethodName根據模塊配置表信息轉成對應的ID。

  4. JS將moduleID,methodID和args以及CallBackID傳遞給OC Bridge,這個過程實質上是基于事件處理的,因為在移動平臺上如果有代碼的執行必定是某個事件觸發的,比如滑動屏幕等等,事件觸發后OC主動調用JS代碼,JS處理業務邏輯過程并將需要調用OC的部分存儲到MessageQueue中,再去通知OC執行。

  5. OC接收到消息,通過模塊配置表拿到對應的模塊和方法,在OC Bridge端,對每一個可被調用的模塊方法都會有一個RCTModuleMethod對象與之對應。

  6. RCTModuleMethod對傳進來的參數進行處理,包括類型轉化以及創建一個Block對象以供回調,會將JS端傳過來的CallBackID以及回調的值存儲進Block對象中

  7. 執行OC端代碼

  8. 執行第6步中生成的Block方法

  9. block里帶著CallbackID和block傳過來的參數去調JS里MessageQueue的方法invokeCallbackAndReturnFlushedQueue。

  10. MessageQueue根據CallBackId找到對應的回調函數

  11. 根據OC傳來的回調值,執行回調函數

整個流程就是這樣,簡單概括下,差不多就是:JS函數調用轉ModuleID/MethodID -> callback轉CallbackID -> OC根據ID拿到方法 -> 處理參數 -> 調用OC方法 -> 回調CallbackID -> JS通過CallbackID拿到callback執行

4. React Native優缺點

優點
  • 能夠利用 JavaScript 動態更新的特性,快速迭代。
  • 相比于原生平臺,開發速度更快,相比于 Hybrid 框架,性能更好。
缺點
  • 不能實現真正意義上的跨平臺,開發者仍然需要為iOS和Android提供兩套實現機制
  • 不能直接取代Native Code開發,很大程度上還加重了開發者的學習成本
  • 語言互轉存在著固定的時間和空間開銷

參考資料

  1. React Native從入門到原理
  2. React Native通信原理
  3. React Native通信機制詳解
  4. React Native開發簡介
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容