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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ספריות AAR

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

Kotlin

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

מגניב

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")
        }
    }
}

מגניב

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).