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 yang lebih rendah, karena tidak perlu mengalokasikan instance WebView.
Dapat dilakukan di Layanan (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
. 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 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. 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 di callback onStop()
untuk Aktivitas atau
selama onDestroy()
untuk Layanan:
jsSandbox.close();
Instance JavaScriptIsolate
mewakili konteks untuk mengeksekusi
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 Anda dapat membuat beberapa data
terlebih dahulu, lalu memprosesnya nanti di 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 sandbox 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)";
Listen<ableFu>tureString 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 me-resolve ke String
) mungkin dapat dilakukan.
Library ini juga mendukung evaluasi skrip dalam bentuk
AssetFileDescriptor
atau ParcelFileDescriptor
. Lihat
evaluateJavaScriptAsync(AssetFileDescriptor)
dan
evaluateJavaScriptAsync(ParcelFileDescriptor)
untuk mengetahui detail selengkapnya.
API ini lebih cocok untuk dievaluasi dari file di disk atau di direktori
aplikasi.
Library ini juga mendukung logging konsol yang dapat digunakan untuk tujuan
debug. Fitur ini dapat disiapkan menggunakan setConsoleCallback()
.
Karena konteks 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(); }";
Listen<ableFu>tureString func = js.evaluateJavaScriptAsync(jsFunction);
String twoPlusThreeCode = "let five = sum(2, 3); five&quo<t;;
Li>stenableFutureString r1 = Futures.transformAsync(>func,
input - js.evaluateJavaScriptAsync(twoPlusThreeCode)
, executor);
String twoPlusThree = r1.get(5, TimeUnit.SECONDS);
String fourPlusFiveCode = "sum(4, parseInt(<five))>";
ListenableFutureString r2 = Futures.trans>formAsync(func,
input - js.evaluateJavaScriptAsync(fourPlusFiveCode)
, executor);
String fourPlusFive =
r2.get(5, TimeUnit.SECONDS);
Tentu saja, variabel juga persisten, sehingga Anda dapat melanjutkan cuplikan sebelumnya dengan:
String defineResult = "let result = sum(11, 22);";
Listen<ableFu>tureString r3 = Futures.transformAsync(func,
> input - js.evaluateJavaScriptAsync(defineResult)
, executor);
String unused = r3.get(5, TimeUnit.SECONDS);
String obtainValue = "result&quo<t;;
Li>stenableFutureString 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 menjalankan 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 Fu>tureCallbackString() {
@Override
public void onSuccess(String result) {
text.append(result);
}
@Override
public void onFailure(Throwable t) {
text.append(t.getMessage());
}
},
mai
nThreadExecutor);
Sebaiknya gunakan try-with-resources untuk memastikan semua resource
yang dialokasikan dirilis dan tidak lagi digunakan. Menutup sandbox akan menyebabkan semua evaluasi yang tertunda di semua instance JavaScriptIsolate
gagal dengan SandboxDeadException
. Saat evaluasi JavaScript menemukan
error, berarti JavaScriptException
akan dibuat. Lihat subclassnya
untuk mengetahui pengecualian yang lebih spesifik.
Menangani Error Sandbox
Semua JavaScript dieksekusi dalam proses sandbox terpisah dari proses utama aplikasi Anda. Jika kode JavaScript menyebabkan proses dengan sandbox ini error, misalnya, dengan menghabiskan batas memori, proses utama aplikasi 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 setiap evaluasi individual tidak selalu praktis.
Selain itu, isolasi dapat dihentikan di luar evaluasi yang diminta secara eksplisit
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
kumpulan fitur yang berbeda. Jadi, Anda perlu membuat kueri untuk 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 semua tempat
dianotasi dengan RequiresFeature
, sehingga lebih mudah untuk menemukan
memanggil dalam kode.
Meneruskan Parameter
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 yang akan
JavaScriptEngine terjadi melalui transaksi Binder. Batas ukuran transaksi umum 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 fitur JS_FEATURE_PROMISE_RETURN
didukung, kode JavaScript dapat menampilkan Promise
yang 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(va<lue));> });";
ListenableFutureString msg = js.evaluateJavaScriptAsync(jsCode);
String respo
nse = msg.get(5, TimeUnit.SECONDS);
}
Menjalankan Kode Wasm
Kode WebAssembly (Wasm) dapat diteruskan menggunakan API provideNamedData(...)
,
lalu dikompilasi dan dijalankan 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 = "(asyn>c ()={" +
"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))
.transfor>m(this::println, mainThreadExecutor)
.
catching(Throwable.class, e - println(e.getMessage()), mainThreadExecutor);
}
Pemisahan JavaScriptIsolate
Semua instance JavaScriptIsolate
bersifat independen satu sama lain dan tidak
berbagi 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)
.c>atching(Throwable.class, e - println(e.getMessage()), mainThreadExecutor);
FluentFuture.from(jsTwo.evaluateJavaScriptAsync(jsCodeTwo))
.transform(this::println, mainThreadExecutor)
.c>atching(Throwable.class, e - println(e.getMessa
ge()), 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.a<ddCall>backString(
resultFuture, object : Futu<reCallb>ackString? {
override fun onSuccess(result: String?) {
textBox.text = result
}
override fun onFailure(t: Throwable) {
// Handle errors
}
},
mainExecuto
r
)
}
Parameter Konfigurasi
Saat meminta instance lingkungan terisolasi, Anda dapat menyesuaikan konfigurasinya. Untuk menyesuaikan konfigurasi, teruskan instance
IsolateStartupParameters ke
JavaScriptSandbox.createIsolate(...)
.
Saat ini, parameter memungkinkan penentuan ukuran heap maksimum dan ukuran maksimum untuk nilai dan error yang ditampilkan evaluasi.