JS in Android (Google V8)

簡介

由于項(xiàng)目動(dòng)態(tài)化的需要,希望在 Android 應(yīng)用中嵌入腳本語言。常見有 LuaJs。Lua 的集成在之前的文章中已經(jīng)介紹過。lua 是一個(gè)非常輕量的語言,專為嵌入而生,這是他的優(yōu)勢(shì)。但因?yàn)樘p了,可以說幾乎沒有標(biāo)準(zhǔn)庫,這也使得用起來不是很方便。但目前最大的問題是 lua 腳本無法完美地運(yùn)行在 web 與微信小程序中,而 js 在這方面有先天的優(yōu)勢(shì)。

為了更加緊密地結(jié)合原生應(yīng)用,要求是不依賴 webview,而是程序本身與腳本語言進(jìn)行交互。鑒于 js 不像 lua 那樣原生提供了交互 api,所以必須考慮嵌入一個(gè)腳本引擎來實(shí)現(xiàn)。

目前常見的 js 引擎有下面幾個(gè)

  • V8:是 Google 開發(fā)的一個(gè)開源 js 引擎,使用 c++ 作為開發(fā)語言,性能極高。j2v8 項(xiàng)目通過 JNI 將其移植到了 java。
  • Rhino: 是一個(gè) Mozilla 開發(fā)的開源 js 引擎。它的特點(diǎn)是完全使用 java 實(shí)現(xiàn),在和 java 的交互上有先天優(yōu)勢(shì)。并已經(jīng)被作為 JDK6/7 標(biāo)準(zhǔn)庫默認(rèn)的腳本引擎。(在 Android 中被移除了,需要手動(dòng)導(dǎo)入 jar)在 JDK8 中被 Nashorn 取代。
  • JsCore: 是 WebKit 內(nèi)核默認(rèn)的開源 js 引擎,由蘋果使用 C 開發(fā)。AndroidJSCore 通過 JNI 移植到了 Android,但是此項(xiàng)目已停止維護(hù)并合并到 LiquidCore。

關(guān)于上面三個(gè)引擎在 Android 上的效率表現(xiàn),可以參照網(wǎng)易杭州前端技術(shù)部的 測(cè)試文章??傮w來看 Rhino 表現(xiàn)極差,畢竟是純 java 實(shí)現(xiàn)也可以理解了。V8 表現(xiàn)優(yōu)異,而且 j2v8 封裝的很好,最終決定使用這個(gè)方案了。

J2V8

HelloWorld

V8 引擎為了實(shí)現(xiàn)極致的性能,其內(nèi)部是非常復(fù)雜的。幸運(yùn)的的是,j2v8 做了非常好的封裝,并且 java 層非常簡潔,上手可以說是非??炝?。如果有之前 lua 的整合經(jīng)驗(yàn),那么幾乎是一氣呵成。已經(jīng)發(fā)布到了 Maven 中央倉庫,Android Studio 可以直接添加 implementation 'com.eclipsesource.j2v8:j2v8:5.0.103@aar' 來自動(dòng)化集成。

要運(yùn)行 js 首先需要?jiǎng)?chuàng)建一個(gè)運(yùn)行環(huán)境,下面是一個(gè) HelloWorld,主要翻譯自官方文檔

下面的例子創(chuàng)建了一個(gè) js 運(yùn)行時(shí),并執(zhí)行一段 js 代碼獲得了結(jié)果:

public static void main(String[] args) {
 V8 runtime = V8.createV8Runtime(); // 創(chuàng)建 js 運(yùn)行時(shí)
 int result = runtime.executeIntegerScript("" // 執(zhí)行一段 js 代碼
  + "var hello = 'hello, ';\n"
  + "var world = 'world!';\n"
  + "hello.concat(world).length;\n");
 System.out.println(result);
 runtime.release(true); // 為 true 則會(huì)檢查并拋出內(nèi)存泄露錯(cuò)誤(如果存在的話)便于及時(shí)發(fā)現(xiàn)
}

