NativeScript工做原理

NativeScript是一個runtime,它提供一些機制可使用JavaScript構建原生的IOS、Android甚至WP(將來會加入)應用。NativeScript有不少很是酷的功能,好比MVVMCSS渲染原生UI。可是NativeScript最使人興奮的是它使JavaScript能夠直接調用native API。javascript

這聽起來能夠會使人困惑,首先看一個例子,下面是使用NativeScript編寫Android app的一段代碼:html

var time = new android.text.format.Time();
time.set( 1, 0, 2015 );
console.log( time.format( "%D" ) ); //01/01/15

上述的JavaScript代碼實例化了一個Java對象android.text.format.Time,調用它的set方法和format方法而且在控制檯輸出log。java

咱們先不解釋上述代碼的實現原理,再看一個使用NativeScript編寫IOS app的例子:node

var alert = new UIAlertView();
alert.message = "Hello world!";
alert.addButtonWithTitle( "OK" );
alert.show();

上述的JavaScript代碼實例化了一個Objective-C類UIAlertView,隨後給它的message屬性賦值,調用了addButtonWithTitle方法和show方法,運行效果以下圖:
android

NativeScript並不是只包含JavaScript化的Objective-C和Java代碼,還集合了一系列的跨平臺module,好比發送http請求、構建UI組件等等。大部分app都須要調用原生的API,NativeScript的runtime簡化了原生API的調用方式。ios

這句話能夠這麼理解,Objective-C和Java也須要調用原生API而且調用方式存在差別,NativeScript削減了差別化,令原生API的調用方式更加簡單統一。web

下面咱們看看NativeScript的工做原理。npm

1. NativeScript runtime

雖然NativeScript的代碼看起來很神奇,可是內部的工做原理其實很簡單。NativeScript本質上仍然是JavaScript,解析執行JavaScript的天然是JavaScript引擎。在不一樣的平臺,NativeScript使用平臺默認的JavaScript引擎,好比Android平臺的V8引擎、IOS平臺的JavaScriptCore。既然使用JavaScript引擎解析代碼,那麼全部的native API的調用語法必須寫成規範的JavaScript語法,這樣才能夠被JavaScript引擎成功解析。編程

NativeScript使用的是最新穩定版本的V8和JavaScriptCore。所以,NativeScript對ECMAScript規範的支持狀況與它使用JavaScript的引擎徹底相同。也就是說,Android平臺依賴V8對ECMAScript規範的實現程度,IOS依賴JavaScriptCore對ECMAScript規範的實現程度。app

時刻謹記NativeScript是依賴JavaScript引擎這一點很是重要。

咱們再看第一個例子中的第一行代碼:

var time = new android.text.format.Time();

在Android平臺,上述NativeScript代碼由V8及時編譯(JIT Compiled)並執行。對於簡單的表達式(好比var x = 1 + 2),咱們很容易理解是怎麼工做的。可是V8是如何識別android.text.format.Time的呢?

2. NativeScript如何操做JavaScript引擎

V8之因此可以識別android對象是因爲NativeScript runtime把它注入到了JavaScript運行環境中。V8提供了大量的API供使用者配置個性化的JavaScript運行環境,甚至能夠注入C++代碼用來統計JavaScript的CPU使用狀況、管理JavaScript的GC等等。
Alt text

在這些API當中,有些Context類能夠提供操做全局做用域的API,這就是NativeScript之因此可以在全局做用域內注入android對象的原理。這種原理其實與Node.js全局方法(好比require())的實現原理相同。IOS的JavaScriptCore引擎也提供了相似的機制。

咱們再回顧一下以前的代碼:

var time = new android.text.format.Time();

如今咱們知道了這段代碼運行在V8上,而且V8能夠識別android.text.format.Time()是由於NativeScript在全局做用域內注入了android對象。可是仍然有不少疑問沒有解決,好比NativeScript如何知道須要注入哪些API?NativeScript如何知道調用Time()會產生什麼效果?

下面咱們依次解決這些疑問。

3. Metadata(元數據)

NativeScript經過reflection(反射)來構建它所運行平臺的可用API。不熟悉其餘編程語言的JavaScript開發者可能並不瞭解reflection,JavaScript是一門很是自由的語言,並不須要reflection。可是在其餘編程語言中,尤爲是Java,reflection是在runtime時獲取某個class詳細信息的惟一途徑。

能夠簡單的把reflection理解爲在runtime(運行時)而不是編譯期獲取某個object或class完整結構的途徑。reflection的詳細介紹感興趣的能夠參考這裏

NativeScript使用reflection構建了適用於各平臺的API列表。從性能角度來說,生成這些API數據是很是有必要的,NativeScript在編譯以前生成這些數據,而後在Android/IOS編譯階段嵌入已生成的元數據。

瞭解了以上機制以後,咱們再回顧一下以前的代碼:

var time = new android.text.format.Time();

如今咱們知道了以上代碼之因此可以在V8上運行,使由於NativeScript注入了android.text.format.Time對象。NativeScript經過一個獨立的元數據處理過程當中明確了須要注入的API,而且在Android和IOS的編譯階段嵌入了所需的元數據。

