NativeScript的工作原理:用JavaScript調用原生API實現跨平臺

碼頭工人 9年前發布 | 76K 次閱讀 移動開發 NativeScript

原文  http://ourjs.com/detail/550138f51e8c708516000005

注* NativeScript是最近推出的一個跨平臺解決方案,可以讓你可以用JavaScript來直接寫Android、iOS本地應用程序,未來還即將擴展到Windows平臺。是最近比較受關注的項目。它與nw (原名node-webkit ,用Web寫winodw/linux桌面應用)和phonegap內嵌webview寫APP的實現方式有著本質的不同,它直接用JavaScript調用系統原生API,因而有一些原生應用的特點。

NativeScript


NativeScript 是一個運行環境,可以讓你使用通用的JavaScript代碼,打造原生的iOS,Android和Windows(即將推出)應用程序。 NativeScript有很多很酷的功能,比如支持JavaScript對象雙向綁定到原生UI組件,以及用CSS為原生應用程序寫樣式。但我最喜歡的功能是NativeScript可以讓您直接訪問本地平臺的原生API。

注* 可以理解為NativeScript是一個JavaScript V8運行環境的命令轉發代理,將JavaScript調用轉發給不同平臺上的原生API如Android、iOS,以及即將支持的Windows。

例如,看看這個NativeScript寫的Android應用程序的代碼:

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

你只需要一兩分鐘來分析一下就明白了,這段JavaScript代碼實例化一個Java android.text.format.Time()對象,調用其set()方法,然后打印format后的返回值,是字符串“01/01/15”。

我知道你已經很激動了,先不要慌,讓我們再來看看iOS的代碼:

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

這段JavaScript代碼實例化一個Objective-C UIAlertView類,設置它的信息屬性,然后調用它的addButtonWithTitle()和show()方法。當您運行這段代碼,你會看到hello word的警告框。

NativeScript運行時


該 NativeScript運行環境看起來可能像變魔術一樣,不管你信不信,該架構并不是那么的復雜。一切從JavaScript虛擬機開始,NativeScript從這里開始執行JavaScript指令。具體來說,NativeScript在Android采用v8;在iOS上采用JavaScriptCore。由于NativeScript使用JavaScript虛擬機,你訪問原生API的所有JavaScript代碼,仍然需要遵守JavaScript的語法結構和規范。

一般來說,NativeScript會同時采用V8和JavaScriptCore的最新穩定版;因此NativeScript對ECMAScript語言的支持在iOS的桌面Safari上面幾乎是相同的,并且NativeScript在Android上面也幾乎與桌面瀏覽器相同。包括一些ES6的新語法。

了解NativeScript使用了JavaScript虛擬機是很重要的,但這只是實現的第一步,讓我們來看看上文示例的第一行代碼:

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

在NativeScript Android運行環境中,該代碼會被編譯(JIT)并在V8中執行。這我們可能會很容易理解變...:

var x = 1 + 2;

但是接下來的問題是...V8怎么知道什么是android.text.format.Time()呢?

我們將重點講V8和Android的實現,基本架構模式同樣適用于iOS上的JavaScriptCore。如果出現顯著差別,本文會提到。

這里將不討論NativeScript在Windows上的實現細節,因為解決方案可能還會變,但是,當前的Windows實現跟iOS上的JavaScriptCore運行原理幾乎一樣。


NativeScript是怎樣管理JavaScript虛擬機的


V8 知道android是什么,因為NativeScript在運行時進行了注入,因為V8擁有一堆讓你配置JavaScript環境的API。在 JavaScript中您可以使用自定義的C++代碼來分析CPU使用率,管理的JavaScript垃圾收集,等等一大堆API

面對這些API的是幾個“Context”類,可以讓你操縱全局變量,從而有可能為NativeScript注入一個全局的android對象。這實際上使用了與Node.js的相同運行機制,使全局API可用 - 如 require() - NativeScript使用它注入可以讓你訪問本地代碼的API。 JavaScriptCore的也有類似的機制。酷吧?

讓我們回到我們的代碼:

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

現在你知道這個代碼在V8中運行時,而V8已經知道什么是android.text.format.Time()了,因為NativeScript注入了必需的對象到全局范圍。但仍存在著一些大的懸而未決的問題,如何讓NativeScript明白那些注入的API到底是干什么的,然后調用?

Metadata(元數據)


