การประเมิน JavaScript
ไลบรารี Jetpack JavaScriptEngine เป็นวิธีที่ทำให้แอปพลิเคชันสามารถ ประเมินโค้ด JavaScript โดยไม่ต้องสร้างอินสแตนซ์ WebView
สำหรับแอปพลิเคชันที่ต้องการการประเมิน JavaScript แบบไม่โต้ตอบ การใช้ ไลบรารี JavaScriptEngine มีข้อดีดังต่อไปนี้
ใช้ทรัพยากรน้อยลง เนื่องจากไม่จำเป็นต้องจัดสรร WebView อินสแตนซ์
ซึ่งทำได้ใน Service (งาน WorkManager)
สภาพแวดล้อมที่แยกต่างหากหลายจุดที่มีโอเวอร์เฮดต่ำ ทำให้แอปพลิเคชันสามารถ เรียกใช้ข้อมูลโค้ด 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
อนุญาตให้ใช้การลองใช้ทรัพยากรสำหรับ Use Case การบล็อกอย่างง่าย
หรือตรวจสอบว่าวงจรของอินสแตนซ์ JavaScriptSandbox
ได้รับการจัดการโดย
คอมโพเนนต์โฮสติ้ง ปิดในการเรียกกลับ onStop()
สำหรับกิจกรรม หรือ
ในช่วง onDestroy()
สำหรับบริการ:
jsSandbox.close();
อินสแตนซ์ JavaScriptIsolate
แสดงบริบทสำหรับการดำเนินการ
โค้ด JavaScript สามารถจัดสรรได้เมื่อจำเป็น ทำให้มีการรักษาความปลอดภัยที่หละหลวม
ขอบเขตของสคริปต์จากต้นทางที่แตกต่างกัน หรือการเปิดใช้ JavaScript ที่ใช้งานพร้อมกัน
เนื่องจาก JavaScript เป็นเธรดเดี่ยวตามปกติ การโทรครั้งต่อๆ ไปไปยัง
อินสแตนซ์เดียวกันจะอยู่ในสถานะเดียวกัน จึงทำให้สามารถสร้างข้อมูลบางส่วนได้
ก่อน แล้วจึงประมวลผลภายหลังในอินสแตนซ์เดียวกันของ JavaScriptIsolate
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate();
เผยแพร่ JavaScriptIsolate
อย่างชัดเจนโดยเรียกใช้เมธอด close()
การปิดอินสแตนซ์ Isolated ที่เรียกใช้โค้ด 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 ค่านี้ต้อง
ของประเภท String
ของ JavaScript; ไม่เช่นนั้น 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);
}
การแยก JavaScript
อินสแตนซ์ 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 ให้เพิ่มทรัพยากร Dependency ไปยัง
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(...)
ปัจจุบันพารามิเตอร์อนุญาตให้ระบุขนาดฮีปสูงสุดและขนาดสูงสุดได้ สำหรับผลลัพธ์และข้อผิดพลาดในการประเมิน