如何編寫自己的 Native Bridge
和很多人一樣,在我弄清楚 React Native 的實現機制之前,其實已經在實際項目中用過一段兒時間了。不過在我學習 React Native 實現機制的過程中,逐漸開始給這個項目貢獻代碼,最終成為核心開發者中的一員。
盡管如此,這個項目中的 Native Bridge 對我來說還是很神秘的,它到底是通過什么『魔法』打通了 JavaScript 和 Objective C 這兩種不同編程語言之間的邊界。正因為缺少對這部分內容的了解,才促使我花了一些時間做了這些調研,然后把我的理解通過這幾篇文章分享給大家。
毋庸置疑,在沒有一個對項目清晰的規劃設計之前就寫代碼是沒有任何意義的。架構設計對于軟件開發來說是很重要的一步,我會講解整個思考過程,詳細解釋我們做的一些決策以及背后的原因。
第一章:設計一個架構
第一個迭代
在開始一個新項目之前,首先需要確定一下在這個項目中應該采用哪些技術方案:
在我們這個場景下,選擇的技術方法如下:
- Objective C 來控制 Cocoa UI
- C++ 來和 JSVM 交互
- JSVM (V8 或 Chakra)
- JavaScript
- React + 自定義渲染器
我默認主要的執行邏輯是用 C++ 寫的,它內部有一個自定義全局上下文的 JS 引擎(基本上所有主流的 JS 引擎 都提供擴展內置函數的能力),所以當看到自定義全局上下文的時候不要驚訝,其實并沒有什么特殊的地方,只是通過 C++ 擴充了一些內置的 JS 函數,稍候我們會介紹相關的內容。
一旦我們調用這些自定義的 JS 函數,JS 虛擬機就會執行我們在 C++ 里面實現的那部分代碼,然后就可以繼續調用 Objective C 的代碼來繪制 UI。聽起來有點兒復雜,但是不要灰心,很快就看到曙光了。
上述所有的邏輯都可以在同一個線程(主線程)中運行,但是可能會導致一些性能問題。為了避免性能問題,我門給通過 Objective C 進行 UI 渲染的工作專設了一個新的線程。
盡管聽起來比較合理,可是實際上卻并不管用。
問題在于 Apple 限制了只可以在主線程中渲染 UI,這也就導致我們只能在主線程中去執行 Objective C 的代碼。可惜的是,這樣應用的入口就和平臺綁定了(至少要讓它跑在其他平臺上會變的非常復雜)。但事已至此,我們只能調整我們最初的設計,來滿足這項限制。
修正方案
如果必須在主線程渲染 UI,那我們就接受這個限制。不過我們要運行一個 Cocoa 應用,而非 C++ 的程序。在它啟動時我們創建一個包含了 JSVM 的后臺線程來運行打包好的 JS 代碼。和前一種方法里一樣,因為有 JSVM 的存在,我們可以在 JS 代碼中調用 C++ 寫的函數。一旦這樣的函數被調用,主線程就會收到指令并繪制 UI。
在主線程中,(JS 線程)發來的指令通過 Objective C 渲染出對應的界面元素。如果成功,Objective C 會回調一個從 C++ 傳遞過來的函數(代表 JSVM 對 JS 邏輯的回調)。
最終的架構
好了,最后總結一下:
-
因為需要在主線程中繪制 UI,所以我們需要一個 Cocoa 類型的應用;
-
當程序啟動的時候,我們需要創建一個 JS 線程來初始化 JS 引擎,然后執行我們打包在一起的 JS 代碼。之前說過,我們給 JSVM 上下文打過補丁,暴露了一些額外的 API 來操作 UI 層;
-
一旦JS線程需要繪制UI的時候,就給主線程發一個命令,主線程收到之后,Objective C 代碼就開始執行對應的繪制邏輯;
-
最后,Objective C 執行一系列的回調函數,從而把最終的執行結果傳遞通過 JS 線程 傳遞給我們在 JS 里面邏輯。
搭建基礎平臺
動手實踐的時候到了!首先,我們創建一個空白的 Cocoa 應用,如下圖所示:
新建 Mac OS X Cocoa Application 窗口
第一步完成之后,我們就有了一個基礎的程序架構。之后,編輯 AppDelegate.m 文件里面的 applicationDidFinishLaunching 函數,在這個函數里面應該啟動一個新的線程來初始化 JS 引擎,代碼如下:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { _jsvmThread = [[NSThread alloc] initWithTarget:self selector:@selector(runJSVMThread) object:nil]; [_jsvmThread start]; }
現在你可能有兩個疑問:
_jsvmThread @selector(runJSVMThread)
我們是通過創建一個 NSThread 的實例來啟動一個新的線程的, _jsvmThread 就是 NSThread 的一個實例。后續如果要執行 C++ 回調函數的時候,我們需要 NSThread 實例的引用,因此通過 _jsvmThread 變量保存了下來。
關于 Selector 的問題,根據 Apple 的文檔所述,是一個用來選擇一個對象要執行那個方法的名稱,或是在源碼編譯后用來代替這個名稱的唯一標識符。更多細節建議還是參考 Apple的官方文檔 。
換句話說,當創建了 NSThread 之后就會執行當前實例上面的 runJSVMThread 方法來完成 JS引擎 的初始化工作:
- (void)runJSVMThread { [[[ChakraProxy alloc] init] run]; NSRunLoop *runloop = [NSRunLoop currentRunLoop]; while (true) { [runloop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; } }
正如在代碼中看到的,我這里用了 ChakraCore 而沒有選擇 V8。原因是:
-
編譯 V8 花了我1個多小時的時間,然后又花了2個小時才能把 HelloWorld 運行起來;
-
編譯 ChakraCore 以及運行 HelloWorld 總共花了 10 多分鐘的時間。
由于我們只是編寫一個原型,所以我把 ChakraCore 提供的更好的開發體驗放在了首位。
ChakraProxy 初始化之后,我們必須通過一個 runloop 來保證線程不會退出(如果沒有這個機制的話,線程的代碼執行完畢之后就結束了)
現在我們的程序可以正常啟動了,而且還啟動了一個新的線程等待 UI 繪制的命令。現在我們來著手實現 ChakraProxy 這個類。
ChakraProxy 的邏輯很簡單,簡單來說就是用來整合 Objective C 和 C++ 代碼的一個封裝:
ChakraProxy.h
#import <Foundation/Foundation.h> @interface ChakraProxy : NSObject -(void)run; @end
ChakraProxy.m
#import "ChakraProxy.h" @implementation ChakraProxy -(void)run { NSLog(@"Hello from the JSVM thread!"); } @end
顯而易見,ChakraProxy 除了輸出一行日志之外,什么也沒有做,但是我們可以在此基礎上開展更多的工作了。在下一篇我們會添加更多的功能,比如整合 ChakraCore,基本的橋接模型,以及其他好多好多內容。
來自:http://efe.baidu.com/blog/how-to-create-you-own-native-bridge/