一旦創(chuàng)建了運(yùn)行時(shí),就可以在上面執(zhí)行腳本。根據(jù)返回類型不同有好幾種腳本執(zhí)行方法。在本例中,我們使用 executeIntegerScript,因?yàn)榻Y(jié)果是一個(gè) int。當(dāng)應(yīng)用程序終止時(shí),必須釋放運(yùn)行時(shí)。

內(nèi)存管理

需要說明的,這里的內(nèi)存管理指的是 java 管理 c 的內(nèi)存。至于 js 對(duì)象的生命周期,會(huì)由 v8 引擎自動(dòng)管理。

手動(dòng)管理

顯然 c 需要手動(dòng)管理內(nèi)存。而 java 層拿到是其實(shí)直接一個(gè) c 的引用,因此必須手動(dòng)釋放內(nèi)存。否則會(huì)導(dǎo)致 c 層內(nèi)存泄露。注意:finalize() 方法中釋放是不可取的,因?yàn)榇撕瘮?shù)調(diào)用的實(shí)際不確定,甚至不保證會(huì)調(diào)用。

對(duì)于 j2v8 來說,以下對(duì)象必須手動(dòng)釋放:

  • 自行創(chuàng)建的對(duì)象。例如 new V8Object() 創(chuàng)建的。
  • 從 js 中主動(dòng)獲取的對(duì)象。例如 v8.getObject().
  • 從 js 數(shù)組中提取的。例如 v8Array.getObject(0).

注意:

  • c++ 層作為參數(shù)傳入到 java 的對(duì)象無需釋放。因?yàn)樗皇?java 自己創(chuàng)建的。
  • 但是若傳入的是數(shù)組,那么從數(shù)組中獲取的對(duì)象必須釋放,因?yàn)樗?java 主動(dòng)獲取的。
  • 創(chuàng)建出的用作傳給(或返回給) js 的對(duì)象必須釋放,因?yàn)樗?java 創(chuàng)建的。

自動(dòng)管理

從 j2v8 V4 開始支持了 自動(dòng)內(nèi)存管理。但這也不是全自動(dòng)的,需要手動(dòng)創(chuàng)建一個(gè)內(nèi)存管理器。

為了避免忘記釋放對(duì)象,MemoryManager 一旦被實(shí)例化,就會(huì)自動(dòng)跟蹤所有創(chuàng)建的 v8 對(duì)象,并在自身被釋放時(shí)釋放它們。作為開發(fā)者,可以在一個(gè)代碼塊的開始實(shí)例化 MemoryManager,然后調(diào)用一些 v8 api,最后在代碼塊結(jié)束時(shí)釋放它。這樣就可以保證這個(gè)代碼塊不會(huì)造成任何的 v8 內(nèi)存泄露。顯然自動(dòng)管理要求實(shí)例化內(nèi)存管理器,因此在頻繁調(diào)用的函數(shù)上面并不是一個(gè)最佳的選擇,因?yàn)檫@會(huì)不斷地創(chuàng)建對(duì)象造成額外開銷。

下面是沒有使用 MemoryManager 時(shí)的一個(gè)例子:

loDash = nodeJS.require(new File("/Users/irbull/node_modules/lodash"));

V8Object o1 = o("a", 1);
V8Object o2 = o("b", 2);
V8Object o3 = o("c", 3);
V8Object objects = (V8Object) loDash.executeJSFunction("assign", o1, o2, o3);
LoDashObject e1 = loDash(objects);
LoDashObject e2 = e1.e("values");
V8Function f = f((V8Object receiver, V8Array parameters) -> parameters.getInteger(0) * 3);
LoDashObject result = e2.e("map",f);
System.out.println(result);

loDash.release();
e1.release();
e2.release();
f.release();
o1.release();
o2.release();
o3.release();
result.release();
objects.release();

在用了自動(dòng)內(nèi)存管理后,代碼會(huì)變得非常簡潔:

MemoryManager scope = new MemoryManager(v8); // 實(shí)例化 MemoryManager
loDash = nodeJS.require(new File("/Users/irbull/node_modules/lodash"));

V8Object objects = (V8Object) loDash.executeJSFunction("assign", o("a", 1), o("b", 2), o("c", 3));
LoDashObject result = loDash(objects).e("values").e("map",
  f((V8Object receiver, V8Array parameters) -> parameters.getInteger(0) * 3));
