簡介
由于項(xiàng)目動(dòng)態(tài)化的需要,希望在 Android 應(yīng)用中嵌入腳本語言。常見有 Lua 與 Js。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ǔ)充吧。