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, gunakan atribut Library JavaScriptEngine memiliki keuntungan berikut:
Konsumsi resource lebih rendah, karena tidak perlu mengalokasikan WebView di instance Compute Engine.
Dapat dilakukan di Service (tugas WorkManager).
Beberapa lingkungan terisolasi dengan overhead rendah, yang memungkinkan aplikasi untuk menjalankan beberapa cuplikan JavaScript secara bersamaan.
Kemampuan untuk meneruskan data dalam jumlah besar dengan menggunakan panggilan API.
Penggunaan Dasar
Untuk memulai, buat instance JavaScriptSandbox
. Hal ini mewakili
koneksi ke mesin JavaScript di luar proses.
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceAsync(context);
Sebaiknya sejajarkan siklus proses sandbox dengan yang memerlukan evaluasi JavaScript.
Misalnya, komponen yang menghosting sandbox mungkin 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. Channel
IllegalStateException
ditampilkan saat aplikasi mencoba mengalokasikan
instance JavaScriptSandbox
kedua. Namun, jika beberapa lingkungan eksekusi
diperlukan, beberapa instance JavaScriptIsolate
dapat dialokasikan.
Jika tidak lagi digunakan, tutup instance sandbox untuk mengosongkan resource. Tujuan
Instance JavaScriptSandbox
mengimplementasikan antarmuka AutoCloseable
, yang
memungkinkan penggunaan try-with-resources 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 dieksekusi
kode JavaScript. Kunci keamanan dapat dialokasikan jika diperlukan, sehingga memberikan keamanan yang lemah
batas untuk skrip dengan asal yang berbeda atau pengaktifan JavaScript serentak
eksekusi karena JavaScript pada dasarnya
berbentuk thread tunggal. Panggilan berikutnya ke
instance yang sama memiliki status yang sama, sehingga beberapa data dapat dibuat
terlebih dahulu, lalu proses 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) akan menghasilkan IsolateTerminatedException
. Tujuan
atau isolasi dibersihkan selanjutnya di latar belakang jika
mendukung JS_FEATURE_ISOLATE_TERMINATION
, sebagaimana dijelaskan dalam
menangani error sandbox nanti di bagian ini
kami. Jika tidak, pembersihan akan ditunda hingga semua evaluasi yang tertunda
selesai atau {i>sandbox<i} ditutup.
Aplikasi dapat membuat dan mengakses instance JavaScriptIsolate
dari
thread apa pun.
Sekarang, aplikasi siap mengeksekusi 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
hasil ekspresi terakhir dalam kode JavaScript. Ini harus
dari jenis 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 menjadi String
) mungkin dimungkinkan.
Library ini juga mendukung evaluasi skrip dalam bentuk
AssetFileDescriptor
atau ParcelFileDescriptor
. Lihat
evaluateJavaScriptAsync(AssetFileDescriptor)
dan
evaluateJavaScriptAsync(ParcelFileDescriptor)
untuk detail selengkapnya.
API ini lebih cocok untuk mengevaluasi dari file pada disk atau dalam aplikasi
direktori.
Library ini juga mendukung logging konsol yang dapat digunakan untuk proses debug
tujuan. Hal ini dapat disiapkan menggunakan setConsoleCallback()
.
Karena konteksnya tetap ada, Anda dapat mengupload kode dan mengeksekusinya 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 persisten, sehingga Anda dapat melanjutkan langkah sebelumnya cuplikan 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 dan mengeksekusi kode JavaScript mungkin akan 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 Anda menggunakan fitur coba dengan sumber daya untuk memastikan
resource dilepaskan
dan tidak digunakan lagi. Menutup hasil sandbox
dalam semua evaluasi yang tertunda di semua instance JavaScriptIsolate
gagal
dengan SandboxDeadException
. Saat evaluasi JavaScript menemukan
error, berarti JavaScriptException
akan dibuat. Mengacu pada subclass-nya
untuk pengecualian yang lebih spesifik.
Menangani Error Sandbox
Semua JavaScript dieksekusi dalam proses sandbox terpisah dari proses utama aplikasi Anda. Jika kode JavaScript menyebabkan proses dalam sandbox ini hingga tidak bekerja, misalnya, karena kehabisan batas memori, tidak akan terpengaruh.
Error sandbox akan menyebabkan semua isolasi dalam sandbox tersebut dihentikan. Paling sering
gejala yang jelas dari hal ini adalah bahwa
semua evaluasi akan mulai gagal dengan
IsolateTerminatedException
Tergantung pada situasinya,
pengecualian khusus seperti SandboxDeadException
atau
MemoryLimitExceededException
dapat ditampilkan.
Menangani error untuk masing-masing evaluasi tidak selalu praktis.
Selain itu, isolasi dapat dihentikan di luar permintaan eksplisit
evaluasi karena tugas latar belakang atau evaluasi di isolasi lain. Tabrakan
logika penanganan dapat dipusatkan dengan melampirkan 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, implementasi sandbox mungkin memiliki
set fitur yang berbeda. Jadi, Anda perlu melakukan kueri masing-masing
fitur menggunakan JavaScriptSandbox.isFeatureSupported(...)
. Penting
untuk memeriksa status fitur sebelum memanggil metode yang mengandalkan fitur ini.
Metode JavaScriptIsolate
yang mungkin tidak tersedia di semua tempat
dianotasi dengan RequiresFeature
, sehingga lebih mudah untuk menemukan
memanggil dalam kode.
Parameter Penerusan
Jika JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT
adalah
didukung, permintaan evaluasi yang dikirim ke mesin JavaScript tidak terikat
oleh batas transaksi binder. Jika fitur ini tidak didukung, semua data akan
JavaScriptEngine terjadi melalui transaksi Binder. Persyaratan
batas ukuran transaksi berlaku untuk setiap panggilan yang meneruskan data atau
menampilkan data.
Respons akan selalu ditampilkan sebagai String dan tunduk pada Binder
batas ukuran maksimum transaksi jika
JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT
bukan
didukung. Nilai non-string harus dikonversi secara eksplisit ke String JavaScript
jika tidak,
string kosong akan ditampilkan. Jika JS_FEATURE_PROMISE_RETURN
didukung, kode JavaScript mungkin juga akan menampilkan Promise
me-resolve 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
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, kemudian dikompilasi dan dieksekusi dengan cara biasa, seperti yang ditunjukkan di bawah ini.
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 JavaScriptIsolate
Semua instance JavaScriptIsolate
tidak saling bergantung dan tidak
bagikan apa saja. 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 ditunjukkan di bawah ini:
// 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 instance
konfigurasi Anda. Untuk mengubah konfigurasi, teruskan
IsolateStartupParameters untuk
JavaScriptSandbox.createIsolate(...)
Parameter saat ini memungkinkan penetapan ukuran heap maksimum dan ukuran maksimum untuk evaluasi error dan nilai return.