System.out.println(result);

scope.release(); // 釋放

如果希望保留一些對(duì)象供將來使用,MemoryManager 提供了 persist API。例如,我們可以在 result 對(duì)象上使用它。

內(nèi)存管理器也可以嵌套。在進(jìn)入新范圍時(shí),可以創(chuàng)建一個(gè)新的 MemoryManager 來管理該范圍內(nèi)的對(duì)象。所有以前的 MemoryManager 將收到所有新對(duì)象創(chuàng)建和刪除的通知。

多線程

一般來說認(rèn)為 js 是單線程的。j2v8 所創(chuàng)建的 runtime 也只能同時(shí)在一個(gè)線程上使用。不過我們可以在不同線程上創(chuàng)建多個(gè) runtime 來同時(shí)運(yùn)行,但是這樣不同線程的 runtime 是相互隔離的,無法訪問其他線程下的變量與函數(shù)。

幸運(yùn)的是,j2v8 提供了線程切換。盡管同時(shí)只有1個(gè)線程可以訪問,但是我們可以在不同的線程間切換使用。j2v8 通過 V8Locker 來鎖定線程。在 runtime 創(chuàng)建時(shí),會(huì)自動(dòng)實(shí)例化一個(gè) V8Locker,在實(shí)例化時(shí)會(huì)自動(dòng)鎖定到當(dāng)前線程,只有獲得鎖的線程才允許訪問 runtime。要想其他線程獲得鎖,只需先釋放當(dāng)前線程的鎖即可。

下面是一個(gè)簡單的例子:(使用 kotlin 語言)此例子中定義了一個(gè)多線程的 AsyncTask,并在準(zhǔn)備工作中釋放了主線程的鎖,在子線程中取得了鎖并執(zhí)行了一些操作,在子線程退出前釋放鎖。最后主線程重新獲得鎖,完成了一個(gè)完整的線程切換。

private class LoadTask() : AsyncTask<int, int, int>() {
    override fun onPreExecute() {
        super.onPreExecute()
        if (v8.locker.hasLock()){
            v8.locker.release() // 釋放主線程的鎖
        }
    }
    override fun doInBackground(vararg params: int?): int {
        v8.locker.acquire() // 子線程獲得鎖
        // 執(zhí)行一些 v8 操作
        // ...
        v8.locker.release() // 釋放子線程的鎖
        return 1
    }
    override fun onPostExecute(result: int?) {
        super.onPostExecute(result)
        v8.locker.acquire() // 主線程重新獲得鎖
    }
}

Java 調(diào)用 Js

Java 調(diào)用 js 非常簡單,總共有三種方式。

1. 執(zhí)行 js 腳本

這就是 HelloWorld 中展示的方式。v8 可以直接執(zhí)行 js 腳本,并取得返回值。

2. 通過函數(shù)名調(diào)用

方式一雖然簡單,但若需要傳遞復(fù)雜的參數(shù)(例如數(shù)組)就很麻煩了。通過函數(shù)名可以定位到 js 函數(shù),并調(diào)用它,支持傳遞復(fù)雜的參數(shù)。下面是一個(gè)簡單的例子:

首先我們定義個(gè) js 全局函數(shù):

function add(a, b){
    return a + b
}

然后在 java(kotlin) 中執(zhí)行它:

val arg = V8Array(v8).push(12).push(21) // 創(chuàng)建參數(shù)數(shù)組
val r = v8.executeIntegerFunction("add", arg) // 調(diào)用函數(shù)
arg.close() //別忘記釋放對(duì)象
// r = 33

這樣,通過創(chuàng)建一個(gè)數(shù)組,我們可以傳各種各樣的參數(shù)了。

3. 通過 Function 對(duì)象調(diào)用

在 js 中萬物皆對(duì)象,函數(shù)也不例外。在 j2v8 中,一切 js 對(duì)象都用 V8Object 表示,我們可以直接將其強(qiáng)制轉(zhuǎn)換為 V8Function。V8Function 表示的就是一個(gè) js 函數(shù)對(duì)象,它擁有 call() 方法可以直接被調(diào)用。

