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實現動態配置
- 通過 HTTP 請求獲取 JSON 格式的配置文件。
- 配置文件中標記了每一個元素的屬性,比如位置,顏色,圖片 URL 等。
- 解析完 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方法并且注冊回調函數。
模塊配置表
取出所有可被調用的模塊,每個可被調用模塊類都實現了
RCTBridgeModule
接口,可以通過runtime接口objc_getClassList
或objc_copyClassList
取出項目里所有類,然后逐個判斷是否實現了RCTBridgeModule
接口,就可以找到所有模塊類。取出模塊中所有可被調用的方法,模塊方法里有句代碼:
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 }
- 在讀取完所有可被調用模塊和可被調用方法后,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
,而RCTBridge
的setUp
方法主要是為了初始化BatchedBridge
,BatchedBridge
主要是用來批量讀取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對象
方法調用流程
JS調用模塊方法。
在JS端有一個JS Bridge專門負責處理JS與OC交互部分,同理在OC端也有一個OC Bridge,JS Bridge將調用的模塊方法記錄并轉化成相應的ModuleName,MethodName和args。
然后在MessageQueue中將調用模塊方法時的回調函數注冊一個CallBack ID,將ID和回調函數存儲在一個成員變量的列表中,并將第2步中的ModuleName和MethodName根據模塊配置表信息轉成對應的ID。
JS將moduleID,methodID和args以及CallBackID傳遞給OC Bridge,這個過程實質上是基于事件處理的,因為在移動平臺上如果有代碼的執行必定是某個事件觸發的,比如滑動屏幕等等,事件觸發后OC主動調用JS代碼,JS處理業務邏輯過程并將需要調用OC的部分存儲到MessageQueue中,再去通知OC執行。
OC接收到消息,通過模塊配置表拿到對應的模塊和方法,在OC Bridge端,對每一個可被調用的模塊方法都會有一個RCTModuleMethod對象與之對應。
RCTModuleMethod對傳進來的參數進行處理,包括類型轉化以及創建一個Block對象以供回調,會將JS端傳過來的CallBackID以及回調的值存儲進Block對象中
執行OC端代碼
執行第6步中生成的Block方法
block里帶著CallbackID和block傳過來的參數去調JS里MessageQueue的方法invokeCallbackAndReturnFlushedQueue。
MessageQueue根據CallBackId找到對應的回調函數
根據OC傳來的回調值,執行回調函數
整個流程就是這樣,簡單概括下,差不多就是:JS函數調用轉ModuleID/MethodID -> callback轉CallbackID -> OC根據ID拿到方法 -> 處理參數 -> 調用OC方法 -> 回調CallbackID -> JS通過CallbackID拿到callback執行
4. React Native優缺點
優點
- 能夠利用 JavaScript 動態更新的特性,快速迭代。
- 相比于原生平臺,開發速度更快,相比于 Hybrid 框架,性能更好。
缺點
- 不能實現真正意義上的跨平臺,開發者仍然需要為iOS和Android提供兩套實現機制
- 不能直接取代Native Code開發,很大程度上還加重了開發者的學習成本
- 語言互轉存在著固定的時間和空間開銷