1. React Native簡(jiǎn)單介紹
目前App開(kāi)發(fā)的主流方式有三種: Native開(kāi)發(fā)祖驱,Hybird開(kāi)發(fā)以及Web開(kāi)發(fā)
原生Native開(kāi)發(fā)
主要采用Object-C/Swift方式進(jìn)行原生開(kāi)發(fā)印衔。運(yùn)行效率高,流暢,用戶(hù)體驗(yàn)好,可以做各種復(fù)雜的動(dòng)畫(huà)效果。平臺(tái)獨(dú)立性,代碼無(wú)法在其他平臺(tái)上運(yùn)行,無(wú)法做到跨平臺(tái)利凑。更新審核周期比較長(zhǎng),不利于App問(wèn)題的快速修復(fù)
Hybird開(kāi)發(fā)
以原生開(kāi)發(fā)為主。
更新頻繁,活動(dòng)頁(yè)面,運(yùn)營(yíng)頁(yè)面等采用H5方式接入嫌术。定義好原生功能與H5之間的協(xié)議,攔截特定的URL Schema進(jìn)行原生功能的調(diào)用,App調(diào)用H5提供的js方法,給H5傳值和通知H5
Web開(kāi)發(fā)
是Web App,以Web為主,通過(guò)js或者插件方式調(diào)用原生功能,如撥打電話(huà),位置服務(wù)等哀澈。
一套Web代碼可以分別在各個(gè)平臺(tái)上運(yùn)行。受限制與UIWebView,app的性能和體驗(yàn)都無(wú)法與純?cè)鷄pp相提并論度气。比較有代表性的:采用cordova和ionic進(jìn)行web app開(kāi)發(fā),通過(guò)開(kāi)發(fā)原生插件功能供Web端調(diào)用
React Native的出現(xiàn)
不同的開(kāi)發(fā)方式都在解決如下的幾個(gè)問(wèn)題
- 使得APP的體驗(yàn)效果和原生應(yīng)用一樣好
- 跨平臺(tái)割按,提高項(xiàng)目代碼的重用性
- 應(yīng)對(duì)廣告或者活動(dòng)更新,能夠進(jìn)行熱替換而不用進(jìn)行APP新發(fā)布
因此Facebook在2015年發(fā)布了React Native框架磷籍,旨在幫助前端程序員解決如上的棘手問(wèn)題适荣,在發(fā)布當(dāng)初,相比于其他Hybird框架院领,React Native有如下的特點(diǎn)
- 基于組件開(kāi)發(fā),提供代碼的復(fù)用率弛矛。
- 各個(gè)平臺(tái)功能代碼可以進(jìn)行復(fù)用,官方數(shù)據(jù)表明:iOS和android功能代碼可以達(dá)到90%以上的復(fù)用。
- 不用Webview,徹底擺脫了Webview的限制:交互和性能問(wèn)題栅盲。
- 相對(duì)其它Hybrid 方案,React Native性能更好,用戶(hù)體驗(yàn)更接近原生汪诉。
- 減少編譯時(shí)間,提高開(kāi)發(fā)效率。
- 可以采用熱更新方式進(jìn)行app功能升級(jí)和問(wèn)題修復(fù),提高app的迭代率和開(kāi)發(fā)效率
React Native實(shí)例
- 在JS語(yǔ)言中嵌入了HTML和CSS的元素谈秫,這種被擴(kuò)展了的JavaScript語(yǔ)言稱(chēng)為jsx
- React Native框架中扒寄,JavaScript內(nèi)存中維護(hù)了一個(gè)Virtual DOM,JSX內(nèi)容在Virtual DOM中被轉(zhuǎn)化翻譯成真實(shí)的DOM樹(shù)拟烫,Virtual DOM與真實(shí)顯示的DOM保持一一對(duì)應(yīng)的關(guān)系
- 當(dāng)界面發(fā)生變化時(shí)该编,得益于高效的 DOM Diff 算法,我們能夠知道 Virtual DOM 的變化硕淑,從而高效的改動(dòng) DOM课竣,避免了重新繪制 DOM
JSX實(shí)例
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. 動(dòng)態(tài)配置
在當(dāng)下移動(dòng)端App越來(lái)越人性化的趨勢(shì)下嘉赎,App的更新迭代速度很快,但是限制于App Store和安卓市場(chǎng)的應(yīng)用版本更新限制于樟,如果每次界面上有部分需要更新公条,例如廣告更換,頁(yè)面布局調(diào)整等迂曲,都需要通過(guò)發(fā)布一個(gè)新的版本來(lái)實(shí)現(xiàn)靶橱,著實(shí)對(duì)應(yīng)用商和用戶(hù)來(lái)說(shuō)都是不合理的,因此如果有一種方式可以動(dòng)態(tài)的配置移動(dòng)端界面便是極好的路捧。
很多時(shí)候关霸,我們都是利用 JSON 文件實(shí)現(xiàn)動(dòng)態(tài)配置的效果,它的核心流程如下
JSON實(shí)現(xiàn)動(dòng)態(tài)配置
- 通過(guò) HTTP 請(qǐng)求獲取 JSON 格式的配置文件杰扫。
- 配置文件中標(biāo)記了每一個(gè)元素的屬性队寇,比如位置,顏色章姓,圖片 URL 等佳遣。
- 解析完 JSON 后,我們調(diào)用 Objective-C 的代碼啤覆,完成 UI 控件的渲染苍日。
通過(guò)這種方法惭聂,我們實(shí)現(xiàn)了在后臺(tái)配置 app 的展示樣式窗声。從本質(zhì)上來(lái)說(shuō),移動(dòng)端和服務(wù)端約定了一套協(xié)議辜纲,但是協(xié)議內(nèi)容嚴(yán)重依賴(lài)于應(yīng)用內(nèi)要展示的內(nèi)容笨觅,不利于拓展。也就是說(shuō)耕腾,如果業(yè)務(wù)要求頻繁的增加或修改頁(yè)面见剩,這套協(xié)議很難應(yīng)付。
然而這種通過(guò)JSON通信扫俺,配置一些可選項(xiàng)的方式在很多情況下都不能夠滿(mǎn)足現(xiàn)在快速迭代開(kāi)發(fā)的App苍苞,如果想要改變一些業(yè)務(wù)邏輯或者進(jìn)行一些復(fù)雜度比較高的修改操作,則客戶(hù)端只讀取JSON配置項(xiàng)是做不到的狼纬,無(wú)法調(diào)用處理業(yè)務(wù)邏輯的方法等羹呵,不具備調(diào)試功能。
各種移動(dòng)平臺(tái)支持JavaScript
然而疗琉,基于現(xiàn)在移動(dòng)設(shè)備都支持JavaScript代碼的執(zhí)行這一條件(例如iOS上內(nèi)置了JavaScript Core來(lái)執(zhí)行JavaScript代碼)冈欢,React Native的推出發(fā)揮了這一優(yōu)點(diǎn),通過(guò)JavaScript代碼盈简,不僅僅只是傳遞簡(jiǎn)單的配置信息凑耻,更可以進(jìn)行業(yè)務(wù)邏輯的處理太示。
Learn once, write everywhere
和其他Hybird框架所宣傳的"Write once, run everywhere"不同,React Native其實(shí)不能真正意義上稱(chēng)為"跨平臺(tái)"框架香浩,因?yàn)樗谋举|(zhì)是使用了各個(gè)移動(dòng)平臺(tái)都支持JavaScript語(yǔ)言类缤,React Native幫我們做好了將JavaScript代碼轉(zhuǎn)化成Object-C或者Java語(yǔ)言,并且?guī)臀覀兲幚砗昧烁鞣N回調(diào)問(wèn)題邻吭,因此表面上我們只需要編寫(xiě)JavaScript語(yǔ)言呀非,即可在不同的平臺(tái)上展現(xiàn)應(yīng)用,這也是React Native的開(kāi)發(fā)初衷: 分別開(kāi)發(fā)安卓和iOS而不用寫(xiě)一行原生代碼
3. 通信機(jī)制
iOS -- JavaScript / Objective-C
我們雖然使用的是React Native框架镜盯,但還是需要依賴(lài)UIKit等框架岸裙,從而調(diào)用Objective-C代碼以在iOS平臺(tái)上執(zhí)行,JavaScript其實(shí)只是為我們提供了編寫(xiě)業(yè)務(wù)邏輯和前端界面的輔助工具速缆,React Native在iOS上能夠運(yùn)行的實(shí)質(zhì)是利用JS代碼調(diào)用OC代碼執(zhí)行降允,我們需要關(guān)注的重點(diǎn)就是JS與OC之間的通信機(jī)制,包括JS是如何去調(diào)用OC代碼艺糜,又如何實(shí)現(xiàn)回調(diào)功能剧董,這是React Native的核心功能之一。
我們都知道破停,JS作為一種腳本語(yǔ)言翅楼,是不需要像C語(yǔ)言那樣被編譯鏈接然后執(zhí)行,在執(zhí)行腳本語(yǔ)言時(shí)真慢,會(huì)在運(yùn)行時(shí)動(dòng)態(tài)地進(jìn)行詞法和語(yǔ)法的分析毅臊,然后生成一課抽象語(yǔ)法樹(shù)和對(duì)應(yīng)的字節(jié)碼,由JS解釋器等將字節(jié)碼轉(zhuǎn)化成對(duì)應(yīng)的機(jī)器碼黑界,而整個(gè)流程都是由JS引擎來(lái)加以完成管嬉。
JavaScript Core
在iOS平臺(tái)下,React Native利用了iOS提供的JavaScript Core作為JS解析器朗鸠,然而RN并沒(méi)有完全使用JS Core中提供的JS-OC互調(diào)的特性蚯撩,而是自己實(shí)現(xiàn)了一套通用的方案,以便兼容不同的版本
OC調(diào)用JS
OC向JS傳信息有現(xiàn)成接口烛占,stringByEvaluatingJavaScriptFromString方法可以直接在當(dāng)前context上執(zhí)行一段JS腳本胎挎,并且可以獲取執(zhí)行后的返回值,這個(gè)返回值就相當(dāng)于JS向OC傳遞信息忆家。
JSContext *context = [[JSContext alloc] init];
JSValue *jsVal = [context evaluateScript:@"21+7"];
int iVal = [jsVal toInt32];
在上述例子中犹菇,JSContext
代表了當(dāng)前JS的執(zhí)行環(huán)境,evaluateScript
方法則會(huì)去執(zhí)行后面跟著的js腳本內(nèi)容弦赖,返回值會(huì)存放在 JSValue
中项栏,從而完成OC調(diào)用JS并獲取JS返回信息。
JS調(diào)用OC
React Native基于上述OC調(diào)用JS的方法蹬竖,經(jīng)過(guò)一些封裝在OC里面定義了一個(gè)模塊方法沼沈,JS可以直接調(diào)用這個(gè)模塊方法并且可以注冊(cè)回調(diào)函數(shù)流酬。
//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內(nèi)部定義了一個(gè)模塊RCTSQLManager列另,并且在模塊內(nèi)部定義了方法 -query: successCallback
芽腾;我們?cè)贘S中可以直接調(diào)用RCTSQLManager的query方法并且注冊(cè)回調(diào)函數(shù)。
模塊配置表
取出所有可被調(diào)用的模塊页衙,每個(gè)可被調(diào)用模塊類(lèi)都實(shí)現(xiàn)了
RCTBridgeModule
接口摊滔,可以通過(guò)runtime接口objc_getClassList
或objc_copyClassList
取出項(xiàng)目里所有類(lèi),然后逐個(gè)判斷是否實(shí)現(xiàn)了RCTBridgeModule
接口店乐,就可以找到所有模塊類(lèi)艰躺。取出模塊中所有可被調(diào)用的方法,模塊方法里有句代碼:
RCT_EXPORT()
眨八,模塊里的方法加上這個(gè)宏就可以實(shí)現(xiàn)暴露給JS腺兴,無(wú)需其他規(guī)則,這個(gè)宏的作用是用編譯屬性__attribute__
給二進(jìn)制文件新建一個(gè)section廉侧,屬于__DATA
數(shù)據(jù)段页响,名字為RCTExport
,并在這個(gè)段里加入當(dāng)前方法名段誊。編譯器在編譯時(shí)會(huì)找到__attribute__
進(jìn)行處理闰蚕,為生成的可執(zhí)行文件加入相應(yīng)的內(nèi)容。
#define RCT_EXPORT(JS_name) __attribute__((used, section("__DATA,RCTExport" \
))) static const char *__rct_export_entry__[] = { __func__, #JS_name }
- 在讀取完所有可被調(diào)用模塊和可被調(diào)用方法后连舍,OC告訴JS有哪些模塊没陡,哪些方法是可以被JS調(diào)用的,在這里的實(shí)現(xiàn)機(jī)制是OC生成一份模塊配置表然后OC端和JS端分別持有這一份配置表烟瞧,表的內(nèi)容大致如下诗鸭,可以看到每個(gè)模塊都有對(duì)應(yīng)的編號(hào)染簇,每個(gè)方法也有對(duì)應(yīng)的編號(hào)参滴,在JS調(diào)用OC時(shí),通過(guò)傳遞對(duì)應(yīng)的ModuleID和MethodID即可匹配OC模塊及方法锻弓。
{
"remoteModuleConfig": {
"RCTSQLManager": {
"methods": {
"query": {
"type": "remote",
"methodID": 0
}
},
"moduleID": 4
},
...
},
}
React Native初始化分析
每個(gè)應(yīng)用有一個(gè)唯一的 rootWindow
,每一個(gè)UIWindow
有一個(gè)唯一的rootView
,在React Native 中,對(duì)應(yīng)的就是RCTRootView
,它持有一個(gè)RCTBridge
,RCTBridge
的職能是通訊橋,負(fù)責(zé)各個(gè)模塊之間和js之間的通訊, RCTBatchedBridge
繼承RCTBridge
,它有一個(gè)唯一的但是可變的currentBridge
,實(shí)際上RCTBridge
是唯一的, RCTBatchedBridge
是唯一的,通訊時(shí),實(shí)際上RCTBatchedBridge
承擔(dān)一個(gè)適配的職責(zé)砾赔。
因此,實(shí)際上在創(chuàng)建一個(gè)RootView
之前青灼,React Native都會(huì)預(yù)先創(chuàng)建好一個(gè)RCTBridge
暴心,而RCTBridge
的setUp
方法主要是為了初始化BatchedBridge
,BatchedBridge
主要是用來(lái)批量讀取JavaScript對(duì)Objective-C的調(diào)用杂拨,BatchedBridge
內(nèi)部還依賴(lài)一個(gè)JSCExecutor
专普,用于執(zhí)行JS代碼,下面我們簡(jiǎn)單地了解一下BatchedBridge
初始化過(guò)程中都做了哪些工作弹沽。
1. 讀取 JavaScript 源碼
這個(gè)過(guò)程將應(yīng)用的js代碼加載到內(nèi)存檀夹,供接下來(lái)在OC中調(diào)用執(zhí)行JS代碼
2. 初始化模塊信息
這一步主要是發(fā)現(xiàn)所有需要暴露給JavaScript的模塊及模塊中需要暴露的方法筋粗,每一個(gè)需要暴露的模塊都會(huì)被加上 RCT_EXPORT_MODULE
的宏,宏的內(nèi)容如下:
#define RCT_EXPORT_MODULE(js_name) \
RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString *)moduleName { return @#js_name; } \
+ (void)load { RCTRegisterModule(self); }
這個(gè)類(lèi)在執(zhí)行l(wèi)oad方法時(shí)會(huì)調(diào)用RCTRegisterModule
方法炸渡,將自身注冊(cè)到RCTModuleClasses
中
void RCTRegisterModule(Class moduleClass)
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
RCTModuleClasses = [NSMutableArray new];
});
[RCTModuleClasses addObject:moduleClass];
}
因此我們可以從RCTModuleClasses
中獲取出所有模塊信息娜亿,每一條模塊信息都被存儲(chǔ)與RCTModuleData
對(duì)象中
for (Class moduleClass in RCTGetModuleClasses()) {
RCTModuleData *moduleData = [[RCTModuleData alloc]initWithModuleClass:moduleClass bridge:self];
[moduleClassesByID addObject:moduleClass];
[moduleDataByID addObject:moduleData];
}
3. 初始化 JavaScript 代碼的執(zhí)行器,即 RCTJSCExecutor 對(duì)象
在這一步操作中蚌堵,通過(guò)addSynchronousHookWithName
這一方法向JavaScript的添加了若干的Block對(duì)象作為全局變量买决,以供第5步過(guò)程中在執(zhí)行JavaScript源碼時(shí)處理這些Block對(duì)象
4. 生成模塊列表并寫(xiě)入 JavaScript 端
這一步操作是將OC端生成的模塊列表信息注入到JavaScript端中,以便雙方都持有一份模塊列表信息吼畏。
- (NSString *)moduleConfig{
NSMutableArray<NSArray *> *config = [NSMutableArray new];
for (RCTModuleData *moduleData in _moduleDataByID) {
[config addObject:@[moduleData.name]];
}
}
可以看到督赤,Objective-C將config信息存儲(chǔ)到了JavaScript的全局變量中,名稱(chēng)為__fbBatchedBridgeConfig
5. 執(zhí)行 JavaScript 源碼
在所有的初始化都完成后泻蚊,只需要運(yùn)行js代碼即可够挂,運(yùn)行過(guò)程中也會(huì)執(zhí)行第3步過(guò)程添加進(jìn)全局變量的Block對(duì)象
方法調(diào)用流程
JS調(diào)用模塊方法。
在JS端有一個(gè)JS Bridge專(zhuān)門(mén)負(fù)責(zé)處理JS與OC交互部分藕夫,同理在OC端也有一個(gè)OC Bridge孽糖,JS Bridge將調(diào)用的模塊方法記錄并轉(zhuǎn)化成相應(yīng)的ModuleName,MethodName和args毅贮。
然后在MessageQueue中將調(diào)用模塊方法時(shí)的回調(diào)函數(shù)注冊(cè)一個(gè)CallBack ID办悟,將ID和回調(diào)函數(shù)存儲(chǔ)在一個(gè)成員變量的列表中,并將第2步中的ModuleName和MethodName根據(jù)模塊配置表信息轉(zhuǎn)成對(duì)應(yīng)的ID滩褥。
JS將moduleID病蛉,methodID和args以及CallBackID傳遞給OC Bridge,這個(gè)過(guò)程實(shí)質(zhì)上是基于事件處理的瑰煎,因?yàn)樵谝苿?dòng)平臺(tái)上如果有代碼的執(zhí)行必定是某個(gè)事件觸發(fā)的铺然,比如滑動(dòng)屏幕等等,事件觸發(fā)后OC主動(dòng)調(diào)用JS代碼酒甸,JS處理業(yè)務(wù)邏輯過(guò)程并將需要調(diào)用OC的部分存儲(chǔ)到MessageQueue中魄健,再去通知OC執(zhí)行。
OC接收到消息插勤,通過(guò)模塊配置表拿到對(duì)應(yīng)的模塊和方法沽瘦,在OC Bridge端,對(duì)每一個(gè)可被調(diào)用的模塊方法都會(huì)有一個(gè)RCTModuleMethod對(duì)象與之對(duì)應(yīng)农尖。
RCTModuleMethod對(duì)傳進(jìn)來(lái)的參數(shù)進(jìn)行處理析恋,包括類(lèi)型轉(zhuǎn)化以及創(chuàng)建一個(gè)Block對(duì)象以供回調(diào),會(huì)將JS端傳過(guò)來(lái)的CallBackID以及回調(diào)的值存儲(chǔ)進(jìn)Block對(duì)象中
執(zhí)行OC端代碼
執(zhí)行第6步中生成的Block方法
block里帶著CallbackID和block傳過(guò)來(lái)的參數(shù)去調(diào)JS里MessageQueue的方法invokeCallbackAndReturnFlushedQueue盛卡。
MessageQueue根據(jù)CallBackId找到對(duì)應(yīng)的回調(diào)函數(shù)
根據(jù)OC傳來(lái)的回調(diào)值助隧,執(zhí)行回調(diào)函數(shù)
整個(gè)流程就是這樣,簡(jiǎn)單概括下滑沧,差不多就是:JS函數(shù)調(diào)用轉(zhuǎn)ModuleID/MethodID -> callback轉(zhuǎn)CallbackID -> OC根據(jù)ID拿到方法 -> 處理參數(shù) -> 調(diào)用OC方法 -> 回調(diào)CallbackID -> JS通過(guò)CallbackID拿到callback執(zhí)行
4. React Native優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
- 能夠利用 JavaScript 動(dòng)態(tài)更新的特性并村,快速迭代漏健。
- 相比于原生平臺(tái),開(kāi)發(fā)速度更快橘霎,相比于 Hybrid 框架蔫浆,性能更好。
缺點(diǎn)
- 不能實(shí)現(xiàn)真正意義上的跨平臺(tái)姐叁,開(kāi)發(fā)者仍然需要為iOS和Android提供兩套實(shí)現(xiàn)機(jī)制
- 不能直接取代Native Code開(kāi)發(fā)瓦盛,很大程度上還加重了開(kāi)發(fā)者的學(xué)習(xí)成本
- 語(yǔ)言互轉(zhuǎn)存在著固定的時(shí)間和空間開(kāi)銷(xiāo)