הערכת 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; אחרת, ה-Library 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
עלול להשליך.
הטיפול בקריסות בכל הערכה בודדת אינו תמיד מעשי.
בנוסף, בידוד עשוי להסתיים מחוץ לדומיין שהתבקש באופן מפורש
הערכה עקב משימות ברקע או הערכות במקומות אחרים. הקריסה
אפשר לרכז את לוגיקת הטיפול באמצעות צירוף של קריאה חוזרת באמצעות
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 הבסיסית, הטמעה של Sandbox עשויה להיות
קבוצות שונות של תכונות זמינות. לכן צריך לשלוח שאילתה לגבי כל
באמצעות JavaScriptSandbox.isFeatureSupported(...)
. חשוב
כדי לבדוק את הסטטוס של התכונות לפני קריאה ל-methods שמסתמכות על התכונות האלה.
שיטות JavaScriptIsolate
שייתכן שלא יהיו זמינות בכל מקום
עם הערה RequiresFeature
, וכך קל יותר לזהות
קריאות בקוד.
פרמטרים מעבירים
אם JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT
הוא
אם אין תמיכה, בקשות ההערכה שנשלחות למנוע ה-JavaScript לא כפופות
בהתאם למגבלות של טרנזקציית קלסר. אם התכונה לא נתמכת, כל הנתונים עבור
מנוע JavaScript מתרחש באמצעות טרנזקציה של Binder. ההנחיות הכלליות
מגבלת גודל העסקה חלה על כל קריאה שעוברת בחבילת הגלישה או
מחזירה נתונים.
התגובה תמיד מוחזרת כמחרוזת והיא כפופה ל-Binnder
מגבלת הגודל המקסימלי לעסקה אם
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) באמצעות 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(...)
.
נכון לעכשיו, פרמטרים מאפשרים לציין את גודל הערימה המקסימלי ואת הגודל המקסימלי להערכה של ערכים מוחזרים ושגיאות.