אופטימיזציה לכותבי ספריות

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

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

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

הסבר על סוגים של כללי שמירה

יש שני סוגים שונים של כללי שמירה שאפשר להגדיר בספריות:

  • כללי שמירה על נתונים של צרכנים צריכים לציין כללים לשמירה של כל מה שהספרייה משקפת. אם ספרייה משתמשת ב-reflection או ב-JNI כדי לקרוא לקוד שלה, או לקוד שהוגדר על ידי אפליקציית לקוח, הכללים האלה צריכים לתאר איזה קוד צריך לשמור. ספריות צריכות לארוז כללי שמירה של צרכנים, שמשתמשים באותו פורמט כמו כללי שמירה של אפליקציות. הכללים האלה נכללים בארטיפקטים של ספריות (AAR או JAR) ונעשה בהם שימוש אוטומטי במהלך האופטימיזציה של אפליקציית Android כשמשתמשים בספרייה. הכללים האלה נשמרים בקובץ שצוין באמצעות המאפיין consumerProguardFiles בקובץ build.gradle.kts (או build.gradle). מידע נוסף זמין במאמר בנושא כתיבת כללי שמירה לצרכנים.
  • כללי שמירה של בניית ספריות מוחלים כשבונים את הספרייה. הם נדרשים רק אם מחליטים לבצע אופטימיזציה חלקית של הספרייה בזמן הבנייה. הם צריכים לוודא שממשק ה-API הציבורי של הספרייה לא יוסר, אחרת ממשק ה-API הציבורי לא יהיה זמין בהפצת הספרייה, כלומר מפתחי אפליקציות לא יוכלו להשתמש בספרייה. הכללים האלה מופיעים בקובץ שצוין במאפיין proguardFiles בקובץ build.gradle.kts (או build.gradle). מידע נוסף זמין במאמר אופטימיזציה של בניית ספריית AAR.

דרישות והנחיות לאופטימיזציה

להגדרת R8 בספריות יש השפעה גלובלית על הגודל הסופי של הקובץ הבינארי ועל הביצועים של האפליקציה שמשתמשת בספריות. בנוסף לשיטות המומלצות הכלליות לכללי שמירה, יוצרים של ספריות צריכים לעמוד בדרישות ספציפיות ולפעול בהתאם להנחיות נוספות.

עמידה בדרישות האופטימיזציה

חוסר יעילות בספריות הוא גורם משמעותי לניפוח האפליקציה, לבזבוז זיכרון, להפעלות איטיות ולמקרי ANR (שגיאות מסוג 'האפליקציה לא מגיבה'). כדי להימנע מפגיעה משמעותית באיכות האפליקציה ובחוויית המשתמש, הספריות צריכות לעמוד בדרישות הבאות:

  • אין כללי שמירה רחבים או כאלה שחלים על כל החבילה: בספרייה שלכם לא יכולים להיות כללי שמירה רחבים ששומרים על רוב הקוד בספרייה שלכם או בספרייה אחרת. כללי שמירה רחבים עשויים לפתור קריסות בטווח הקצר, אבל הם מגדילים את גודל האפליקציה של כל האפליקציות שמשתמשות בספרייה שלכם.

    אל תכללו כללי שמירה שחלים על כל החבילה (כמו -keep class com.mylibrary.** {*; }) לחבילות בספרייה או בספריות אחרות שמופיעות בהפניה. כללים כאלה מגבילים את האופטימיזציה של החבילות האלה בכל האפליקציות שמשתמשות בספרייה שלכם.

  • אין כללים גלובליים לא הולמים: אל תשתמשו אף פעם באפשרויות גלובליות כמו -dontobfuscate או -allowaccessmodification.

  • שימוש ב-codegen במקום ב-reflection כשזה אפשרי: כשזה אפשרי, עדיף להשתמש ביצירת קוד (codegen) במקום ב-reflection. יצירת קוד (codegen) ורפלקציה הן שתי גישות נפוצות להימנעות מקוד חוזר (boilerplate) בתכנות, אבל יצירת קוד תואמת יותר לאופטימיזציה של אפליקציות כמו R8.

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

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

    הרבה ספריות מודרניות משתמשות ביצירת קוד במקום בהשתקפות. במאמר בנושא KSP מוסבר על נקודת כניסה נפוצה שמשמשת את Room,‏ Dagger2 ועוד הרבה ספריות אחרות.

  • תמיכה במצב מלא של R8: הספרייה לא אמורה לקרוס כשמופעל מצב מלא של R8. השימוש במצב המלא של R8 הוא המצב המומלץ לשימוש ב-R8, והוא מוגדר כברירת מחדל מאז AGP 8.0, שהפכה ליציבה בשנת 2023. אם הספריה קורסת ב-R8, הפתרון הוא לזהות את נקודת הכניסה הספציפית של השתקפות או JNI ולהוסיף כלל ממוקד, ולא לשמור את החבילה כולה.