對于NativeScript,反射是讓NativeScript可以調用每個平臺上的API的基石。包括 android.text.format.Time。因為從性能角度來看重構這些API是很困難的,NativeScript會提前做掉這些,并在 Android/iOS預編繹過程中嵌入預先生成的元數據。

考慮到這一點,讓我們再次回到我們的代碼:

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

現在你了解了這個V8代碼是這樣運行的,即NativeScript注入了android.text.format.Time的JavaScript對象,通過每一個單獨的元數據注入。下一個問題:如何將NativeScript里的JavaScript調用Time()轉發到本機 android.text.format.Time()?

調用本地代碼


NativeScript如何調用本機代碼的答案就在于JavaScript虛擬機的API。我們上次使用V8的API是注入全局變量。這一次,我們將著眼于在JavaScript回調中調用給定的C++代碼。

例如,JavaScript函數調用的代碼 new android.text.format.Time(),V8會產生一個回調。也就是說V8有一個回調,讓NativeScript攔截函數調用,然后用自定義的C ++代碼執行一些動作,并返回一個新的結果。

在Android中的情況下,NativeScript運行的C++代碼不能直接訪問Java API,如android.text.format.Time。然而,Android的JNI ,或Java本地接口,提供了C++和Java之間的橋接能力,所以NativeScript使用JNI完成轉發。在iOS中這個橋梁是不必要的,因為C++代碼可以直接調用Objective-C的API。

了解了這些,讓我們再回到代碼:

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

我們已經知道,這個代碼在V8中運行;是因為NativeScript注入過對象,它知道什么是android.text.format.Time;并且 NativeScript中有這些基于元數據生成的API。我們現在知道,當 Timer() 執行時,會發生下面的事情:

1)V8運行回調函數。
2)NativeScript運行時通過它的元數據知道,Time()調用需要實例化一個android.text.format.Time對象。
3)NativeScript運行時使用JNI來實例化一個android.text.format.Time對象并保持對它的引用。
4)NativeScript運行時將代理的Java Time對象轉化成JavaScript對象返回。
5)控制返回的JavaScript代理對象為被存儲起來的本地時間變量。

代理對象是在NativeScript中保持JavaScript對象與本地平臺對象的人工映射。例如,讓我們來看看前面代碼的下一行:

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

基于所生成的元數據,NativeScript知道代理對象上的所有方法。在這種情況下,代碼調用Timer對象的set()方法時。此方法會再次調用V8及其功能回調; 然后NativeScript通過Android JNI轉發到Java時間對象上相應的方法調用。

這就是NativeScript在部分的工作原理。酷吧?

現在,還遺留下了一些非常復雜的部分,因為將Objective-C和Java對象轉換成JavaScript對象可能會很麻煩,尤其是考慮到不同的繼承類型語言時。

我們不打算深入探討這些問題的細節,NativeScript的另一個特點讓你不必深入到本地代碼,比如:TNS模塊。


TNS Modules


TNS modules是Telerik NativeScript modules的簡寫。跟Node模塊一樣,它同樣使用CommonJS。因此如果你已經會用require()和exports對象,那么你就已經掌握了TNS模塊。

TNS模塊允許你將特定的本地調用抽象成平臺無關的API,NativeScript本身提供了幾十個這樣的模塊供您使用。舉個例子,假設您需要在您的iOS / Android應用程序創建的文件。你可能在Android中要寫以下代碼:

new java.io.File( path );

同樣在iOS里寫下面的代碼:

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

但是如果你使用TNS文件系統模塊,你的代碼將是一樣的,而不必擔心的iOS / Android的內部實現細節:

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

更酷的是,你可以自己寫TNS模塊。比如這里有一個TNS模塊,檢索設備的操作系統版本:

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

// device.android.js module.exports = {     version: android.os.Build.VERSION.RELEASE }</pre>此代碼只檢索一個版本屬性,但它給你多少靈感?使用自定義TNS模塊是微不足道的,跟在nodejs中使用NPM模塊一樣:

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

如果你已經熟悉了npm的使用,NativeScript模塊非常容易編寫,分發和使用。就個人而言,作為一個Web開發人員,原生的iOS和 Android代碼讓我害怕,尤其是當Java / Objective-C的API文檔扔在一起的功能,它降低了我們跨平臺開發的障礙。

原文地址: developer.telerik.com
 本文由用戶 碼頭工人 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!