การเรียกใช้ JavaScript และ WebAssembly

การประเมิน 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(...)

ปัจจุบันพารามิเตอร์อนุญาตให้ระบุขนาดฮีปสูงสุดและขนาดสูงสุดได้ สำหรับผลลัพธ์และข้อผิดพลาดในการประเมิน