การประเมิน JavaScript
ไลบรารี Jetpack JavaScriptEngine ช่วยให้แอปพลิเคชันประเมินโค้ด JavaScript ได้โดยไม่ต้องสร้างอินสแตนซ์ WebView
สําหรับแอปพลิเคชันที่ต้องการประเมิน JavaScript แบบไม่โต้ตอบ การใช้ไลบรารี JavaScriptEngine มีข้อดีดังนี้
ใช้ทรัพยากรน้อยลง เนื่องจากไม่จำเป็นต้องจัดสรร WebView อินสแตนซ์
ซึ่งทำได้ใน Service (งาน WorkManager)
สภาพแวดล้อมแบบแยกหลายรายการที่มีค่าใช้จ่ายเพิ่มเติมต่ำ ซึ่งช่วยให้แอปพลิเคชันเรียกใช้สnippet JavaScript หลายรายการพร้อมกันได้
ความสามารถในการส่งข้อมูลจํานวนมากโดยใช้การเรียก API
การใช้งานพื้นฐาน
เริ่มต้นด้วยการสร้างอินสแตนซ์ของ JavaScriptSandbox
ซึ่งแสดงถึงการเชื่อมต่อกับเครื่องมือ JavaScript นอกกระบวนการ
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceAsync(context);
ขอแนะนำให้ปรับวงจรของแซนด์บ็อกซ์ให้สอดคล้องกับวงจรของ ที่ต้องมีการประเมิน JavaScript
เช่น คอมโพเนนต์ที่โฮสต์แซนด์บ็อกซ์อาจเป็น Activity
หรือ
Service
ระบบอาจใช้ Service
รายการเดียวเพื่อรวมการประเมิน JavaScript สำหรับคอมโพเนนต์แอปพลิเคชันทั้งหมด
รักษาอินสแตนซ์ JavaScriptSandbox
ไว้เนื่องจากมีการจัดสรรอย่างเป็นธรรม
มีราคาแพง ใช้อินสแตนซ์ JavaScriptSandbox
ได้เพียง 1 รายการต่อแอปพลิเคชัน CANNOT TRANSLATE
IllegalStateException
จะถูกโยนเมื่อแอปพลิเคชันพยายามจัดสรร
JavaScriptSandbox
อินสแตนซ์ที่สอง อย่างไรก็ตาม หากต้องใช้สภาพแวดล้อมการดําเนินการหลายรายการ คุณจะจัดสรรอินสแตนซ์ JavaScriptIsolate
ได้หลายรายการ
เมื่อไม่มีการใช้งานแล้ว ให้ปิดอินสแตนซ์แซนด์บ็อกซ์เพื่อเพิ่มพื้นที่ว่าง อินสแตนซ์ JavaScriptSandbox
ใช้อินเทอร์เฟซ AutoCloseable
ซึ่งอนุญาตให้ใช้ try-with-resources สําหรับกรณีการใช้งานการบล็อกแบบง่าย
หรือตรวจสอบว่าวงจรของอินสแตนซ์ JavaScriptSandbox
ได้รับการจัดการโดย
คอมโพเนนต์โฮสติ้ง ปิดในการเรียกกลับ onStop()
สำหรับกิจกรรม หรือ
ในช่วง onDestroy()
สำหรับบริการ:
jsSandbox.close();
อินสแตนซ์ JavaScriptIsolate
แสดงถึงบริบทสําหรับการเรียกใช้โค้ด JavaScript คุณสามารถจัดสรรหน่วยความจำได้เมื่อจำเป็น ซึ่งจะสร้างขอบเขตการรักษาความปลอดภัยที่อ่อนแอสำหรับสคริปต์ที่มาจากแหล่งที่มาต่างกัน หรือเปิดใช้การดำเนินการ JavaScript พร้อมกัน เนื่องจาก JavaScript เป็นภาษาแบบเธรดเดียวโดยพื้นฐาน การเรียกใช้อินสแตนซ์เดียวกันในภายหลังจะแชร์สถานะเดียวกัน คุณจึงสร้างข้อมูลบางอย่างก่อนแล้วประมวลผลในภายหลังในอินสแตนซ์เดียวกันของ JavaScriptIsolate
ได้
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate();
เผยแพร่ JavaScriptIsolate
อย่างชัดเจนโดยเรียกใช้เมธอด close()
การปิดอินสแตนซ์แยกที่ใช้โค้ด JavaScript (มี Future
ที่ไม่สมบูรณ์) จะทำให้เกิด IsolateTerminatedException
ระบบจะล้างข้อมูลการแยกในเบื้องหลังในภายหลังหากการติดตั้งใช้งานรองรับ JS_FEATURE_ISOLATE_TERMINATION
ตามที่อธิบายไว้ในส่วนการจัดการข้อขัดข้องในแซนด์บ็อกซ์ในหน้านี้ มิเช่นนั้น ระบบจะเลื่อนการล้างข้อมูลจนกว่าการประเมินทั้งหมดที่รอดำเนินการอยู่จะ
เสร็จสมบูรณ์หรือแซนด์บ็อกซ์ปิดอยู่
แอปพลิเคชันสามารถสร้างและเข้าถึงอินสแตนซ์ JavaScriptIsolate
จากเธรดใดก็ได้
ตอนนี้แอปพลิเคชันพร้อมที่จะเรียกใช้โค้ด 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);
ข้อมูลโค้ด JavaScript เดียวกันที่จัดรูปแบบให้เรียบร้อย
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);
ระบบจะส่งข้อมูลโค้ดเป็น String
และส่งผลลัพธ์เป็น String
โปรดทราบว่าการเรียกใช้ evaluateJavaScriptAsync()
จะแสดงผลลัพธ์ที่ประเมินแล้วของนิพจน์สุดท้ายในโค้ด JavaScript โดยต้องเป็นประเภท JavaScript String
ไม่เช่นนั้น API ของไลบรารีจะแสดงผลค่าว่าง
โค้ด JavaScript ไม่ควรใช้คีย์เวิร์ด return
หากแซนด์บ็อกซ์รองรับฟีเจอร์บางอย่าง ประเภทผลลัพธ์เพิ่มเติม (เช่น Promise
ที่แปลงเป็น String
) ก็อาจเป็นไปได้
ไลบรารียังสนับสนุนการประเมินสคริปต์ที่อยู่ในรูปของ
AssetFileDescriptor
หรือ ParcelFileDescriptor
ดูรายละเอียดเพิ่มเติมได้ที่ evaluateJavaScriptAsync(AssetFileDescriptor)
และ evaluateJavaScriptAsync(ParcelFileDescriptor)
API เหล่านี้เหมาะกับการประเมินจากไฟล์ในดิสก์หรือในแอปมากกว่า
ไดเรกทอรี
ไลบรารียังรองรับการบันทึกคอนโซลซึ่งใช้เพื่อแก้ไขข้อบกพร่องได้
วัตถุประสงค์ ซึ่งตั้งค่าได้โดยใช้ setConsoleCallback()
เนื่องจากบริบทยังคงอยู่ คุณจึงอัปโหลดโค้ดและเรียกใช้หลายครั้งได้ในระหว่างอายุการใช้งานของ 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);
แน่นอนว่าตัวแปรต่างๆ จะทำงานตลอดเวลา ดังนั้นคุณสามารถดำเนินการกับ ตัวอย่างที่มี:
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);
ตัวอย่างเช่น ข้อมูลโค้ดที่สมบูรณ์สำหรับการจัดสรรออบเจ็กต์ที่จำเป็นทั้งหมดและเรียกใช้โค้ด JavaScript อาจมีลักษณะดังนี้
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);
เราขอแนะนำให้คุณใช้ "ลองใช้ทรัพยากร" เพื่อให้มั่นใจว่าทั้งหมดจะได้รับการจัดสรร
จะถูกปล่อยและไม่ได้ใช้งานอีกต่อไป การปิดแซนด์บ็อกซ์ส่งผลให้การประเมินที่รอดําเนินการทั้งหมดในอินสแตนซ์ JavaScriptIsolate
ทั้งหมดไม่สําเร็จพร้อมSandboxDeadException
เมื่อการประเมิน JavaScript พบ
เกิดข้อผิดพลาด ระบบจะสร้างJavaScriptException
ขึ้น ดูข้อยกเว้นที่เฉพาะเจาะจงมากขึ้นได้จากคลาสย่อย
การจัดการกับข้อขัดข้องของแซนด์บ็อกซ์
JavaScript ทั้งหมดจะถูกเรียกใช้ในกระบวนการที่ทำแซนด์บ็อกซ์แยกต่างหากจาก ขั้นตอนหลักของแอปพลิเคชัน หากโค้ด JavaScript ทําให้กระบวนการที่ใช้แซนด์บ็อกซ์นี้ขัดข้อง เช่น ใช้หน่วยความจําเกินขีดจํากัด กระบวนการหลักของแอปพลิเคชันจะไม่ได้รับผลกระทบ
ข้อขัดข้องของแซนด์บ็อกซ์จะทำให้การแยกส่วนทั้งหมดในแซนด์บ็อกซ์นั้นสิ้นสุดลง อาการที่เห็นได้ชัดที่สุดคือการประเมินทั้งหมดจะเริ่มไม่ผ่านด้วย IsolateTerminatedException
ข้อมูลเพิ่มเติมขึ้นอยู่กับสถานการณ์
ข้อยกเว้นเฉพาะ เช่น SandboxDeadException
หรือ
ระบบอาจส่ง MemoryLimitExceededException
ออกไป
การจัดการข้อขัดข้องสำหรับการประเมินแต่ละรายการนั้นไม่สามารถทำได้จริงเสมอไป
นอกจากนี้ ไอโซเลตอาจสิ้นสุดการทำงานนอกการประเมินที่ขออย่างชัดเจนเนื่องจากงานเบื้องหลังหรือการประเมินในไอโซเลตอื่นๆ อุบัติเหตุ
สามารถรวมตรรกะการจัดการจากส่วนกลางด้วยการแนบ Callback โดยใช้
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);
ฟีเจอร์แซนด์บ็อกซ์ที่ไม่บังคับ
การใช้งานแซนด์บ็อกซ์อาจมีชุดฟีเจอร์ที่แตกต่างกันไปโดยขึ้นอยู่กับเวอร์ชัน WebView ที่เกี่ยวข้อง คุณจึงต้องค้นหาฟีเจอร์ที่จําเป็นแต่ละรายการโดยใช้ JavaScriptSandbox.isFeatureSupported(...)
คุณควรตรวจสอบสถานะฟีเจอร์ก่อนเรียกใช้เมธอดที่อาศัยฟีเจอร์เหล่านี้
JavaScriptIsolate
เมธอดที่อาจใช้ไม่ได้ในบางที่ ได้แก่
ใส่คำอธิบายประกอบเป็นคำอธิบายประกอบ RequiresFeature
ทำให้สังเกตเห็นได้ง่ายขึ้น
ในโค้ด
การส่งพารามิเตอร์
หากรองรับ JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT
คําขอการประเมินที่ส่งไปยังเครื่องมือ JavaScript จะไม่จํากัดตามขีดจํากัดธุรกรรมของ Binder หากฟีเจอร์นี้ไม่ได้รับการสนับสนุน ข้อมูลทั้งหมดจะ
JavaScriptEngine เกิดขึ้นผ่านธุรกรรม Binder การจำกัดขนาดธุรกรรมทั่วไปมีผลกับทุกการเรียกใช้ที่ส่งข้อมูลหรือแสดงผลข้อมูล
ระบบจะแสดงผลคำตอบเป็นสตริงเสมอและขึ้นอยู่กับขีดจำกัดขนาดธุรกรรมสูงสุดของ Binder หากระบบไม่รองรับ JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT
ค่าที่ไม่ใช่สตริงต้องแปลงเป็นสตริง JavaScript อย่างชัดแจ้ง มิฉะนั้นระบบจะแสดงผลสตริงว่าง หากระบบรองรับฟีเจอร์ JS_FEATURE_PROMISE_RETURN
โค้ด JavaScript อาจแสดงผล Promise ที่แปลงเป็น String
ในการส่งอาร์เรย์ไบต์ขนาดใหญ่ไปยังอินสแตนซ์ JavaScriptIsolate
คุณจะต้อง
ใช้ provideNamedData(...)
API ได้ การใช้งาน API นี้ไม่อยู่ภายใต้ขีดจํากัดธุรกรรมของ Binder อาร์เรย์ไบต์แต่ละรายการต้องส่งผ่านโดยใช้ตัวระบุที่ไม่ซ้ำกันซึ่งใช้ซ้ำไม่ได้
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);
}
กำลังรันโค้ด Wasm
คุณสามารถส่งโค้ด WebAssembly (Wasm) โดยใช้ provideNamedData(...)
API จากนั้นคอมไพล์และเรียกใช้ได้ตามปกติ ดังที่แสดงด้านล่าง
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);
}
การแยก JavaScriptIsolate
อินสแตนซ์ JavaScriptIsolate
ทั้งหมดเป็นอิสระจากกันและไม่
แชร์อะไรก็ได้ ตัวอย่างข้อมูลต่อไปนี้จะแสดงผลเป็น
Hi from AAA!5
และ
Uncaught Reference Error: a is not defined
เนื่องจากอินสแตนซ์ "jsTwo
" ไม่มีสิทธิ์เข้าถึงออบเจ็กต์ที่สร้างใน "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);
การรองรับ Kotlin
หากต้องการใช้ไลบรารี Jetpack นี้กับโคโริวทีนของ Kotlin ให้เพิ่มข้อกําหนดใน kotlinx-coroutines-guava
ซึ่งช่วยให้ผสานรวมกับ ListenableFuture
ได้
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.6.0"
}
ตอนนี้คุณเรียก API ของไลบรารี Jetpack จากขอบเขต coroutine ได้แล้ว ดังที่แสดงด้านล่าง
// 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
)
}
พารามิเตอร์การกําหนดค่า
เมื่อขออินสแตนซ์สภาพแวดล้อมแบบแยก คุณจะปรับแต่งการกำหนดค่าได้ หากต้องการปรับแต่งการกําหนดค่า ให้ส่งอินสแตนซ์ IsolateStartupParameters ไปยัง JavaScriptSandbox.createIsolate(...)
ปัจจุบันพารามิเตอร์อนุญาตให้ระบุขนาดฮีปสูงสุดและขนาดสูงสุดสำหรับค่าที่แสดงผลและข้อผิดพลาดในการประเมิน