המלצות נוספות

בנוסף לדרישות האופטימיזציה, הנה המלצות נוספות.

  • אל תשתמשו ב--repackageclasses בקובץ כללי השמירה של הצרכנים בספרייה. עם זאת, כדי לבצע אופטימיזציה של בניית הספרייה, אפשר להשתמש ב--repackageclasses עם שם חבילה פנימי, כמו <your.library.package>.internal, בקובץ כללי השמירה של הספרייה. השינוי הזה יכול לשפר את היעילות של הספרייה באפליקציות לא מותאמות. עם זאת, בדרך כלל אין צורך בכך, כי האפליקציות צריכות להיות מותאמות גם הן.
  • צריך להצהיר על כל המאפיינים שדרושים לספרייה כדי לפעול בקובצי כללי השמירה של הספרייה, גם אם יש חפיפה עם המאפיינים שמוגדרים ב-proguard-android-optimize.txt.
  • אם אתם צריכים את המאפיינים הבאים בהפצה של הספרייה, צריך לשמור אותם בקובץ הכללים לשמירה של הספרייה, ולא בקובץ הכללים לשמירה של הצרכן של הספרייה:
    • AnnotationDefault
    • EnclosingMethod
    • Exceptions
    • InnerClasses
    • RuntimeInvisibleAnnotations
    • RuntimeInvisibleParameterAnnotations
    • RuntimeInvisibleTypeAnnotations
    • RuntimeVisibleAnnotations
    • RuntimeVisibleParameterAnnotations
    • RuntimeVisibleTypeAnnotations
    • Signature
  • מפתחי ספריות צריכים להשאיר את המאפיין RuntimeVisibleAnnotations בכללי השמירה לצרכנים אם משתמשים באנוטציות בזמן ריצה.
  • יוצרי ספריות לא צריכים להשתמש באפשרויות הגלובליות הבאות בכללי השמירה של הצרכן:
    • -include
    • -basedirectory
    • -injars
    • -outjars
    • -libraryjars
    • -repackageclasses
    • -flattenpackagehierarchy
    • -allowaccessmodification
    • -renamesourcefileattribute
    • -ignorewarnings
    • -addconfigurationdebugging
    • -printconfiguration
    • -printmapping
    • -printusage
    • -printseeds
    • -applymapping
    • -obfuscationdictionary
    • -classobfuscationdictionary
    • -packageobfuscationdictionary

מתי אפשר להשתמש בהשתקפות

אם אתם חייבים להשתמש ב-reflection, אתם יכולים להשתמש בו רק באחת מהאפשרויות הבאות:

  • סוגים ספציפיים של טירגוט (מיישמי ממשק ספציפיים או מחלקות משנה)
  • קידוד באמצעות הערת זמן ריצה ספציפית

