تنفيذ JavaScript وWebAssembly

تقييم JavaScript

توفّر مكتبة Jetpack JavaScriptEngine وسيلة تتيح تنفيذ لتقييم رمز JavaScript بدون إنشاء مثيل WebView.

بالنسبة إلى التطبيقات التي تتطلب تقييم JavaScript غير تفاعلي، يمكن استخدام تتميز مكتبة JavaScriptEngine بالمزايا التالية:

  • انخفاض استهلاك الموارد، لعدم الحاجة إلى تخصيص WebView مثال.

  • يمكن تنفيذه في إحدى الخدمات (مهمة WorkManager).

  • يمكن استخدام بيئات معزولة متعددة وبتكلفة أقل، مما يتيح للتطبيق تشغيل عدة مقتطفات JavaScript في الوقت نفسه

  • إمكانية تمرير كميات كبيرة من البيانات باستخدام طلب بيانات من واجهة برمجة التطبيقات

الاستخدام الأساسي

للبدء، أنشئ مثيلاً لـ JavaScriptSandbox. يمثل ذلك اتصال بمحرك JavaScript خارج المعالجة.

ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
               JavaScriptSandbox.createConnectedInstanceAsync(context);

يُنصح بمواءمة دورة حياة وضع الحماية مع مراحل نشاط الذي يحتاج إلى تقييم JavaScript.

على سبيل المثال، قد يكون المكوِّن الذي يستضيف وضع الحماية Activity أو Service يمكن استخدام نوع Service واحد لتغليف تقييم JavaScript. لجميع مكونات التطبيق.

الإبقاء على حالة JavaScriptSandbox لأنّ تخصيصها إلى حدّ كبير مكلف. يُسمح بمثيل JavaScriptSandbox واحد فقط لكل تطبيق. إنّ يتم طرح IllegalStateException عند محاولة أحد التطبيقات تخصيص مثال JavaScriptSandbox الثاني. ومع ذلك، إذا كانت بيئات التنفيذ متعددة مطلوبة، يمكن تخصيص عدّة مثيلات JavaScriptIsolate.

عند عدم استخدامها، أغلِق مثيل وضع الحماية لإخلاء الموارد. تشير رسالة الأشكال البيانية ينفِّذ المثيل JavaScriptSandbox واجهة AutoCloseable، التي يتيح تجربة الموارد لحالات الاستخدام البسيطة للحظر. بدلاً من ذلك، يمكنك التأكُّد من إدارة مراحل نشاط المثيل 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. يجب أن يكون هذا من النوع String من JavaScript وإلا ستعرض واجهة برمجة التطبيقات للمكتبة قيمة فارغة. يجب ألا يستخدم رمز JavaScript كلمة return الرئيسية. إذا كان وضع الحماية يتيح استخدام ميزات معيّنة وأنواع إرجاع إضافية (على سبيل المثال، سمة Promise التي تحل إلى String).

تدعم المكتبة أيضًا تقييم النصوص البرمجية التي تكون في شكل AssetFileDescriptor أو ParcelFileDescriptor. عرض evaluateJavaScriptAsync(AssetFileDescriptor) و evaluateJavaScriptAsync(ParcelFileDescriptor) للاطّلاع على مزيد من التفاصيل. تكون واجهات برمجة التطبيقات هذه أكثر ملاءمة للتقييم من ملف على القرص أو داخل تطبيق. الأخرى.

تتيح المكتبة أيضًا تسجيل وحدة التحكّم التي يمكن استخدامها لتصحيح الأخطاء. الأهداف. يمكن إعداد هذه الميزة باستخدام 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.

التعامل مع الأعطال لكل تقييم فردي ليس عملية دائمًا. علاوة على ذلك، قد يتم إنهاء العزل خارج نطاق التقييم بسبب مهام الخلفية أو التقييمات في العزلات الأخرى. التعطُّل يمكن تركيز منطق المعالجة من خلال إرفاق رد اتصال باستخدام 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);

ميزات Sandbox الاختيارية

بناءً على إصدار WebView الأساسي، قد يتضمن تطبيق وضع الحماية مجموعات مختلفة من الميزات المتاحة. لذلك، من الضروري الاستعلام عن كل قيمة باستخدام JavaScriptSandbox.isFeatureSupported(...). من المهم للتحقّق من حالة الميزة قبل طُرق الاتصال التي تعتمد على هذه الميزات.

طرق JavaScriptIsolate التي قد لا تكون متاحة في كل مكان وتتم إضافة تعليقات توضيحية إليها باستخدام تعليق توضيحي RequiresFeature، ما يسهِّل العثور على هذه العناصر في التعليمات البرمجية.

تمرير المعلّمات

إذا كان JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT متاحة، لا يتم ربط طلبات التقييم المرسلة إلى محرك JavaScript من خلال حدود معاملات أداة الربط. وإذا لم تكن الميزة متوفرة، فسيتم نقل جميع البيانات إلى تحدث JavaScriptEngine من خلال معاملة Binder. الإجراءات العامة حد حجم المعاملة يسري على كل مكالمة تمر في البيانات أو البيانات.

يتم دائمًا إرجاع الردّ على شكل سلسلة ويخضع للرابط. الحد الأقصى لحجم المعاملة إذا JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT ليس يجب تحويل القيم التي ليست سلسلة بشكل صريح إلى سلسلة JavaScript وإلا سيتم إرجاع سلسلة فارغة. في حال JS_FEATURE_PROMISE_RETURN الميزة متاحة، فقد يعرض رمز JavaScript بدلاً من ذلك رسالة إلى String.

لتمرير صفائف البايت الكبيرة إلى المثيل JavaScriptIsolate، يمكنك استخدام واجهة برمجة تطبيقات provideNamedData(...) لا يخضع استخدام واجهة برمجة التطبيقات هذه لقيود حدود معاملات 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(...) ثم تجميعها وتنفيذها بالطريقة المعتادة، كما هو موضّح أدناه.

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، يجب إضافة تبعية إلى kotlinx-coroutines-guava وهذا يسمح بالتكامل مع ListenableFuture

dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.6.0"
}

ويمكن الآن طلب واجهات برمجة تطبيقات مكتبة Jetpack من نطاق الكوروتين، كما هو موضح أدناه:

// 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
    )
}

مَعلمات الإعدادات

عند طلب مثيل بيئة معزولة، يمكنك تعديل التكوين. لتعديل الإعدادات، مرِّر مثيل IsolateStartupparams لـ JavaScriptSandbox.createIsolate(...)

تتيح المعلَمات حاليًا تحديد الحد الأقصى لحجم الذاكرة والحد الأقصى للحجم لتقييم القيم الإرجاع والأخطاء.