ב-Android 12 יש שינויים במערכת build של קובצי DEX (dexpreopt) של מודולי Java עם תלויות <uses-library>, שמתבצעת קומפילציה מראש (AOT). במקרים מסוימים, שינויים במערכת build עלולים לגרום לכשלים ב-builds. בדף הזה מוסבר איך להתכונן למקרים של שיבושים, ואיך לפתור אותם או לצמצם את ההשפעה שלהם.
Dexpreopt הוא תהליך של קומפילציה מראש של ספריות ואפליקציות Java. התהליך Dexpreopt מתבצע במארח בזמן הבנייה (בניגוד ל-dexopt, שמתבצע במכשיר). המבנה של יחסי התלות בספרייה משותפת שמשמשים מודול Java (ספרייה או אפליקציה) נקרא הקשר של טוען המחלקות (CLC). כדי להבטיח את הנכונות של dexpreopt, צריך לוודא שפקודות ה-CLC בזמן הבנייה ובזמן הריצה זהות. ה-CLC של זמן הבנייה הוא מה שהקומפיילר dex2oat משתמש בו בזמן dexpreopt (הוא מתועד בקובצי ה-ODEX), וה-CLC של זמן הריצה הוא ההקשר שבו הקוד שעבר קומפילציה מראש נטען במכשיר.
מטעמי דיוק וביצועים, ה-CLCs בזמן הבנייה ובזמן הריצה צריכים להיות זהים. כדי שהנתונים יהיו מדויקים, צריך לטפל בכיתות כפולות. אם התלות בספרייה המשותפת בזמן הריצה שונה מהתלות שבה נעשה שימוש בזמן הקומפילציה, יכול להיות שחלק מהמחלקות ייפתרו בצורה שונה, מה שיגרום לבאגים קלים בזמן הריצה. הביצועים מושפעים גם מהבדיקות בזמן הריצה של מחלקות כפולות.
תרחישי שימוש מושפעים
השינויים האלה משפיעים בעיקר על תרחיש השימוש של האתחול הראשון: אם ART מזהה חוסר התאמה בין CLC בזמן הבנייה לבין CLC בזמן הריצה, הוא דוחה את הארטיפקטים של dexpreopt ומריץ במקומם את dexopt. באתחולים הבאים זה בסדר כי אפשר לבצע dexopt לאפליקציות ברקע ולאחסן אותן בדיסק.
אזורים מושפעים ב-Android
הבעיה הזו משפיעה על כל האפליקציות והספריות של Java שיש להן תלות בזמן ריצה בספריות אחרות של Java. ב-Android יש אלפי אפליקציות, ומאות מהן משתמשות בספריות משותפות. השינוי משפיע גם על שותפים, כי יש להם ספריות ואפליקציות משלהם.
שינויי תוכנה שעלולים לגרום לכשלים
מערכת ה-build צריכה לדעת את התלות של <uses-library> לפני שהיא יוצרת כללי build של dexpreopt. עם זאת, הוא לא יכול לגשת למניפסט ישירות ולקרוא את התגים <uses-library> שבו, כי למערכת הבנייה אין הרשאה לקרוא קבצים שרירותיים כשהיא יוצרת כללי בנייה (מסיבות שקשורות לביצועים). בנוסף, יכול להיות שהמניפסט ארוז בתוך קובץ APK או קובץ שנבנה מראש. לכן, המידע <uses-library>
חייב להופיע בקובצי ה-build (Android.bp או Android.mk).
בעבר, ART השתמש בפתרון עקיף שהתעלם מתלות בספרייה משותפת (שנקרא &-classpath). הפתרון העקיף הזה לא היה בטוח וגרם לבאגים לא בולטים, ולכן הוא הוסר ב-Android 12.
כתוצאה מכך, מודולים של Java שלא מספקים מידע נכון בקובצי ה-build שלהם עלולים לגרום לשיבושים ב-build (שנגרמים מחוסר התאמה של CLC בזמן ה-build) או לנסיגות בזמן האתחול הראשון (שנגרמות מחוסר התאמה של CLC בזמן האתחול ואחריו dexopt).<uses-library>
מסלול המיגרציה
כדי לתקן build פגום:
כדי להשבית באופן גלובלי את הבדיקה בזמן הבנייה של מוצר מסוים, מגדירים
PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := trueב-makefile של המוצר. השינוי הזה מתקן שגיאות בבנייה (למעט מקרים מיוחדים שמפורטים בקטע תיקון בעיות). עם זאת, זהו פתרון זמני, והוא עלול לגרום לחוסר התאמה של CLC בזמן האתחול, ואחריו dexopt.
כדי לפתור את הבעיה, צריך להוסיף את פרטי
<uses-library>לקובצי ה-build של המודולים שנכשלו (פרטים נוספים זמינים במאמר פתרון בעיות). ברוב המודולים, כדי לעשות את זה צריך להוסיף כמה שורות ב-Android.bpאו ב-Android.mk.משביתים את הבדיקה בזמן הבנייה ואת dexpreopt במקרים הבעייתיים, על בסיס כל מודול. כדאי להשבית את dexpreopt כדי לא לבזבז משך זמן של תהליך build ואחסון על פריטי מידע שנוצרו בתהליך פיתוח (artifacts) שנדחים בזמן האתחול.
כדי להפעיל מחדש את הבדיקה בזמן הבנייה באופן גלובלי, צריך לבטל את ההגדרה של
PRODUCT_BROKEN_VERIFY_USES_LIBRARIESשהוגדרה בשלב 1. אחרי השינוי הזה, הבנייה לא אמורה להיכשל (בגלל שלבים 2 ו-3).מתקנים את המודולים שהשבתתם בשלב 3, אחד בכל פעם, ואז מפעילים מחדש את dexpreopt ואת הבדיקה של
<uses-library>. במקרה הצורך, מדווחים על באגים.
ב-Android 12, מופעלת אכיפה של בדיקות <uses-library> בזמן ה-build.
תיקון בעיות
בקטעים הבאים מוסבר איך לתקן סוגים ספציפיים של שיבושים.
שגיאת build: חוסר התאמה של CLC
מערכת ה-build מבצעת בדיקת עקביות בזמן ה-build בין המידע בקובצי Android.bp או Android.mk לבין המניפסט. מערכת ה-build לא יכולה לקרוא את המניפסט, אבל היא יכולה ליצור כללי build כדי לקרוא את המניפסט (לחלץ אותו מקובץ APK אם צריך), ולהשוות בין תגי <uses-library> במניפסט לבין פרטי <uses-library> בקובצי ה-build. אם הבדיקה נכשלת,
השגיאה נראית כך:
error: mismatch in the <uses-library> tags between the build system and the manifest:
- required libraries in build system: []
vs. in the manifest: [org.apache.http.legacy]
- optional libraries in build system: []
vs. in the manifest: [com.x.y.z]
- tags in the manifest (.../X_intermediates/manifest/AndroidManifest.xml):
<uses-library android:name="com.x.y.z"/>
<uses-library android:name="org.apache.http.legacy"/>
note: the following options are available:
- to temporarily disable the check on command line, rebuild with RELAX_USES_LIBRARY_CHECK=true (this will set compiler filter "verify" and disable AOT-compilation in dexpreopt)
- to temporarily disable the check for the whole product, set PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true in the product makefiles
- to fix the check, make build system properties coherent with the manifest
- see build/make/Changes.md for details
כפי שמופיע בהודעת השגיאה, יש כמה פתרונות, בהתאם לדחיפות:
- כדי להגדיר תיקון זמני לכל המוצרים, מגדירים את הערך
PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := trueבקובץ ה-Makefile של המוצר. בדיקת העקביות בזמן הבנייה עדיין מתבצעת, אבל אם הבדיקה נכשלת זה לא אומר שהבנייה נכשלה. במקום זאת, אם הבדיקה נכשלת, מערכת ה-build מורידה את המסנן של dex2oat ל-verifyב-dexpreopt, מה שמשבית לחלוטין את הקומפילציה של AOT עבור המודול הזה. - כדי לבצע תיקון מהיר וגלובלי בשורת הפקודה, משתמשים במשתנה הסביבה
RELAX_USES_LIBRARY_CHECK=true. היא משפיעה כמוPRODUCT_BROKEN_VERIFY_USES_LIBRARIES, אבל מיועדת לשימוש בשורת הפקודה. משתנה הסביבה מבטל את משתנה המוצר. - כדי לתקן את שורש הבעיה, צריך להגדיר את מערכת ה-build כך שתזהה את התגים
<uses-library>במניפסט. בדיקה של הודעת השגיאה מראה אילו ספריות גורמות לבעיה (כך גם בדיקה שלAndroidManifest.xmlאו של המניפסט בתוך קובץ APK שאפשר לבדוק באמצעותaapt dump badging $APK | grep uses-library).
למודולים Android.bp:
מחפשים את הספרייה החסרה במאפיין
libsשל המודול. אם היא קיימת, בדרך כלל Soong מוסיף ספריות כאלה באופן אוטומטי, למעט במקרים המיוחדים הבאים:- הספרייה לא מוגדרת כספריית SDK (היא מוגדרת כ-
java_libraryולא כ-java_sdk_library). - הספרייה כוללת שם ספרייה שונה (במניפסט) משם המודול שלה (במערכת build).
כדי לפתור את הבעיה באופן זמני, מוסיפים את
provides_uses_lib: "<library-name>"להגדרת הספרייהAndroid.bp. כדי לפתור את הבעיה לטווח ארוך, צריך להמיר את הספרייה לספריית SDK או לשנות את השם של המודול שלה.- הספרייה לא מוגדרת כספריית SDK (היא מוגדרת כ-
אם השלב הקודם לא סיפק פתרון, מוסיפים
uses_libs: ["<library-module-name>"]לספריות הנדרשות אוoptional_uses_libs: ["<library-module-name>"]לספריות האופציונליות להגדרהAndroid.bpשל המודול. הנכסים האלה מקבלים רשימה של שמות מודולים. הסדר היחסי של הספריות ברשימה צריך להיות זהה לסדר במניפסט.
למודולים Android.mk:
בודקים אם בספרייה יש שם ספרייה שונה (במניפסט) משם המודול שלה (במערכת build). אם כן, אפשר לפתור את הבעיה באופן זמני על ידי הוספת
LOCAL_PROVIDES_USES_LIBRARY := <library-name>לקובץAndroid.mkשל הספרייה, או הוספתprovides_uses_lib: "<library-name>"לקובץAndroid.bpשל הספרייה (שני המקרים אפשריים כי מודולAndroid.mkעשוי להיות תלוי בספרייהAndroid.bp). כדי לפתור את הבעיה לטווח ארוך, צריך לשנות את השם של מודול הספרייה.מוסיפים
LOCAL_USES_LIBRARIES := <library-module-name>לספריות הנדרשות ו-LOCAL_OPTIONAL_USES_LIBRARIES := <library-module-name>לספריות האופציונליות להגדרת המודולAndroid.mk. המאפיינים האלה מקבלים רשימה של שמות מודולים. הסדר היחסי של הספריות ברשימה צריך להיות זהה לסדר שלהן בקובץ המניפסט.
שגיאת בנייה: נתיב ספרייה לא ידוע
אם מערכת ה-Build לא מוצאת נתיב ל-DEX jar (<uses-library>) (נתיב בזמן ה-Build במארח או נתיב התקנה במכשיר), בדרך כלל ה-Build נכשל. אם לא נמצא נתיב, יכול להיות שהספרייה מוגדרת בצורה לא צפויה. כדי לפתור את הבעיה ב-build באופן זמני, משביתים את dexpreopt עבור המודול הבעייתי.
Android.bp (מאפייני מודול):
enforce_uses_libs: false,
dex_preopt: {
enabled: false,
},
Android.mk (משתני מודול):
LOCAL_ENFORCE_USES_LIBRARIES := false
LOCAL_DEX_PREOPT := false
כדי לבדוק תרחישים שלא נתמכים, צריך לשלוח דוח על באג.
שגיאת בנייה: חסרה תלות בספרייה
ניסיון להוסיף את <uses-library> X מהמניפסט של מודול Y לקובץ הבנייה של Y עלול לגרום לשגיאת בנייה בגלל התלות החסרה, X.
זו דוגמה להודעת שגיאה במודולים של Android.bp:
"Y" depends on undefined module "X"
זוהי דוגמה להודעת שגיאה במודולים של Android.mk:
'.../JAVA_LIBRARIES/com.android.X_intermediates/dexpreopt.config', needed by '.../APPS/Y_intermediates/enforce_uses_libraries.status', missing and no known rule to make it
מקור נפוץ לשגיאות כאלה הוא מצב שבו שם הספרייה שונה מהשם של המודול התואם במערכת ה-build. לדוגמה, אם הרשומה במניפסט <uses-library> היא com.android.X, אבל השם של מודול הספרייה הוא רק X, תופיע שגיאה. כדי לפתור את הבעיה הזו, צריך לציין למערכת build שהמודול בשם X מספק <uses-library> בשם com.android.X.
זו דוגמה לספריות Android.bp (מאפיין מודול):
provides_uses_lib: “com.android.X”,
זו דוגמה לספריות Android.mk (משתנה מודול):
LOCAL_PROVIDES_USES_LIBRARY := com.android.X
אי-התאמה של CLC בזמן האתחול
באתחול הראשון, מחפשים ב-logcat הודעות שקשורות לחוסר התאמה של CLC, כמו שמוצג בהמשך:
$ adb wait-for-device && adb logcat \
| grep -E 'ClassLoaderContext [a-z ]+ mismatch' -A1
הפלט יכול לכלול הודעות מהסוג שמוצג כאן:
[...] W system_server: ClassLoaderContext shared library size mismatch Expected=..., found=... (PCL[]... | PCL[]...)
[...] I PackageDexOptimizer: Running dexopt (dexoptNeeded=1) on: ...
אם מוצגת אזהרה על חוסר התאמה של CLC, מחפשים פקודת dexopt עבור המודול הפגום. כדי לפתור את הבעיה, מוודאים שהבדיקה בזמן הבנייה של המודול עוברת. אם זה לא עובד, יכול להיות שמדובר במקרה מיוחד שלא נתמך על ידי מערכת ה-build (למשל, אפליקציה שמעלה APK אחר, ולא ספרייה). מערכת ה-build לא מטפלת בכל המקרים, כי משך זמן של תהליך build אי אפשר לדעת בוודאות מה האפליקציה טוענת בזמן הריצה.
הקשר של טוען המחלקות
ה-CLC הוא מבנה דמוי עץ שמתאר את ההיררכיה של טועני המחלקות. מערכת ה-build משתמשת ב-CLC במובן מצומצם (היא מכסה רק ספריות, לא חבילות APK או טועני מחלקות בהתאמה אישית): זהו עץ של ספריות שמייצג סגירה טרנזיטיבית של כל יחסי התלות <uses-library> של ספרייה או אפליקציה. הרכיבים ברמה העליונה של CLC הם יחסי התלות <uses-library> הישירים שצוינו במניפסט (נתיב המחלקה). כל צומת בעץ CLC הוא צומת <uses-library> שיכול להיות שיש לו צומתי משנה משלו מסוג <uses-library>.
מכיוון שהתלויות של <uses-library> הן גרף אציקלי מכוון, ולא בהכרח עץ, יכול להיות ש-CLC יכיל כמה תתי-עצים לאותה ספרייה. במילים אחרות, CLC הוא גרף התלות ש "נפרס" לעץ. השכפול מתבצע רק ברמה הלוגית. טועני המחלקות הבסיסיים בפועל לא משוכפלים (בזמן הריצה יש מופע יחיד של טוען מחלקות לכל ספרייה).
ה-CLC מגדיר את סדר החיפוש של ספריות כשמבצעים המרה של מחלקות Java שמשמשות את הספרייה או האפליקציה. סדר החיפוש חשוב כי ספריות יכולות להכיל מחלקות כפולות, והמחלקה מומרת להתאמה הראשונה.
CLC במכשיר (בזמן ריצה)
PackageManager (ב-frameworks/base) יוצר CLC כדי לטעון מודול Java במכשיר. הוא מוסיף את הספריות שמפורטות בתגי <uses-library> במניפסט של המודול כרכיבי CLC ברמה העליונה.
לכל ספרייה בשימוש, PackageManager מקבל את כל התלויות שלה <uses-library> (שמצוינות כתגים במניפסט של אותה ספרייה) ומוסיף CLC מקונן לכל תלות. התהליך הזה נמשך באופן רקורסיבי עד שכל צמתי העלים בעץ ה-CLC שנבנה הם ספריות ללא <uses-library>תלות.
PackageManager מודע רק לספריות משותפות. ההגדרה של 'משותף' בהקשר הזה שונה מהמשמעות הרגילה שלו (כמו ב'משותף' לעומת 'סטטי'). ב-Android, ספריות Java משותפות הן אלה שמופיעות בהגדרות XML שמותקנות במכשיר (/system/etc/permissions/platform.xml). כל רשומה מכילה את השם של ספרייה משותפת, נתיב לקובץ ה-JAR של DEX ורשימה של יחסי תלות (ספריות משותפות אחרות שהספרייה הזו משתמשת בהן בזמן ריצה, ומצוינות בתגי <uses-library> במניפסט שלה).
במילים אחרות, יש שני מקורות מידע שמאפשרים ל-PackageManager
ליצור CLC בזמן ריצה: תגי <uses-library>במניפסט ויחסי תלות בספרייה משותפת בהגדרות XML.
CLC במארח (בזמן הבנייה)
ה-CLC נדרש לא רק כשמטעינים ספרייה או אפליקציה, אלא גם כשמבצעים קומפילציה שלהן. הקומפילציה יכולה להתבצע במכשיר (dexopt) או במהלך הבנייה (dexpreopt). האופטימיזציה של dex מתבצעת במכשיר, ולכן יש לה את אותם נתונים כמו PackageManager (מניפסטים ויחסי תלות של ספריות משותפות).
לעומת זאת, Dexpreopt מתבצע במארח ובסביבה שונה לחלוטין, והוא צריך לקבל את אותו מידע ממערכת ה-build.
לכן, ה-CLC של זמן הבנייה שמשמש את dexpreopt וה-CLC של זמן הריצה שמשמש את PackageManager הם אותו דבר, אבל הם מחושבים בשתי דרכים שונות.
ה-CLCs בזמן הבנייה ובזמן הריצה חייבים להיות זהים, אחרת הקוד שעבר קומפילציה של AOT שנוצר על ידי dexpreopt יידחה. כדי לבדוק את השוויון בין CLC בזמן הבנייה לבין CLC בזמן הריצה, מהדר dex2oat מתעד את ה-CLC בזמן הבנייה בקובצי *.odex (בשדה classpath של כותרת קובץ ה-OAT). כדי למצוא את ה-CLC המאוחסן, משתמשים בפקודה הזו:
oatdump --oat-file=<FILE> | grep '^classpath = '
חוסר התאמה בין CLC בזמן הבנייה לבין CLC בזמן הריצה מדווח ב-logcat במהלך האתחול. מחפשים אותו באמצעות הפקודה הבאה:
logcat | grep -E 'ClassLoaderContext [a-z ]+ mismatch'
חוסר התאמה פוגע בביצועים, כי הוא מחייב את הספרייה או האפליקציה לעבור dexopt או לפעול ללא אופטימיזציות (לדוגמה, יכול להיות שיהיה צורך לחלץ את הקוד של האפליקציה בזיכרון מתוך ה-APK, פעולה יקרה מאוד).
ספרייה משותפת יכולה להיות אופציונלית או נדרשת. מנקודת המבט של dexpreopt, ספרייה נדרשת צריכה להיות נוכחת בזמן הבנייה (היעדר שלה הוא שגיאת בנייה). ספרייה אופציונלית יכולה להיות קיימת או לא קיימת משך זמן של תהליך build: אם היא קיימת, היא מתווספת ל-CLC, מועברת ל-dex2oat ומתועדת בקובץ *.odex. אם ספרייה אופציונלית לא קיימת, המערכת מדלגת עליה ולא מוסיפה אותה ל-CLC. אם יש אי התאמה בין הסטטוס בזמן הבנייה לסטטוס בזמן הריצה (הספרייה האופציונלית קיימת במקרה אחד, אבל לא במקרה השני), אז ה-CLCs בזמן הבנייה ובזמן הריצה לא תואמים והקוד שעבר קומפילציה נדחה.
פרטים מתקדמים על מערכת ה-build (כלי לתיקון מניפסטים)
לפעמים תגי <uses-library> חסרים במניפסט המקור של ספרייה או אפליקציה. זה יכול לקרות, למשל, אם אחד מהתלויות הטרנזיטיביות של הספרייה או האפליקציה מתחיל להשתמש בתג <uses-library> אחר, והמניפסט של הספרייה או האפליקציה לא מעודכן כך שיכלול אותו.
מערכת Soong יכולה לחשב באופן אוטומטי חלק מתגי <uses-library> שחסרים בספרייה או באפליקציה מסוימת, כי ספריות ה-SDK נמצאות בסגירת התלות הטרנזיטיבית של הספרייה או האפליקציה. הסגירה נדרשת כי יכול להיות שהספרייה (או האפליקציה) תלויה בספרייה סטטית שתלויה בספריית SDK, ויכול להיות שהיא תלויה שוב באופן טרנזיטיבי דרך ספרייה אחרת.
לא ניתן לחשב את כל התגים <uses-library> בדרך הזו, אבל כשזה אפשרי, עדיף לאפשר ל-Soong להוסיף רשומות למניפסט באופן אוטומטי. כך יש פחות סיכוי לטעות והתחזוקה פשוטה יותר. לדוגמה, אם הרבה אפליקציות משתמשות בספרייה סטטית שמוסיפה תלות חדשה, צריך לעדכן את כל האפליקציות, וזה קשה לתחזוקה.<uses-library>