تنفيذ 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);

ننصحك باستخدام try-with-resources للتأكّد من تحرير كل موارد المخصّصة وعدم استخدامها بعد ذلك. يؤدي إغلاق وضع المحاكاة إلى تعذُّر إكمال جميع التقييمات في انتظار المراجعة في جميع نُسخ 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);

ميزات "وضع الحماية" الاختيارية

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

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

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

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

يتم دائمًا عرض الردّ كسلسلة، وهو يخضع للحد الأقصى لحجم معاملة 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);
}

فصل 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"
}

يمكن الآن طلب واجهات برمجة تطبيقات مكتبة 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
    )
}

مَعلمات الضبط

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

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