Evaluasi JavaScript
Library Jetpack JavaScriptEngine menyediakan cara bagi aplikasi untuk mengevaluasi kode JavaScript tanpa membuat instance WebView.
Untuk aplikasi yang memerlukan evaluasi JavaScript non-interaktif, penggunaan library JavaScriptEngine memiliki keuntungan berikut:
Konsumsi resource lebih rendah karena tidak perlu mengalokasikan instance WebView.
Dapat dilakukan di Layanan (tugas WorkManager).
Beberapa lingkungan terisolasi dengan overhead rendah, sehingga memungkinkan aplikasi menjalankan beberapa cuplikan JavaScript secara bersamaan.
Kemampuan untuk meneruskan data dalam jumlah besar dengan menggunakan panggilan API.
Penggunaan Dasar
Untuk memulai, buat instance JavaScriptSandbox
. Ini mewakili
koneksi ke mesin JavaScript di luar proses.
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceAsync(context);
Sebaiknya selaraskan siklus proses sandbox dengan siklus proses komponen yang memerlukan evaluasi JavaScript.
Misalnya, komponen yang menghosting sandbox dapat berupa Activity
atau
Service
. Satu Service
dapat digunakan untuk mengenkapsulasi evaluasi JavaScript
untuk semua komponen aplikasi.
Pertahankan instance JavaScriptSandbox
karena alokasinya cukup
mahal. Hanya satu instance JavaScriptSandbox
per aplikasi yang diizinkan. IllegalStateException
ditampilkan saat aplikasi mencoba mengalokasikan
instance JavaScriptSandbox
kedua. Namun, jika diperlukan beberapa lingkungan eksekusi, beberapa instance JavaScriptIsolate
dapat dialokasikan.
Jika tidak lagi digunakan, tutup instance sandbox untuk membebaskan resource. Instance
JavaScriptSandbox
mengimplementasikan antarmuka AutoCloseable
, yang
memungkinkan penggunaan try-with-resource untuk kasus penggunaan pemblokiran sederhana.
Atau, pastikan siklus proses instance JavaScriptSandbox
dikelola oleh komponen hosting, menutupnya dalam callback onStop()
untuk Aktivitas atau selama onDestroy()
untuk Layanan:
jsSandbox.close();
Instance JavaScriptIsolate
mewakili konteks untuk mengeksekusi
kode JavaScript. Library ini dapat dialokasikan jika diperlukan, sehingga memberikan batas keamanan yang lemah untuk skrip dengan origin yang berbeda atau memungkinkan eksekusi JavaScript serentak karena pada dasarnya JavaScript bersifat thread tunggal. Panggilan berikutnya ke
instance yang sama memiliki status yang sama, sehingga Anda dapat membuat beberapa data
terlebih dahulu lalu memprosesnya nanti dalam instance JavaScriptIsolate
yang sama.
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate();
Rilis JavaScriptIsolate
secara eksplisit dengan memanggil metode close()
.
Menutup instance isolasi yang menjalankan kode JavaScript
(memiliki Future
yang tidak lengkap) menghasilkan IsolateTerminatedException
. Isolasi
selanjutnya akan dibersihkan di latar belakang jika implementasi tersebut
mendukung JS_FEATURE_ISOLATE_TERMINATION
, seperti yang dijelaskan di bagian
menangani error sandbox nanti di
halaman ini. Jika tidak, pembersihan akan ditunda hingga semua evaluasi yang tertunda selesai atau sandbox ditutup.
Aplikasi dapat membuat dan mengakses instance JavaScriptIsolate
dari
thread mana pun.
Sekarang, aplikasi siap menjalankan beberapa kode JavaScript:
final String code = "function sum(a, b) { let r = a + b; return r.toString(); }; sum(3, 4)";
ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code);
String result = resultFuture.get(5, TimeUnit.SECONDS);
Cuplikan JavaScript yang sama diformat dengan baik:
function sum(a, b) {
let r = a + b;
return r.toString(); // make sure we return String instance
};
// Calculate and evaluate the expression
// NOTE: We are not in a function scope and the `return` keyword
// should not be used. The result of the evaluation is the value
// the last expression evaluates to.
sum(3, 4);
Cuplikan kode diteruskan sebagai String
dan hasilnya dikirim sebagai String
.
Perhatikan bahwa memanggil evaluateJavaScriptAsync()
akan menampilkan hasil evaluasi ekspresi terakhir dalam kode JavaScript. Jenis nilai ini harus
berjenis String
JavaScript; jika tidak, API library akan menampilkan nilai kosong.
Kode JavaScript tidak boleh menggunakan kata kunci return
. Jika sandbox
mendukung fitur tertentu, jenis nilai yang ditampilkan tambahan (misalnya, Promise
yang di-resolve ke String
) mungkin dapat dilakukan.
Library ini juga mendukung evaluasi skrip yang berbentuk
AssetFileDescriptor
atau ParcelFileDescriptor
. Lihat
evaluateJavaScriptAsync(AssetFileDescriptor)
dan
evaluateJavaScriptAsync(ParcelFileDescriptor)
untuk detail selengkapnya.
API ini lebih cocok untuk mengevaluasi dari file pada disk atau dalam direktori
aplikasi.
Library ini juga mendukung logging konsol yang dapat digunakan untuk tujuan
proses debug. Ini dapat disiapkan menggunakan setConsoleCallback()
.
Karena konteksnya tetap ada, Anda dapat mengupload kode dan menjalankannya beberapa kali
selama masa aktif JavaScriptIsolate
:
String jsFunction = "function sum(a, b) { let r = a + b; return r.toString(); }";
ListenableFuture<String> func = js.evaluateJavaScriptAsync(jsFunction);
String twoPlusThreeCode = "let five = sum(2, 3); five";
ListenableFuture<String> r1 = Futures.transformAsync(func,
input -> js.evaluateJavaScriptAsync(twoPlusThreeCode)
, executor);
String twoPlusThree = r1.get(5, TimeUnit.SECONDS);
String fourPlusFiveCode = "sum(4, parseInt(five))";
ListenableFuture<String> r2 = Futures.transformAsync(func,
input -> js.evaluateJavaScriptAsync(fourPlusFiveCode)
, executor);
String fourPlusFive = r2.get(5, TimeUnit.SECONDS);
Tentu saja, variabel juga bersifat persisten, sehingga Anda dapat melanjutkan cuplikan sebelumnya dengan:
String defineResult = "let result = sum(11, 22);";
ListenableFuture<String> r3 = Futures.transformAsync(func,
input -> js.evaluateJavaScriptAsync(defineResult)
, executor);
String unused = r3.get(5, TimeUnit.SECONDS);
String obtainValue = "result";
ListenableFuture<String> r4 = Futures.transformAsync(func,
input -> js.evaluateJavaScriptAsync(obtainValue)
, executor);
String value = r4.get(5, TimeUnit.SECONDS);
Misalnya, cuplikan lengkap untuk mengalokasikan semua objek yang diperlukan dan mengeksekusi kode JavaScript mungkin terlihat seperti berikut:
final ListenableFuture<JavaScriptSandbox> sandbox
= JavaScriptSandbox.createConnectedInstanceAsync(this);
final ListenableFuture<JavaScriptIsolate> isolate
= Futures.transform(sandbox,
input -> (jsSandBox = input).createIsolate(),
executor);
final ListenableFuture<String> js
= Futures.transformAsync(isolate,
isolate -> (jsIsolate = isolate).evaluateJavaScriptAsync("'PASS OK'"),
executor);
Futures.addCallback(js,
new FutureCallback<String>() {
@Override
public void onSuccess(String result) {
text.append(result);
}
@Override
public void onFailure(Throwable t) {
text.append(t.getMessage());
}
},
mainThreadExecutor);
Sebaiknya gunakan try-with-resources untuk memastikan semua resource
yang dialokasikan dirilis dan tidak lagi digunakan. Jika sandbox ditutup, semua evaluasi yang tertunda di semua instance JavaScriptIsolate
akan gagal dengan SandboxDeadException
. Saat evaluasi JavaScript mengalami
error, JavaScriptException
akan dibuat. Lihat subclass-nya
untuk pengecualian yang lebih spesifik.
Menangani Error Sandbox
Semua JavaScript dijalankan dalam proses sandbox terpisah yang berbeda dari proses utama aplikasi Anda. Jika kode JavaScript menyebabkan proses yang di-sandbox ini error, misalnya karena batas memori habis, proses utama aplikasi tidak akan terpengaruh.
Error sandbox akan menyebabkan semua isolasi di sandbox tersebut dihentikan. Gejala yang paling
jelas dari hal ini adalah semua evaluasi akan mulai gagal dengan
IsolateTerminatedException
. Bergantung pada situasinya, pengecualian
yang lebih spesifik seperti SandboxDeadException
atau
MemoryLimitExceededException
dapat ditampilkan.
Menangani error untuk setiap evaluasi tidak selalu praktis.
Selain itu, isolasi dapat berhenti di luar evaluasi yang diminta
secara eksplisit karena ada tugas latar belakang atau evaluasi pada isolasi lain. Logika
penanganan error dapat dipusatkan dengan menambahkan callback menggunakan
JavaScriptIsolate.addOnTerminatedCallback()
.
final ListenableFuture<JavaScriptSandbox> sandboxFuture =
JavaScriptSandbox.createConnectedInstanceAsync(this);
final ListenableFuture<JavaScriptIsolate> isolateFuture =
Futures.transform(sandboxFuture, sandbox -> {
final IsolateStartupParameters startupParams = new IsolateStartupParameters();
if (sandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE)) {
startupParams.setMaxHeapSizeBytes(100_000_000);
}
return sandbox.createIsolate(startupParams);
}, executor);
Futures.transform(isolateFuture,
isolate -> {
// Add a crash handler
isolate.addOnTerminatedCallback(executor, terminationInfo -> {
Log.e(TAG, "The isolate crashed: " + terminationInfo);
});
// Cause a crash (eventually)
isolate.evaluateJavaScriptAsync("Array(1_000_000_000).fill(1)");
return null;
}, executor);
Fitur Sandbox Opsional
Bergantung pada versi WebView yang mendasarinya, penerapan sandbox mungkin memiliki serangkaian fitur yang tersedia. Jadi, Anda perlu mengkueri setiap fitur
yang diperlukan menggunakan JavaScriptSandbox.isFeatureSupported(...)
. Penting
untuk memeriksa status fitur sebelum memanggil metode yang mengandalkan fitur ini.
Metode JavaScriptIsolate
yang mungkin tidak tersedia di mana saja
dianotasi dengan anotasi RequiresFeature
, sehingga lebih mudah untuk menemukan
panggilan ini dalam kode.
Meneruskan Parameter
Jika JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT
didukung, permintaan evaluasi yang dikirim ke mesin JavaScript tidak terikat
oleh batas transaksi binder. Jika fitur ini tidak didukung, semua data ke JavaScriptEngine akan terjadi melalui transaksi Binder. Batas ukuran transaksi umum berlaku untuk setiap panggilan yang meneruskan data atau menampilkan data.
Respons selalu ditampilkan sebagai String dan tunduk pada batas ukuran maksimum
transaksi Binder jika
JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT
tidak
didukung. Nilai non-string harus dikonversi secara eksplisit ke String JavaScript
jika tidak, string kosong akan ditampilkan. Jika fitur JS_FEATURE_PROMISE_RETURN
didukung, kode JavaScript dapat menampilkan penyelesaian Promise ke String
.
Untuk meneruskan array byte besar ke instance JavaScriptIsolate
, Anda
dapat menggunakan provideNamedData(...)
API. Penggunaan API ini tidak terikat oleh
batas transaksi Binder. Setiap array byte harus diteruskan menggunakan ID
unik yang tidak dapat digunakan kembali.
if (sandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROVIDE_CONSUME_ARRAY_BUFFER)) {
js.provideNamedData("data-1", "Hello Android!".getBytes(StandardCharsets.US_ASCII));
final String jsCode = "android.consumeNamedDataAsArrayBuffer('data-1').then((value) => { return String.fromCharCode.apply(null, new Uint8Array(value)); });";
ListenableFuture<String> msg = js.evaluateJavaScriptAsync(jsCode);
String response = msg.get(5, TimeUnit.SECONDS);
}
Menjalankan Kode Wasm
Kode WebAssembly (Wasm) dapat diteruskan menggunakan provideNamedData(...)
API, lalu dikompilasi dan dieksekusi dengan cara biasa, seperti yang ditunjukkan di bawah.
final byte[] hello_world_wasm = {
0x00 ,0x61 ,0x73 ,0x6d ,0x01 ,0x00 ,0x00 ,0x00 ,0x01 ,0x0a ,0x02 ,0x60 ,0x02 ,0x7f ,0x7f ,0x01,
0x7f ,0x60 ,0x00 ,0x00 ,0x03 ,0x03 ,0x02 ,0x00 ,0x01 ,0x04 ,0x04 ,0x01 ,0x70 ,0x00 ,0x01 ,0x05,
0x03 ,0x01 ,0x00 ,0x00 ,0x06 ,0x06 ,0x01 ,0x7f ,0x00 ,0x41 ,0x08 ,0x0b ,0x07 ,0x18 ,0x03 ,0x06,
0x6d ,0x65 ,0x6d ,0x6f ,0x72 ,0x79 ,0x02 ,0x00 ,0x05 ,0x74 ,0x61 ,0x62 ,0x6c ,0x65 ,0x01 ,0x00,
0x03 ,0x61 ,0x64 ,0x64 ,0x00 ,0x00 ,0x09 ,0x07 ,0x01 ,0x00 ,0x41 ,0x00 ,0x0b ,0x01 ,0x01 ,0x0a,
0x0c ,0x02 ,0x07 ,0x00 ,0x20 ,0x00 ,0x20 ,0x01 ,0x6a ,0x0b ,0x02 ,0x00 ,0x0b,
};
final String jsCode = "(async ()=>{" +
"const wasm = await android.consumeNamedDataAsArrayBuffer('wasm-1');" +
"const module = await WebAssembly.compile(wasm);" +
"const instance = WebAssembly.instance(module);" +
"return instance.exports.add(20, 22).toString();" +
"})()";
// Ensure that the name has not been used before.
js.provideNamedData("wasm-1", hello_world_wasm);
FluentFuture.from(js.evaluateJavaScriptAsync(jsCode))
.transform(this::println, mainThreadExecutor)
.catching(Throwable.class, e -> println(e.getMessage()), mainThreadExecutor);
}
Pemisahan JavaScript
Semua instance JavaScriptIsolate
tidak saling bergantung dan tidak
membagikan apa pun. Cuplikan berikut menghasilkan
Hi from AAA!5
dan
Uncaught Reference Error: a is not defined
karena instance ”jsTwo
” tidak memiliki visibilitas objek yang dibuat di
“jsOne
”.
JavaScriptIsolate jsOne = engine.obtainJavaScriptIsolate();
String jsCodeOne = "let x = 5; function a() { return 'Hi from AAA!'; } a() + x";
JavaScriptIsolate jsTwo = engine.obtainJavaScriptIsolate();
String jsCodeTwo = "a() + x";
FluentFuture.from(jsOne.evaluateJavaScriptAsync(jsCodeOne))
.transform(this::println, mainThreadExecutor)
.catching(Throwable.class, e -> println(e.getMessage()), mainThreadExecutor);
FluentFuture.from(jsTwo.evaluateJavaScriptAsync(jsCodeTwo))
.transform(this::println, mainThreadExecutor)
.catching(Throwable.class, e -> println(e.getMessage()), mainThreadExecutor);
Dukungan Kotlin
Untuk menggunakan library Jetpack ini dengan coroutine Kotlin, tambahkan dependensi ke
kotlinx-coroutines-guava
. Hal ini memungkinkan integrasi dengan
ListenableFuture
.
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.6.0"
}
API library Jetpack kini dapat dipanggil dari cakupan coroutine, seperti yang ditunjukkan di bawah:
// Launch a coroutine
lifecycleScope.launch {
val jsSandbox = JavaScriptSandbox
.createConnectedInstanceAsync(applicationContext)
.await()
val jsIsolate = jsSandbox.createIsolate()
val resultFuture = jsIsolate.evaluateJavaScriptAsync("PASS")
// Await the result
textBox.text = resultFuture.await()
// Or add a callback
Futures.addCallback<String>(
resultFuture, object : FutureCallback<String?> {
override fun onSuccess(result: String?) {
textBox.text = result
}
override fun onFailure(t: Throwable) {
// Handle errors
}
},
mainExecutor
)
}
Parameter Konfigurasi
Saat meminta instance lingkungan yang terisolasi, Anda dapat menyesuaikan konfigurasinya. Untuk mengubah konfigurasi, teruskan instance
IsolateStartupParameters ke
JavaScriptSandbox.createIsolate(...)
.
Saat ini, parameter memungkinkan penentuan ukuran heap maksimum dan ukuran maksimum untuk evaluasi menampilkan nilai dan error.