השימוש ב-reflection באופן הזה מגביל את עלות זמן הריצה ומאפשר לכתוב כללי שמירה על נתונים של צרכנים שמיועדים למטרה מסוימת.

ההשתקפות הספציפית והממוקדת הזו היא דפוס שניתן לראות גם ב-Android framework (לדוגמה, כשמנפחים פעילויות, תצוגות ופריטים שניתנים לציור) וגם בספריות AndroidX (לדוגמה, כשמבצעים בנייה של WorkManager ListenableWorkers או RoomDatabases). לעומת זאת, ההשתקפות הפתוחה של Gson לא מתאימה לשימוש באפליקציות ל-Android.

תפיסות מוטעות נפוצות

יש כמה תפיסות מוטעות נפוצות שעלולות לגרום לכם להגדיר את R8 בצורה שגויה. למשל:

  • הבנה שגויה של האופטימיזציות של R8: בניגוד למה שנהוג לחשוב, האופטימיזציות של R8 לא מוגבלות רק להסתרת קוד, אלא כוללות גם צמצום קוד ואופטימיזציות לוגיות עם טכניקות של הטמעת שיטות ומיזוג מחלקות. מידע נוסף זמין במאמר סקירה כללית על אופטימיזציה של R8.

  • דילוג על אופטימיזציה של ספריות שעברו טשטוש: שגיאה נפוצה היא השמטה של ספרייה מהאופטימיזציה, כי הספרייה עברה אופטימיזציה או טשטוש כשהיא קומפלה ל-AAR (ארכיון Android) או ל-JAR (ארכיון Java). האופטימיזציות במהלך בניית הספרייה מוגבלות, ואסור להשבית את האופטימיזציה של הספרייה באפליקציה על ידי הכללתה בכלל שמירה. מידע נוסף זמין במאמר בנושא אופטימיזציה של build של ספריית AAR.

  • הבנה לא נכונה של האפשרות -keep הכלל -keep מונע מ-R8 להריץ את מעברי האופטימיזציה שלו. מידע נוסף זמין במאמר בנושא בחירת אפשרות השמירה הנכונה.

הגדרת אריזת כללים

כדי לוודא שכללי השמירה של נתוני הצרכנים מוחלים בצורה נכונה, צריך לארוז אותם בצורה מתאימה בהתאם לפורמט של הספרייה.

ספריות AAR

כדי להוסיף כללי צריכה לספריית AAR, משתמשים באפשרות consumerProguardFiles בסקריפט הבנייה של מודול הספרייה של Android. מידע נוסף זמין בהנחיות שלנו ליצירת מודולים של ספריות.

Kotlin

android {
    defaultConfig {
        consumerProguardFiles("consumer-proguard-rules.pro")
    }
    ...
}

Groovy

android {
    defaultConfig {
        consumerProguardFiles 'consumer-proguard-rules.pro'
    }
    ...
}

ספריות JAR

כדי לארוז כללים עם ספריית Kotlin או Java שנשלחת כ-JAR, צריך להציב את קובץ הכללים בספרייה META-INF/proguard/ של ה-JAR הסופי, עם שם קובץ כלשהו. לדוגמה, אם הקוד שלכם נמצא ב-<libraryroot>/src/main/kotlin, צריך להציב קובץ כללים של צרכן ב-<libraryroot>/src/main/resources/META-INF/proguard/consumer-proguard-rules.pro, והכללים יצורפו במיקום הנכון ב-JAR הפלט.

כדי לוודא שהכללים של חבילות ה-JAR הסופיות נכונים, בודקים שהכללים נמצאים בספרייה META-INF/proguard.

אופטימיזציה של בניית ספריית AAR (מתקדם)

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

אם עדיין רוצים לבצע אופטימיזציה של הספרייה בזמן הבנייה, אפשר לעשות זאת באמצעות Android Gradle Plugin.

Kotlin

