標籤:
React Native是facebook剛開源的架構,可以用javascript直接開發原生APP,先不說這個架構後續是否能得到福士認可,單從源碼來說,這個架構源碼裡有非常多的設計思想和實現方式值得學習,本篇先來看看它最基礎的JavaScript-ObjectC通訊機制(以下簡稱JS/OC)。
概覽
React Native用iOS內建的JavaScriptCore作為JS的解析引擎,但並沒有用到JavaScriptCore提供的一些可以讓JS與OC互調的特性,而是自己實現了一套機制,這套機制可以通用於所有JS引擎上,在沒有JavaScriptCore的情況下也可以用webview代替,實際上項目裡就已經有了用webview作為解析引擎的實現,應該是用於相容iOS7以下沒有JavascriptCore的版本。
普通的JS-OC通訊實際上很簡單,OC向JS傳資訊有現成的介面,像webview提供的-stringByEvaluatingJavaScriptFromString方法可以直接在當前context上執行一段JS指令碼,並且可以擷取執行後的傳回值,這個傳回值就相當於JS向OC傳遞資訊。React Native也是以此為基礎,通過各種手段,實現了在OC定義一個模組方法,JS可以直接調用這個模組方法並還可以無縫銜接回調。
舉個例子,OC定義了一個模組RCTSQLManager,裡面有個方法-query:successCallback:,JS可以直接調用RCTSQLManager.query並通過回調擷取執行結果。:
123456789 |
//OC: @implement RCTSQLManager - ( void )query:( NSString *)queryData successCallback:(RCTResponseSenderBlOCk)responseSender { RCT_EXPORT(); NSString *ret = @"ret" responseSender(ret); } @end |
1234 |
//JS: RCTSQLManager.query( "SELECT * FROM table" , function (result) { //result == "ret"; }); |
接下來看看它是怎樣實現的。
模組配置表
首先OC要告訴JS它有什麼模組,模組裡有什麼方法,JS才知道有這些方法後才有可能去調用這些方法。這裡的實現是OC產生一份模組配置表傳給JS,配置表裡包括了所有模組和模組裡方法的資訊。例:
0102030405060708091011121314 |
{ "remoteModuleConfig" : { "RCTSQLManager" : { "methods" : { "query" : { "type" : "remote" , "methodID" : 0 } }, "moduleID" : 4 }, ... }, } |
OC端和JS端分別各有一個bridge,兩個bridge都儲存了同樣一份模組配置表,JS調用OC模組方法時,通過bridge裡的配置表把模組方法轉為模組ID和方法ID傳給OC,OC通過bridge的模組配置表找到對應的方法執行之,以上述代碼為例,流程大概是這樣(先不考慮callback):
在瞭解這個調用流程之前,我們先來看看OC的模組配置表式怎麼來的。我們在建立一個OC模組時,JS和OC都不需要為新的模組手動去某個地方添加一些配置,模組配置表是自動產生的,只要項目裡有一個模組,就會把這個模組加到配置表上,那這個模組配置表是怎樣自動產生的呢?分兩個步驟:
1.取所有模組類
每個模組類都實現了RCTBridgeModule介面,可以通過runtime介面objc_getClassList或objc_copyClassList取出項目裡所有類,然後逐個判斷是否實現了RCTBridgeModule介面,就可以找到所有模組類,實現在RCTBridgeModuleClassesByModuleID()方法裡。
2.模數塊裡暴露給JS的方法
一個模組裡可以有很多方法,一些是可以暴露給JS直接調用的,一些是私人的不想暴露給JS,怎樣做到提取這些暴露的方法呢?我能想到的方法是對要暴露的方法名制定一些規則,比如用RCTExport_作為首碼,然後用runtime方法class_getInstanceMethod取出所有方法名字,提取以RCTExport_為首碼的方法,但這樣做噁心的地方是每個方法必須加首碼。React Native用了另一種黑魔法似的方法解決這個問題:編譯屬性__attribute__。
在上述例子中我們看到模組方法裡有句代碼:RCT_EXPORT(),模組裡的方法加上這個宏就可以實現暴露給JS,無需其他規則,那這個宏做了什麼呢?來看看它的定義:
12 |
#define RCT_EXPORT(JS_name) __attribute__((used, section("__DATA,RCTExport" \ ))) static const char *__rct_export_entry__[] = { __func__, #JS_name } |
這個宏的作用是用編譯屬性__attribute__給二進位檔案建立一個section,屬於__DATA資料區段,名字為RCTExport,並在這個段裡加入當前方法名。編譯器在編譯時間會找到__attribute__進行處理,為產生的可執行檔加入相應的內容。效果可以從linkmap看出來:
0102030405060708091011121314 |
# Sections: # Address Size Segment Section 0x100001670 0x000C0180 __TEXT __text ... 0x10011EFA0 0x00000330 __DATA RCTExport 0x10011F2D0 0x00000010 __DATA __common 0x10011F2E0 0x000003B8 __DATA __bss ... 0x10011EFA0 0x00000010 [ 4] -[RCTStatusBarManager setStyle:animated:].__rct_export_entry__ 0x10011EFB0 0x00000010 [ 4] -[RCTStatusBarManager setHidden:withAnimation:].__rct_export_entry__ 0x10011EFC0 0x00000010 [ 5] -[RCTSourceCode getScriptText:failureCallback:].__rct_export_entry__ 0x10011EFD0 0x00000010 [ 7] -[RCTAlertManager alertWithArgs:callback:].__rct_export_entry__ ... |
可以看到可執行檔資料區段多了個RCTExport段,內容就是各個要暴露給JS的方法。這些內容是可以在運行時擷取到的,在RCTBridge.m的RCTExportedMethodsByModuleID()方法裡擷取這些內容,提取每個方法的類名和方法名,就完成了提模數塊裡暴露給JS方法的工作。
整體的模組類/方法提取實現在RCTRemoteModulesConfig()方法裡。
調用流程
接下來看看JS調用OC模組方法的詳細流程,包括callback回調。這時需要細化一下上述的調用流程圖:
看起來有點複雜,不過一步步說明,應該很容易弄清楚整個流程,圖中每個流程都標了序號,從發起調用到執行回調總共有11個步驟,詳細說明下這些步驟:
1.JS端調用某個OC模組暴露出來的方法。
2.把上一步的調用分解為ModuleName,MethodName,arguments,再扔給MessageQueue處理。
在初始化時模組配置表上的每一個模組都產生了對應的remoteModule對象,對象裡也產生了跟模組配置表裡一一對應的方法,這些方法裡可以拿到自身的模組名,方法名,並對callback進行一些處理,再移交給MessageQueue。具體實現在BatchedBridgeFactory.js的_createBridgedModule裡,整個實現區區24行代碼,感受下JS的魔力吧。
3.在這一步把JS的callback函數緩衝在MessageQueue的一個成員變數裡,用CallbackID代表callback。在通過儲存在MessageQueue的模組配置表把上一步傳進來的ModuleName和MethodName轉為ModuleID和MethodID。
4.把上述步驟得到的ModuleID,MethodId,CallbackID和其他參數argus傳給OC。至於具體是怎麼傳的,後面再說。
5.OC接收到訊息,通過模組配置表拿到對應的模組和方法。
實際上模組配置表已經經過處理了,跟JS一樣,在初始化時OC也對模組配置表上的每一個模組產生了對應的執行個體並緩衝起來,模組上的每一個方法也都產生了對應的RCTModuleMethod對象,這裡通過ModuleID和MethodID取到對應的Module執行個體和RCTModuleMethod執行個體進行調用。具體實現在_handleRequestNumber:moduleID:methodID:params:。
6.RCTModuleMethod對JS傳過來的每一個參數進行處理。
RCTModuleMethod可以拿到OC要調用的目標方法的每個參數類型,處理JS類型到目標類型的轉換,所有JS傳過來的數字都是NSNumber,這裡會轉成對應的int/long/double等類型,更重要的是會為block型別參數的產生一個block。
例如-(void)select:(int)index response:(RCTResponseSenderBlock)callback 這個方法,拿到兩個參數的類型為int,block,JS傳過來的兩個參數類型是NSNumber,NSString(CallbackID),這時會把NSNumber轉為int,NSString(CallbackID)轉為一個block,block的內容是把回調的值和CallbackID傳回給JS。
這些參數組裝完畢後,通過NSInvocation動態調用相應的OC模組方法。
7.OC模組方法調用完,執行block回調。
8.調用到第6步說明的RCTModuleMethod產生的block。
9.block裡帶著CallbackID和block傳過來的參數去調JS裡MessageQueue的方法invokeCallbackAndReturnFlushedQueue。
10.MessageQueue通過CallbackID找到相應的JS callback方法。
11.調用callback方法,並把OC帶過來的參數一起傳過去,完成回調。
整個流程就是這樣,簡單概括下,差不多就是:JS函數調用轉ModuleID/MethodID -> callback轉CallbackID -> OC根據ID拿到方法 -> 處理參數 -> 調用OC方法 -> 回調CallbackID -> JS通過CallbackID拿到callback執行
事件響應
上述第4步留下一個問題,JS是怎樣把資料傳給OC,讓OC去調相應方法的?
答案是通過傳回值。JS不會主動傳遞資料給OC,在調OC方法時,會在上述第4步把ModuleID,MethodID等資料加到一個隊列裡,等OC過來調JS的任意方法時,再把這個隊列返回給OC,此時OC再執行這個隊列裡要調用的方法。
一開始不明白,設計成JS無法直接調用OC,需要在OC去調JS時才通過傳回值觸發調用,整個程式還能跑得通嗎。後來想想純native開發裡的事件響應機制,就有點理解了。native開發裡,什麼時候會執行代碼?只在有事件觸發的時候,這個事件可以是啟動事件,觸摸事件,timer事件,系統事件,回調事件。而在React Native裡,這些事件發生時OC都會調用JS相應的模組方法去處理,處理完這些事件後再執行JS想讓OC執行的方法,而沒有事件發生的時候,是不會執行任何代碼的,這跟native開發裡事件響應機制是一致的。
說到OC調用JS,再補充一下,實際上模組配置表除了有上述OC的模組remoteModules外,還儲存了JS模組localModules,OC調JS某些模組的方法時,也是通過傳遞ModuleID和MethodID去調用的,都會走到-enqueueJSCall:args:方法把兩個ID和參數傳給JS的BatchedBridge.callFunctionReturnFlushedQueue,跟JS調OC原理差不多,就不再贅述了。
總結
整個React Native的JS-OC通訊機制大致就是這樣了,關鍵點在於:模組化,模組配置表,傳遞ID,封裝調用,事件響應,其設計思想和實現方法很值得學習借鑒
React Native通訊機制詳解