בדיקות של Dexpreopt ושל <uses-library>

ב-Android 12 יש שינויים במערכת ה-build לגבי הידור AOT של קובצי DEX (dexpreopt) למודול Java שיש להם יחסי תלות ב-<uses-library>. במקרים מסוימים, השינויים האלה במערכת ה-build עלולים לגרום לשיבושים ב-builds. כדאי להיעזר בדף הזה כדי להתכונן לשיבושים, ולפעול לפי ההוראות שבדף כדי לתקן אותם ולצמצם את ההשפעה שלהם.

Dexpreopt הוא תהליך של הידור מראש של ספריות ואפליקציות של Java. Dexpreopt מתבצע במארח בזמן ה-build (בניגוד ל-dexopt, שמתבצע במכשיר). המבנה של יחסי התלות בספריות משותפות שמשמשים מודול Java (ספרייה או אפליקציה) נקרא הקשר של מעמיס הכיתות (CLC). כדי להבטיח את הנכונות של dexpreopt, קוד ה-CLC בזמן ה-build ובזמן הריצה חייב להיות זהה. CLC בזמן ה-build הוא ה-context שבו משתמש המהדר dex2oat בזמן dexpreopt (הוא מתועד בקובצי ODEX), ו-CLC בזמן הריצה הוא ה-context שבו הקוד המקודם מוטמע במכשיר.

ה-CLCs האלה בזמן ה-build ובזמן הריצה חייבים להיות זהים, גם מטעמי תקינות וגם מטעמי ביצועים. כדי שהקוד יהיה תקין, צריך לטפל בכיתות כפולות. אם יחסי התלות בספרייה המשותפת בזמן הריצה שונים מאלה ששימשו לצורך הידור, יכול להיות שחלק מהכיתות יתקבלו פתרון שונה, וכתוצאה מכך יהיו באגים עדינים בזמן הריצה. הביצועים מושפעים גם מהבדיקות בסביבת זמן הריצה לאיתור כפילויות של כיתות.

תרחישים לדוגמה שמושפעים מהשינוי

הפעלת המכשיר בפעם הראשונה היא תרחיש לדוגמה עיקרי שבו השינויים האלה משפיעים: אם ART מזהה אי-התאמה בין CLCs בזמן ה-build לבין CLCs בזמן הריצה, הוא דוחה את ה-artifacts של dexpreopt ומריץ את dexopt במקום זאת. זה בסדר להפעלות הבאות, כי אפשר לבטל את ה-dexopt של האפליקציות ברקע ולאחסן אותן בדיסק.

האזורים ב-Android שהושפעו

הבעיה הזו משפיעה על כל האפליקציות והספריות של Java שיש להן יחסי תלות בסביבת זמן הריצה בספריות Java אחרות. ל-Android יש אלפי אפליקציות, ומאות מהן משתמשות בספריות משותפות. השותפים מושפעים גם הם, כי יש להם ספריות ואפליקציות משלהם.

שינויים שגורמים לכשלים

מערכת ה-build צריכה לדעת את יחסי התלות של <uses-library> לפני שהיא יוצרת כללי build של dexpreopt. עם זאת, היא לא יכולה לגשת למניפסט ישירות ולקרוא את התגים <uses-library> שבתוכו, כי מערכת ה-build לא מורשית לקרוא קבצים שרירותיים כשהיא יוצרת כללי build (מסיבות ביצועים). בנוסף, יכול להיות שהמניפסט יהיה ארוז בתוך קובץ APK או קובץ build מראש. לכן, המידע של <uses-library> חייב להופיע בקובצי ה-build (Android.bp או Android.mk).

בעבר, ART השתמש בפתרון עקיף שדחה את יחסי התלות בספריות משותפות (שנקראים &-classpath). הפתרון הזה לא היה בטוח וגרם לבאגים מתוחכמים, ולכן הפתרון העקיף הוסר ב-Android 12.

כתוצאה מכך, מודולים של Java שלא מספקים מידע <uses-library> נכון בקובצי ה-build שלהם עלולים לגרום לשגיאות ב-build (שנגרמות כתוצאה מחוסר התאמה של CLC בזמן ה-build) או לרגרסיות בזמן ההפעלה הראשונה (שנגרמות כתוצאה מחוסר התאמה של CLC בזמן ההפעלה, ולאחר מכן dexopt).

נתיב ההעברה

