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