NativeScript工作原理


NativeScript是一個runtime,它提供一些機制可以使用JavaScript構建原生的IOS、Android甚至WP(未來會加入)應用。NativeScript有很多非常酷的功能,比如MVVMCSS渲染原生UI。但是NativeScript最令人興奮的是它使JavaScript可以直接調用native API。

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

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。

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

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

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

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

這句話可以這么理解,Objective-C和Java也需要調用原生API並且調用方式存在差異,NativeScript削減了差異化,令原生API的調用方式更加簡單統一。

下面我們看看NativeScript的工作原理。

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規范的實現程度。

時刻謹記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。

深入學習資料:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM