درحال اجرای جاوا اسکریپت و WebAssembly

ارزیابی جاوا اسکریپت

کتابخانه Jetpack JavaScriptEngine راهی برای ارزیابی کد جاوا اسکریپت توسط برنامه بدون ایجاد نمونه WebView ارائه می‌دهد.

برای برنامه‌هایی که به ارزیابی جاوا اسکریپت غیرتعاملی نیاز دارند، استفاده از کتابخانه JavaScriptEngine مزایای زیر را دارد:

  • مصرف منابع کمتر، زیرا نیازی به اختصاص دادن نمونه WebView نیست.

  • می‌تواند در «سرویس» (تکلیف WorkManager) انجام شود.

  • چندین محیط ایزوله با سربار کم که به برنامه امکان می‌دهد چندین قطعه کد جاوا اسکریپت را به‌طور هم‌زمان اجرا کند.

  • امکان انتقال حجم زیادی از داده‌ها بااستفاده از فراخوانی میانای برنامه‌سازی کاربردی.

استفاده پایه

برای شروع، نمونه‌ای از JavaScriptSandbox ایجاد کنید. این نشان‌دهنده اتصال به موتور جاوا اسکریپت خارج از فرایند است.

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

توصیه می‌شود چرخه حیات جعبه امن با چرخه حیات مؤلفه‌ای که به ارزیابی جاوا اسکریپت نیاز دارد هم‌راستا شود.

برای مثال، مؤلفه‌ای که میزبان محیط امن است ممکن است Activity یا Service باشد. ممکن است از یک Service برای کپسوله‌سازی ارزیابی جاوا اسکریپت برای همه مؤلفه‌های برنامه استفاده شود.

نمونه JavaScriptSandbox را حفظ کنید زیرا تخصیص آن نسبتاً گران است. فقط یک نمونه JavaScriptSandbox برای هر برنامه مجاز است. وقتی برنامه‌ای تلاش می‌کند نمونه JavaScriptSandbox دومی را اختصاص دهد، IllegalStateException ایجاد می‌شود. بااین‌حال، اگر چند محیط اجرا لازم باشد، می‌توان چند نمونه JavaScriptIsolate اختصاص داد.

وقتی دیگر از آن استفاده نمی‌شود، نمونه جعبه امن را ببندید تا منابع آزاد شود. نمونه JavaScriptSandbox یک میانای AutoCloseable را پیاده‌سازی می‌کند که امکان استفاده از try-with-resources را برای موارد استفاده مسدودسازی ساده فراهم می‌کند. یا مطمئن شوید چرخه حیات نمونه JavaScriptSandbox توسط مؤلفه میزبان مدیریت می‌شود و آن را در فراخوانی برگشتی onStop() برای «فعالیت» یا درطول onDestroy() برای «سرویس» می‌بندد:

jsSandbox.close();

نمونه JavaScriptIsolate نشان‌دهنده بافتی برای اجرای کد جاوا اسکریپت است. این فرایندها را می‌توان درصورت لزوم اختصاص داد، که مرزهای امنیتی ضعیفی برای اسکریپت‌های با منشأ متفاوت فراهم می‌کند یا اجرای هم‌زمان جاوا اسکریپت را فعال می‌کند، زیرا جاوا اسکریپت ذاتاً تک‌رشته‌ای است. تماس‌های بعدی به همان نمونه، همان وضعیت را هم‌رسانی می‌کنند، بنابراین می‌توان ابتدا برخی داده‌ها را ایجاد کرد و سپس آن‌ها را بعداً در همان نمونه JavaScriptIsolate پردازش کرد.

JavaScriptIsolate jsIsolate = jsSandbox.createIsolate();

‫JavaScriptIsolate را با فراخوانی صریح روش close() آن آزاد کنید. بستن نمونه ایزوله‌ای که کد جاوا اسکریپت را اجرا می‌کند (با Future ناقص) منجر به IsolateTerminatedException می‌شود. اگر پیاده‌سازی از JS_FEATURE_ISOLATE_TERMINATION پشتیبانی کند، همان‌طور که در بخش رسیدگی به ازکارافتادن جعبه امن در ادامه این صفحه توضیح داده شده است، ایزوله متعاقباً در پس‌زمینه پاک‌سازی می‌شود. درغیراین‌صورت، پاک‌سازی تا زمانی‌که همه ارزیابی‌های معلق تکمیل شوند یا محیط ایمن بسته شود به‌تعویق می‌افتد.