כדי לתקן build שפגום:

  1. כדי להשבית את הבדיקה בזמן ה-build באופן גלובלי עבור מוצר מסוים, מגדירים

    PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true

    בקובץ ה-makefile של המוצר. הפעולה הזו מתקנת שגיאות ב-build (למעט במקרים מיוחדים שמפורטים בקטע תיקון שגיאות). עם זאת, זהו פתרון זמני שעלול לגרום ל-CLC mismatch בזמן האתחול, ולאחר מכן ל-dexopt.

  2. מתקנים את המודולים שנכשלו לפני השבתת הבדיקה בזמן ה-build ברמת המערכת, על ידי הוספת פרטי <uses-library> הנדרשים לקובצי ה-build שלהם (פרטים נוספים זמינים במאמר תיקון שגיאות). ברוב המודולים, צריך להוסיף כמה שורות ב-Android.bp או ב-Android.mk.

  3. משביתים את הבדיקה בזמן ה-build ואת dexpreopt במקרים הבעייתיים, על בסיס מודול. משביתים את dexpreopt כדי לא לבזבז זמן build ואחסון על פריטי מידע שנוצרו בתהליך פיתוח (artifacts) שנדחים במהלך האתחול.

  4. מפעילים מחדש את הבדיקה בזמן ה-build ברמת הפרויקט על ידי ביטול ההגדרה של PRODUCT_BROKEN_VERIFY_USES_LIBRARIES שהוגדרה בשלב 1. ה-build לא אמור להיכשל אחרי השינוי הזה (בגלל שלבים 2 ו-3).

  5. מתקנים את המודולים שהשבתתם בשלב 3, אחד אחרי השני, ואז מפעילים מחדש את dexpreopt ואת הבדיקה <uses-library>. אם צריך, מדווחים על באגים.

בדיקות <uses-library> בזמן ה-build נאכפות ב-Android 12.

תיקון שגיאות

בקטעים הבאים מוסבר איך לתקן סוגים ספציפיים של שגיאות.

שגיאת 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 עדיין מתבצעת, אבל כשל בבדיקת העקביות לא מעיד על כשל ב-build. במקום זאת, כשל בבדיקות גורם למערכת ה-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:

  1. מחפשים את הספרייה החסרה במאפיין libs של המודול. אם הוא נמצא שם, בדרך כלל Soong מוסיף ספריות כאלה באופן אוטומטי, מלבד במקרים המיוחדים הבאים:

    • הספרייה היא לא ספריית SDK (היא מוגדרת כ-java_library ולא כ-java_sdk_library).
    • לספרייה יש שם שונה (במניפסט) משם המודול שלה (במערכת ה-build).

    כדי לפתור את הבעיה באופן זמני, מוסיפים את provides_uses_lib: "<library-name>" להגדרת הספרייה Android.bp. כדי למצוא פתרון לטווח ארוך, צריך לפתור את הבעיה הבסיסית: להמיר את הספרייה לספריית SDK או לשנות את השם של המודול.

  2. אם הבעיה לא נפתרה בשלב הקודם, מוסיפים את הערך uses_libs: ["<library-module-name>"] לספריות הנדרשות או את הערך optional_uses_libs: ["<library-module-name>"] לספריות האופציונליות להגדרה Android.bp של המודול. הנכסים האלה מקבלים רשימה של שמות מודולים. הסדר היחסי של הספריות ברשימה חייב להיות זהה לסדר ב-manifest.

במודולים של Android.mk:

  1. בודקים אם לספרייה יש שם שונה (במניפסט) משם המודול שלה (במערכת ה-build). אם כן, אפשר לתקן את הבעיה באופן זמני על ידי הוספת LOCAL_PROVIDES_USES_LIBRARY := <library-name> לקובץ Android.mk בספרייה, או הוספת provides_uses_lib: "<library-name>" לקובץ Android.bp בספרייה (שני המקרים אפשריים כי מודול Android.mk עשוי להיות תלוי בספריית Android.bp). כדי לקבל פתרון לטווח ארוך, צריך לפתור את הבעיה הבסיסית: לשנות את השם של מודול הספרייה.

  2. מוסיפים LOCAL_USES_LIBRARIES := <library-module-name> לספריות הנדרשות, ו-LOCAL_OPTIONAL_USES_LIBRARIES := <library-module-name> לספריות האופציונליות להגדרה Android.mk של המודול. המאפיינים האלה מקבלים רשימה של שמות מודולים. הסדר היחסי של הספריות ברשימה חייב להיות זהה לסדר ב-manifest.

שגיאת build: נתיב הספרייה לא ידוע

