تقييم 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);
ننصحك باستخدام تجربة الموارد للتأكد من أن جميع الأحداث
يتم تحرير الموارد ولا يتم استخدامها بعد ذلك. إغلاق نتائج وضع الحماية
في جميع التقييمات التي تنتظر المراجعة في جميع الحالات التي يتعذّر فيها إكمال 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);
ميزات Sandbox الاختيارية
بناءً على إصدار WebView الأساسي، قد يتضمن تطبيق وضع الحماية
مجموعات مختلفة من الميزات المتاحة. لذلك، من الضروري الاستعلام عن كل قيمة
باستخدام JavaScriptSandbox.isFeatureSupported(...)
. من المهم
للتحقّق من حالة الميزة قبل طُرق الاتصال التي تعتمد على هذه الميزات.
طرق JavaScriptIsolate
التي قد لا تكون متاحة في كل مكان
وتتم إضافة تعليقات توضيحية إليها باستخدام تعليق توضيحي RequiresFeature
، ما يسهِّل العثور على هذه العناصر
في التعليمات البرمجية.
تمرير المعلّمات
إذا كان JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT
متاحة، لا يتم ربط طلبات التقييم المرسلة إلى محرك JavaScript
من خلال حدود معاملات أداة الربط. وإذا لم تكن الميزة متوفرة، فسيتم نقل جميع البيانات إلى
تحدث JavaScriptEngine من خلال معاملة 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);
}
فصل عزل JavaScript
جميع مثيلات 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
)
}
مَعلمات الإعدادات
عند طلب مثيل بيئة معزولة، يمكنك تعديل
التكوين. لتعديل الإعدادات، مرِّر
مثيل IsolateStartupparams لـ
JavaScriptSandbox.createIsolate(...)
تتيح المعلَمات حاليًا تحديد الحد الأقصى لحجم الذاكرة والحد الأقصى للحجم لتقييم القيم الإرجاع والأخطاء.