برنامه می‌تواند از هر رشته‌ای نمونه JavaScriptIsolate ایجاد کند و به آن دسترسی داشته باشد.

اکنون برنامه آماده اجرای کد جاوا اسکریپت است:

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);

همان قطعه کد جاوا اسکریپت با قالب‌بندی مناسب:

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() نتیجه ارزیابی‌شده آخرین عبارت را در کد جاوا اسکریپت برمی‌گرداند. این باید از نوع جاوا اسکریپت String باشد؛ درغیراین‌صورت، میانای برنامه‌سازی کاربردی کتابخانه مقدار خالی برمی‌گرداند. کد جاوا اسکریپت نباید از کلیدواژه 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);

برای مثال، قطعه کامل برای اختصاص دادن همه اشیاء لازم و اجرای کد جاوا اسکریپت ممکن است شبیه به موارد زیر باشد:

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 می‌شود. وقتی ارزیابی جاوا اسکریپت با خطایی مواجه می‌شود، JavaScriptException ایجاد می‌شود. برای استثناهای خاص‌تر، به زیرکلاس‌های آن مراجعه کنید.

رسیدگی به خرابی‌های جعبه شنی

همه جاوا اسکریپت‌ها در فرایند جعبه امن جداگانه‌ای دور از فرایند اصلی برنامه شما اجرا می‌شوند. اگر کد جاوا اسکریپت باعث شود این فرایند جعبه امنی ازکار بیفتد، برای مثال، با تمام کردن محدودیت حافظه، فرایند اصلی برنامه تحت تأثیر قرار نخواهد گرفت.

خرابی محیط ایمن باعث می‌شود همه ایزوله‌ها در آن محیط ایمن بسته شوند. واضح‌ترین نشانه این مشکل این است که همه ارزیابی‌ها با 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);

ویژگی‌های اختیاری جعبه شنی

بسته به نسخه «وب‌نمای» زیربنایی، پیاده‌سازی جعبه امن ممکن است مجموعه متفاوتی از ویژگی‌های دردسترس داشته باشد. بنابراین، لازم است هر ویژگی موردنیاز را بااستفاده از JavaScriptSandbox.isFeatureSupported(...) پُرسمان کنید. مهم است که قبل‌از فراخوانی روش‌های متکی بر این ویژگی‌ها، وضعیت ویژگی را بررسی کنید.

روش‌های JavaScriptIsolate که ممکن است در همه‌جا دردسترس نباشند با حاشیه‌نویسی RequiresFeature حاشیه‌نویسی شده‌اند تا تشخیص این فراخوانی‌ها در کد آسان‌تر شود.

پارامترهای پاس دادن

اگر JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT پشتیبانی شود، درخواست‌های ارزیابی ارسال‌شده به موتور جاوا اسکریپت محدود به محدودیت‌های تراکنش binder نیستند. اگر از این ویژگی پشتیبانی نشود، همه داده‌ها به JavaScriptEngine ازطریق تراکنش Binder ارسال می‌شود. محدودیت اندازه تراکنش عمومی برای هر تماسی که داده ارسال می‌کند یا داده برمی‌گرداند قابل‌اعمال است.

پاسخ همیشه به‌صورت رشته برگردانده می‌شود و اگر JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT پشتیبانی نشود، تابع حداکثر اندازه مجاز تراکنش Binder است. مقادیر غیررشته‌ای باید صریحاً به «رشته جاوا اسکریپت» تبدیل شوند درغیراین‌صورت رشته خالی برگردانده می‌شود. اگر ویژگی JS_FEATURE_PROMISE_RETURN پشتیبانی شود، کد جاوا اسکریپت ممکن است به‌جای آن یک Promise برگرداند که به String تبدیل می‌شود.

برای ارسال آرایه‌های بایت بزرگ به نمونه JavaScriptIsolate، می‌توانید از API provideNamedData(...) استفاده کنید. استفاده از این 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"
}

اکنون می‌توان میاناهای برنامه‌سازی کاربردی کتابخانه 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(...) ارسال کنید.

درحال‌حاضر پارامترها امکان تعیین حداکثر اندازه توده و حداکثر اندازه برای مقادیر برگشتی ارزیابی و خطاها را فراهم می‌کنند.