הערכת JavaScript
ספריית Jetpack JavaScript מאפשרת לאפליקציה לבדוק קוד JavaScript בלי ליצור מופע של WebView.
עבור אפליקציות שמחייבות הערכה לא אינטראקטיבית של JavaScript, יש להשתמש במשתנה לספריית JavaScript יש את היתרונות הבאים:
צריכת משאבים נמוכה יותר, כי אין צורך להקצות מכונה של WebView.
אפשר לעשות זאת בשירות (משימה ב-WorkManager).
סביבות מבודדות מרובות עם תקורה נמוכה, וכך האפליקציה יכולה להריץ כמה קטעי קוד JavaScript בו-זמנית.
יכולת להעביר כמויות גדולות של נתונים באמצעות קריאה ל-API.
שימוש בסיסי
כדי להתחיל, יוצרים מופע של JavaScriptSandbox
. הוא מייצג חיבור למנוע JavaScript מחוץ לתהליך.
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceAsync(context);
מומלץ להתאים את מחזור החיים של ארגז החול למחזור החיים של הרכיב שצריך להעריך את JavaScript.
לדוגמה, רכיב שמארח את ה-Sandbox עשוי להיות Activity
או
Service
. אפשר להשתמש ב-Service
אחד כדי להכיל את הערכת ה-JavaScript לכל רכיבי האפליקציה.
כדאי לתחזק את המכונה JavaScriptSandbox
כי ההקצאה שלה יקרה למדי. בכל אפליקציה יכולה להיות רק מופע אחד של JavaScriptSandbox
.
IllegalStateException
מושלכת כשאפליקציה מנסה להקצות
למופע JavaScriptSandbox
השני. עם זאת, אם נדרשות כמה סביבות הפעלה, אפשר להקצות כמה מכונות JavaScriptIsolate
.
אם לא משתמשים יותר בו, סוגרים את המופע של ה-Sandbox כדי לפנות משאבים.
במכונה JavaScriptSandbox
מוטמע ממשק AutoCloseable
,
שמאפשר לנסות להשתמש במשאבים בתרחישים לדוגמה פשוטים של חסימה.
לחלופין, מוודאים שמחזור החיים של המכונה JavaScriptSandbox
מנוהל על ידי רכיב האירוח, וסוגרים אותו בקריאה החוזרת (callback) של onStop()
לפעילות או במהלך onDestroy()
לשירות:
jsSandbox.close();
מכונה של JavaScriptIsolate
מייצגת הקשר לביצוע
קוד JavaScript. אפשר להקצות אותם במקרה הצורך, וכך לשפר את האבטחה
גבולות לסקריפטים ממקור שונה או שמאפשרים JavaScript בו-זמנית
מפני ש-JavaScript הוא חלק משרשור יחיד בטבע. קריאות חוזרות לאותו מופע משתפות את אותו מצב, כך שאפשר ליצור נתונים מסוימים ואז לעבד אותם מאוחר יותר באותו מופע של JavaScriptIsolate
.
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate();
משחררים את JavaScriptIsolate
באופן מפורש על ידי קריאה ל-method close()
שלו.
סגירת מכונה מבודדת שמריצה קוד JavaScript
(אם השדה Future
לא הושלם) התוצאה תהיה IsolateTerminatedException
. לאחר מכן, המבודד ינוקה ברקע אם ההטמעה תומכת ב-JS_FEATURE_ISOLATE_TERMINATION
, כפי שמתואר בקטע טיפול בקריסות ב-sandbox בהמשך הדף. אחרת, הניקוי נדחה עד שכל ההערכות שבהמתנה
יושלם או ש-Sandbox ייסגר.
האפליקציה יכולה ליצור מופע של 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. אחרת, ה-API של הספרייה יחזיר ערך ריק.
קוד ה-JavaScript לא יכול להשתמש במילת מפתח מסוג return
. אם מדובר ב-Sandbox
תומכת בתכונות מסוימות ובסוגי החזרה נוספים (לדוגמה, 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);
לדוגמה, קטע הקוד המלא להקצאת כל האובייקטים הנדרשים ולהרצת קוד 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
. מידע על מחלקות המשנה שלו
לקבלת חריגים ספציפיים יותר.
טיפול בקריסות Sandbox
כל הקוד ב-JavaScript מופעל בתהליך נפרד בקונטיינר של חול, מחוץ לתהליך הראשי של האפליקציה. אם קוד ה-JavaScript יגרום לקריסה של התהליך בארגז החול, למשל על ידי מיצוי מגבלת הזיכרון, התהליך הראשי של האפליקציה לא יושפע מכך.
כאשר מתרחש קריסה בארגז החול, כל הבידוד בארגז החול הזה מסתיים. במידה הרבה ביותר
תסמין ברור לכך הוא שכל ההערכות יתחילו להיכשל
IsolateTerminatedException
בהתאם לנסיבות, יכול להיות שיופעלו חריגים ספציפיים יותר כמו SandboxDeadException
או MemoryLimitExceededException
.
לא תמיד יש צורך לטפל בקריסות בכל הערכה בנפרד.
בנוסף, יכול להיות ש-isolate יסתיים מחוץ להערכה שביקשת באופן מפורש בגלל משימות רקע או הערכות ב-isolates אחרים. הקריסה
אפשר לרכז את לוגיקת הטיפול באמצעות צירוף של קריאה חוזרת באמצעות
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 הבסיסית, הטמעה של Sandbox עשויה להיות
קבוצות שונות של תכונות זמינות. לכן צריך לשלוח שאילתה לגבי כל
באמצעות 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
, אפשר להשתמש ב-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) באמצעות ה-API 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"
}
עכשיו אפשר לקרוא לממשקי ה-API של ספריית Jetpack מהיקף של Coroutine, הדוגמה הבאה:
// 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(...)
.
נכון לעכשיו, הפרמטרים מאפשרים לציין את גודל האשפה המקסימלי ואת הגודל המקסימלי של הערכים והשגיאות שמוחזרים מההערכה.