好,咱們繼續解答下一個問題:NativeScript是如何將JavaScript的Time()調用映射到原生的android.text.format.Time()調用呢?

4. 原生代碼的喚起機制

NativeScript喚起原生代碼調用一樣依賴於JavaScript引擎的API。上文提到了NativeScript如何對V8引擎注入全局變量,接下來介紹如何經過回調函數實如今JavaScript代碼中調用C++代碼。

好比在執行new android.text.format.Time()這段代碼,V8引擎將會產生一個回調函數。利用這種機制,NativeScript能夠監聽JavaScript函數的調用,而且在V8回調函數裏執行C++代碼,從而實現原生代碼的調用。

這裏提到的回調函數並非JavaScript的回調函數,而是V8引擎內部的C++函數。V8解析執行JavaScript函數時首先將JavaScript函數映射爲C++函數,而後再執行。

Android平臺下,NativeScript的C++代碼不能直接調用Java的API(好比android.text.format.Time)。這種狀況下須要藉助Android平臺的JNI(Java Native Interface,Java本地接口)實現C++與Java的橋接。藉助於JNI,NativeScript即可以調用Android平臺的原生Java API。

IOS平臺並不須要相似JNI的橋接機制,由於C++能夠直接喚起Objective-C的調用。

瞭解了以上機制,咱們再回顧一下以前的代碼:

var time = new android.text.format.Time();

上文的描述中,咱們知道以上代碼能夠執行的原理是NativeScript經過單獨的元數據生成過程注入了JavaScript引擎android全局對象。而後在執行Time()函數時,依次發生瞭如下行爲:

  1. V8回調函數執行;
  2. NativeScript runtime經過元數據明確Time()的行爲是實例化native對象android.text.format.Time
  3. NativeScript runtime經過JNI實例化android.text.format.Time對象而且保持對這個對象的引用;
  4. NativeScript runtime返回一個JavaScript對象用來代理Java本地對象android.text.format.Time
  5. 回到JavaScript運行環境中,第4步返回的代理對象儲存在本地變了time中。

這裏提到的代理對象是NativeScript用來維持JavaScript對象和native對象的映射關係(mapping)。好比執行如下JavaScript代碼:

var time = new android.text.format.Time();
time.set( 1, 0, 2015 );

根據生成的元數據,NativeScript知道代理對象time的所用API。按照上述步驟,當調用JavaScript函數Time()時,V8執行對應的回調函數,NativeScript監測到函數的調用,便經過JNI喚起Java的Time對象的調用。

以上即是NativeScript的工做原理。

至於如何將Objective-C對象和Java對象映射爲JavaScript對象,這部分工做很是複雜,由於必須考慮到每種編程語言實現繼承模式的差別。感興趣的能夠參考IOS的實現方案Android的實現方案

經過以上內容,雖然咱們知道了如何使用JavaScript代碼調用原生API,可是若是針對每一個不一樣平臺都分別編寫對應的代碼,仍然不可以實現「write once,run anywhere」。爲了實現這個目標,NativeScript提供了一種很是強大的功能:NativeScript modules

5. NativeScript modules

NativeScript modules的原理與Node Modules的原理相似,一樣遵循CommonJS規範,若是你熟悉Node中require()exports的工做原理,那麼NativeScript modules對你來講便很是容易入手了。

NativeScript modules把各平臺專有的API封裝成與平臺無關的API(相似你們熟知的JavaScript各類兼容性工廠函數)。好比如今咱們須要調用各平臺的file API,針對Android平臺的代碼以下:

new java.io.File( path );

針對IOS平臺的代碼 以下:

NSFileManager.defaultManager();
fileManager.createFileAtPathContentsAttributes( path );

是否是很麻煩?可是若是使用NativeScript file-system module,你只須要使用統一的API:

var fs = require( "file-system" );
var file = new fs.File( path );

若是你已經掌握了本文提到的NativeScript工做原理,即可以很容易的編寫NativeScript Module。好比要編寫一個module來獲取設備OS的版本:

// device.ios.js
module.exports = {
    version: UIDevice.currentDevice().systemVersion
}

// device.android.js
module.exports = {
    version: android.os.Build.VERSION.RELEASE
}

調用上述Module的方式與調用npm模塊相同,使用require()以下:

var device = require( "./device" );
console.log( device.version );

NativeScript Module下降了web開發者開發native應用的門檻,即便你不熟悉native API,也能夠花費很是少的時間閱讀各平臺的API文檔,而後編寫一個NativeScript Module來增長後續的開發效率。

6. 總結

本文簡單介紹了NativeScript的工做原理,總結以下:

  1. 經過reflection獲取native API的詳細結構,並生成元數據。這些行爲都是在runtime中JIT編譯;
  2. 根據生成的元數據信息,NativeScript利用JavaScript引擎的callback機制向JavaScript運行環境中注入須要的JavaScript全局對象。這些全局對象本質上是native對象的代理對象;
  3. 經過NativeScript Modules統一API。

深刻學習資料:

相關文章
相關標籤/搜索