if (v8.getType("add") == V8.V8_FUNCTION){ // 先判斷 add 是不是一個(gè)函數(shù)
    val arg = V8Array(v8).push(12).push(21)
    val call = v8.getObject("add") as V8Function // 取得函數(shù)對(duì)象
    val r = call.call(null, arg) // 調(diào)用它
    arg.close()
    call.close()
}

乍一看此方法似乎比方法二要麻煩,確實(shí)一般情況下我們都采用方法二。但試想這樣一個(gè)需求:js 先調(diào)用 java 函數(shù),并傳入一個(gè)類型為 js 函數(shù)的參數(shù)作為回調(diào)。那么此時(shí)方法三就可以大顯身手了。


上面調(diào)用的都是 js 全局函數(shù),所以調(diào)用者都是 v8,也就是運(yùn)行時(shí)本身。如果要調(diào)用對(duì)象的成員函數(shù)呢?也很簡單,只要先通過 val obj = v8.getObject() 獲取到對(duì)象,并使用 obj.executeIntegerFunction() 就可以啦。同理,如果要調(diào)用對(duì)象的對(duì)象的成員函數(shù),只要再獲取一層對(duì)象就好。

其實(shí) V8 本身就繼承自 V8Object,這更體現(xiàn)了 js 萬物皆對(duì)象的特性。運(yùn)行時(shí)自身可以被理解為是一個(gè)根對(duì)象。

Js 調(diào)用 Java (注冊(cè) Java 回調(diào))

Js 不可以直接調(diào)用 Java 的函數(shù),一般來說得先把 Java 函數(shù)注冊(cè)到 js 才可以。本節(jié)部分內(nèi)容來自官方文檔。

接口方式

要注冊(cè)函數(shù)到 js,首先要?jiǎng)?chuàng)建一個(gè)類,并實(shí)現(xiàn) JavaCallback 接口(如果 java 函數(shù)沒有返回值,則實(shí)現(xiàn) JavaVoidCallback 接口即可)。這兩個(gè)接口均有一個(gè) invoke(V8Object receiver, V8Array parameters) 函數(shù),當(dāng)被 js 調(diào)用時(shí)就會(huì)觸發(fā),這樣就可執(zhí)行 java 代碼啦。

  • parameters 是傳入的參數(shù)列表,表現(xiàn)為一個(gè) V8數(shù)組,可以從中提取 js 傳入的各個(gè)參數(shù)。
  • receiver: 是此函數(shù)被調(diào)用時(shí)所基于的對(duì)象。看下面一個(gè)例子:

js:

var array1 = [{first:'Ian'}, {first:'Jordi'}, {first:'Holger'}];
for ( var i = 0; i < array1.length; i++ ) {
  print.call(array1[i], " says Hi."); // print 是 java 注冊(cè)到 js 的一個(gè)函數(shù)
}

java:

JavaVoidCallback callback = new JavaVoidCallback() {
    @Override
    public void invoke(final V8Object receiver, final V8Array parameters) {
        System.out.println(receiver.getString("first") + parameters.get(0));
    }
}

v8.registerJavaMethod(callback, "print") // 注冊(cè)到 js 全局函數(shù),函數(shù)名為 `print`

在這個(gè)例子中,print 不再基于 Global,而是基于 array1[i] 被調(diào)用的,因此 array1[i] 將被傳入 java 作為 receiver. 最終 java 將依次輸出 Ian says Hi., Jordi says Hi., Holger says Hi..

反射方式

接口方式有些麻煩,必須將使用一個(gè)類包裝一下,并實(shí)例化再注冊(cè)到 js(或創(chuàng)建匿名類)。

下面是一個(gè)簡單的例子:

class Console {
     public void log(final String message) {
          System.out.println("[INFO] " + message);
     }
     public void error(final String message) {
          System.out.println("[ERROR] " + message);
     }
}

public void start() {
     Console console = new Console();
     V8Object v8Console = new V8Object(v8);
     v8.add("console", v8Console);
     v8Console.registerJavaMethod(console, "log", "jlog", new Class<?>[] { String.class });
     v8Console.registerJavaMethod(console, "err", "jerr", new Class<?>[] { String.class });
     v8Console.release();
     v8.executeScript("console.jlog('hello, world');");
}