אם מערכת ה-build לא מצליחה למצוא נתיב לקובץ jar של <uses-library> DEX (נתיב בזמן ה-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

מדווחים על באג כדי לבדוק תרחישים שלא נתמכים.

שגיאה ב-build: חסרה תלות בספרייה

ניסיון להוסיף את <uses-library> X מהמניפסט של מודול Y לקובץ ה-build של Y עלול לגרום לשגיאת build בגלל התלות 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 עוברת. אם זה לא עובד, יכול להיות שמדובר במקרה מיוחד שלא נתמך על ידי מערכת ה-build (למשל, אפליקציה שטעינה קובץ APK אחר, ולא ספרייה). מערכת ה-build לא מטפלת בכל המקרים, כי בזמן ה-build אי אפשר לדעת בוודאות מה האפליקציה תטען בזמן הריצה.

הקשר של מעמיס הכיתות

ה-CLC הוא מבנה עץ שמתאר את היררכיית מערכי הטעינה של הכיתות. מערכת ה-build משתמשת ב-CLC במובן מצומצם (היא מכסה רק ספריות, לא חבילות APK או מערכי טעינה של כיתות בהתאמה אישית): זהו עץ של ספריות שמייצג סגירה טרנזיטיבית של כל יחסי התלות של <uses-library> בספרייה או באפליקציה. הרכיבים ברמה העליונה של CLC הם יחסי התלות הישירים של <uses-library> שצוינו במניפסט (classpath). כל צומת בעץ 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 במארח (בזמן ה-build)

צריך CLC לא רק כשאתם מעמיסים ספרייה או אפליקציה, אלא גם כשאתם יוצרים אותם. אפשר לבצע את הידור הקוד במכשיר (dexopt) או במהלך ה-build (dexpreopt). מאחר שהפעולה dexopt מתבצעת במכשיר, יש לה את אותם פרטים כמו ל-PackageManager (מניפסטים ותלות בספריות משותפות). עם זאת, תהליך ה-dexpreopt מתבצע במארח ובסביבה שונה לגמרי, והוא צריך לקבל את אותו מידע ממערכת ה-build.

לכן, ה-CLC בזמן ה-build שמשמש את dexpreopt וה-CLC בזמן הריצה שמשמש את PackageManager הם אותו הדבר, אבל הם מחושבים בשתי דרכים שונות.

חובה שה-CLCs בזמן ה-build ובזמן הריצה יהיו זהים, אחרת הקוד שעבר הידור AOT שנוצר על ידי dexpreopt יידחה. כדי לבדוק את השוויון בין CLC בזמן ה-build ל-CLC בזמן הריצה, המהדר dex2oat מתעד את ה-CLC בזמן ה-build בקבצים *.odex (בשדה classpath בכותרת של קובץ ה-OAT). כדי למצוא את ה-CLC ששמור, משתמשים בפקודה הבאה:

oatdump --oat-file=<FILE> | grep '^classpath = '

אי-התאמה בין CLC בזמן ה-build לבין CLC בזמן הריצה מדווחת ב-logcat במהלך האתחול. מחפשים אותו באמצעות הפקודה הבאה:

logcat | grep -E 'ClassLoaderContext [a-z ]+ mismatch'

אי-התאמה פוגעת בביצועים, כי היא מאלצת את הספרייה או האפליקציה לעבור תהליך dex או לפעול ללא אופטימיזציה (לדוגמה, יכול להיות שיהיה צורך לחלץ את הקוד של האפליקציה מה-APK בזיכרון, פעולה יקרה מאוד).

ספרייה משותפת יכולה להיות אופציונלית או חובה. מבחינת dexpreopt, ספרייה נדרשת חייבת להיות קיימת בזמן ה-build (היעדרה הוא שגיאת build). ספרייה אופציונלית יכולה להיות קיימת או לא קיימת בזמן ה-build: אם היא קיימת, היא מתווספת ל-CLC, מועברת ל-dex2oat ומתוועדת בקובץ *.odex. אם ספרייה אופציונלית לא קיימת, היא מועברת ללא בדיקה ולא מתווספת ל-CLC. אם יש אי-התאמה בין הסטטוס בזמן ה-build לבין הסטטוס בזמן הריצה (הספרייה האופציונלית נמצאת במקרה אחד, אבל לא במקרה השני), ה-CLC בזמן ה-build לא תואם ל-CLC בזמן הריצה והקוד המהדרש נדחה.

פרטים מתקדמים של מערכת build (תיקון מניפסט)

לפעמים תגי <uses-library> חסרים במניפסט המקור של ספרייה או אפליקציה. מצב כזה יכול לקרות, למשל, אם אחד מיחסי התלות הטרנזיטיביים של הספרייה או האפליקציה מתחיל להשתמש בתג <uses-library> אחר, והמניפסט של הספרייה או האפליקציה לא מתעדכן כך שיכלול אותו.

Soong יכול לחשב באופן אוטומטי חלק מהתגים החסרים מסוג <uses-library> לספרייה או לאפליקציה נתונה, כספריות ה-SDK במסגרת סגירת התלות הטרנזיטיבית של הספרייה או האפליקציה. הסגירה נדרשת כי יכול להיות שהספרייה (או האפליקציה) תלויה בספרייה סטטית שתלויה בספריית SDK, ויכול להיות שהיא תלויה שוב באופן טרנזיטיבי דרך ספרייה אחרת.

לא ניתן לחשב את כל התגים מסוג <uses-library> באופן הזה, אבל כשהדבר אפשרי, עדיף לאפשר ל-Soong להוסיף רשומות של מניפסט באופן אוטומטי. כך יש פחות סיכוי לשגיאות ותחזוקת הקוד פשוטה יותר. לדוגמה, כשאפליקציות רבות משתמשות בספרייה סטטית שמוסיפה יחסי תלות חדשים מסוג <uses-library>, צריך לעדכן את כל האפליקציות, וזה קשה לתחזוקה.