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 שבו הקוד המקודם מוטמע במכשיר.

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

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

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

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

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

שינויי הפסקה

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

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

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

  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 של המודול. אם הוא מופיע שם, בדרך כלל הוא מוסיף ספריות כאלה באופן אוטומטי, חוץ מאשר במקרים המיוחדים הבאים:

    • הספרייה היא לא ספריית 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 של המודול. המאפיינים האלה מקבלים רשימה של שמות מודולים. הסדר היחסי של הספריות ברשימה חייב להיות זהה לזה במניפסט.

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

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

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

אי-התאמה בין זמן build בזמן ריצה לבין CLC מדווח ב-Logcat במהלך האתחול. כדי לחפש אותו, אפשר להשתמש בפקודה הבאה:

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

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