android {
    buildTypes {
        release {
            isMinifyEnabled = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
        configureEach {
            consumerProguardFiles("consumer-rules.pro")
        }
    }
}

Groovy

android {
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles
                getDefaultProguardFile('proguard-android-optimize.txt'),
                'proguard-rules.pro'
        }
        configureEach {
            consumerProguardFiles "consumer-rules.pro"
        }
    }
}

שימו לב שההתנהגות של proguardFiles שונה מאוד מזו של consumerProguardFiles:

  • proguardFiles משמשים בזמן הבנייה, לרוב יחד עם getDefaultProguardFile("proguard-android-optimize.txt"), כדי להגדיר איזה חלק מהספרייה צריך לשמור במהלך בניית הספרייה. לפחות, זה ה-API הציבורי שלכם.
  • לעומת זאת, consumerProguardFiles נארזים בספרייה כדי להשפיע על האופטימיזציות שיתבצעו בהמשך, במהלך ה-build של אפליקציה שמשתמשת בספרייה שלכם.

לדוגמה, אם הספרייה שלכם משתמשת ברפלקציה כדי ליצור מחלקות פנימיות, יכול להיות שתצטרכו להגדיר את כללי השמירה גם ב-proguardFiles וגם ב-consumerProguardFiles.

אם משתמשים ב--repackageclasses בגרסת ה-build של הספרייה, צריך לארוז מחדש את המחלקות לחבילת משנה בתוך חבילת הספרייה. לדוגמה, צריך להשתמש ב--repackageclasses 'com.example.mylibrary.internal' במקום ב--repackageclasses 'internal'.

תמיכה בגרסאות שונות של R8 (מתקדם)

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

כדי לציין כללי R8 ממוקדים, צריך לכלול אותם בספרייה META-INF/com.android.tools בתוך classes.jar של AAR או בספרייה META-INF/com.android.tools של JAR.

In an AAR library:
    proguard.txt (legacy location, the file name must be "proguard.txt")
    classes.jar
    └── META-INF
        └── com.android.tools (location of targeted R8 rules)
            ├── r8-from-<X>-upto-<Y>/<R8-rule-files>
            └── ... (more directories with the same name format)

In a JAR library:
    META-INF
    ├── proguard/<ProGuard-rule-files> (legacy location)
    └── com.android.tools (location of targeted R8 rules)
        ├── r8-from-<X>-upto-<Y>/<R8-rule-files>
        └── ... (more directories with the same name format)

בספרייה META-INF/com.android.tools יכולות להיות כמה ספריות משנה עם שמות מהצורה r8-from-<X>-upto-<Y> כדי לציין לאילו גרסאות של R8 נכתבו הכללים. כל תיקיית משנה יכולה להכיל קובץ אחד או יותר עם כללי R8, עם כל שם קובץ וסיומת.

הערה: החלקים -from-<X> ו--upto-<Y> הם אופציונליים, הגרסה <Y> היא בלעדית, וטווח הגרסאות הוא בדרך כלל רציף אבל יכול להיות גם חופף.

לדוגמה, r8, r8-upto-8.0.0, r8-from-8.0.0-upto-8.2.0 ו-r8-from-8.2.0 הם שמות של ספריות שמייצגות קבוצה של כללי R8 ממוקדים. אפשר להשתמש בכל גרסאות R8 בכללים שבספרייה r8. אפשר להשתמש בכללים שבספרייה r8-from-8.0.0-upto-8.2.0 ב-R8 מגרסה 8.0.0 עד גרסה 8.2.0 לא כולל.

הפלאגין של Android Gradle משתמש במידע הזה כדי לבחור את כל הכללים שאפשר להשתמש בהם בגרסה הנוכחית של R8. אם בספרייה לא מצוינים כללים מטורגטים של R8, הפלאגין Android Gradle יבחר את הכללים מהמיקומים הקודמים (proguard.txt עבור AAR או META-INF/proguard/<ProGuard-rule-files> עבור JAR).