// 然后可以直接在 js 中調(diào)用 `console.jlog('hello, world')` 與 `console.jerr('hello, world')` 了。

在本例中,通過反射注冊(cè)了現(xiàn)有 java 對(duì)象的方法。必須指定Java對(duì)象、方法的名稱和參數(shù)列表。并且此處不是直接注冊(cè)為全局函數(shù),而是先創(chuàng)建了一個(gè)名為 console 的 js 對(duì)象,把函數(shù)注冊(cè)到了此對(duì)象上,

不難發(fā)現(xiàn),通過接口方式注冊(cè),參數(shù)可以是動(dòng)態(tài)的;而通過反射注冊(cè),參數(shù)必須明確指定并且與 java 參數(shù)嚴(yán)格匹配,若參數(shù)不匹配則會(huì)異常。

無需注冊(cè)

在 j2v8 V4 中,增加了新的方式,可以無需注冊(cè)將 js 執(zhí)行 java 函數(shù)。

在之前的例子中,java 函數(shù)必須先注冊(cè)到 js 才可以被調(diào)用。試想這樣一個(gè)需求:js 有一個(gè)函數(shù),需要傳入一個(gè)回調(diào)函數(shù)。此時(shí)希望 java 來執(zhí)行此函數(shù),那么參數(shù)應(yīng)該怎么傳遞呢?之前,是沒有辦法,但是現(xiàn)在有了。

js:

function add(a, b, callback){
    callback(a + b)
}
// 我承認(rèn)這個(gè) callback 有點(diǎn)畫蛇添足,這只是為了演示。
// 現(xiàn)在我們要從 java 中調(diào)用這個(gè) add()

j2v8 V4 中新增了 V8Function 類。其實(shí)這個(gè)類我們之前在 java 調(diào)用 js 的時(shí)候已經(jīng)用過了。只不過那時(shí)候是從 js 獲得的,現(xiàn)在我們要自己創(chuàng)建它。

java(kotlin):

val callback = V8Function(v8,
        { receiver: V8Object, parameters: V8Array -> System.out.println(parameters.getInteger(0)) })
val arg = V8Array(v8).push(1).push(2).push(callback)
v8.executeVoidFunction("add", arg)

在這個(gè)例子中,我們直接使用 lambda 表達(dá)式創(chuàng)建了一個(gè) V8Function,作為參數(shù)傳給了 js,然后 js 在執(zhí)行 callback 的時(shí)候也就執(zhí)行了 java 代碼。最終將輸出3。當(dāng)然你也可以使用 v8.add("print", callback) 直接注冊(cè)為 js 全局函數(shù),相比之前的方案要簡潔不少。


好啦,基本上就說到這了。通過這些例子,相信對(duì)于更加基本的 js 對(duì)象操作、數(shù)組操作也已經(jīng)輕車熟路了。用到什么新的再補(bǔ)充吧。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,656評(píng)論 1 32
  • 所有知識(shí)點(diǎn)已整理成app app下載地址 J2EE 部分: 1.Switch能否用string做參數(shù)? 在 Jav...
    侯蛋蛋_閱讀 2,710評(píng)論 1 4
  • V8的前世今生 V8是JavaScript渲染引擎,第一個(gè)版本隨著Chrome的發(fā)布而發(fā)布(具體時(shí)間為2008年9...
    燕京博士閱讀 2,965評(píng)論 1 3
  • 一、簡歷準(zhǔn)備 1、個(gè)人技能 (1)自定義控件、UI設(shè)計(jì)、常用動(dòng)畫特效 自定義控件 ①為什么要自定義控件? Andr...
    lucas777閱讀 5,388評(píng)論 2 54
  • 概要 64學(xué)時(shí) 3.5學(xué)分 章節(jié)安排 電子商務(wù)網(wǎng)站概況 HTML5+CSS3 JavaScript Node 電子...
    阿啊阿吖丁閱讀 9,851評(píng)論 0 3

友情鏈接更多精彩內(nèi)容