تقييم 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(...)
.
تتيح المعلَمات حاليًا تحديد الحد الأقصى لحجم الذاكرة والحد الأقصى للحجم لتقييم القيم الإرجاع والأخطاء.