اجرای جاوا اسکریپت و WebAssembly

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

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

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

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

  • را می توان در یک سرویس (وظیفه WorkManager) انجام داد.

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

  • امکان انتقال مقادیر زیادی داده با استفاده از تماس API.

استفاده پایه

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

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

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

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

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

هنگامی که دیگر استفاده نمی شود، نمونه سندباکس را ببندید تا منابع آزاد شود. نمونه JavaScriptSandbox یک رابط AutoCloseable را پیاده‌سازی می‌کند، که امکان استفاده از تلاش با منابع را برای موارد استفاده مسدود کردن ساده می‌دهد. از طرف دیگر، مطمئن شوید که چرخه عمر نمونه JavaScriptSandbox توسط مؤلفه میزبان مدیریت می‌شود، و آن را در پاسخ به تماس onStop() برای یک Activity یا در طول 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 جاوا اسکریپت باشد. در غیر این صورت، API کتابخانه یک مقدار خالی برمی گرداند. کد جاوا اسکریپت نباید از کلمه کلیدی 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);

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

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

مدیریت خرابی Sandbox

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

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

پاسخ همیشه به عنوان یک رشته برگردانده می شود و در صورتی که JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT پشتیبانی نشود، مشمول محدودیت حداکثر اندازه تراکنش Binder است.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT. مقادیر غیر رشته ای باید صریحاً به یک رشته جاوا اسکریپت تبدیل شوند در غیر این صورت یک رشته خالی برگردانده می شود. اگر ویژگی 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 Code

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

پشتیبانی کاتلین

برای استفاده از این کتابخانه Jetpack با کوروتین های Kotlin، یک وابستگی به kotlinx-coroutines-guava اضافه کنید. این امکان ادغام با ListenableFuture را فراهم می کند.

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

اکنون APIهای کتابخانه 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(...) منتقل کنید.

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