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

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

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

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

שימוש ב-codegen במקום ב-reflection

במידת האפשר, מומלץ להשתמש ביצירת קוד (codegen) במקום בהשתקפות. גם Codegen וגם Reflection הן גישות נפוצות להימנעות מקוד boilerplate במהלך תכנות, אבל Codegen תואם יותר לאופטימיזציה של אפליקציות כמו R8:

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

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

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

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

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

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

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

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

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

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

כתיבת כללי שמירה לצרכנים

בנוסף להנחיות הכלליות לגבי כללי שמירה, יש גם המלצות ספציפיות ליוצרי ספריות.

  • אל תשתמשו בכללים גלובליים לא הולמים – אל תציבו הגדרות גלובליות כמו -dontobfuscate או -allowaccessmodification בקובץ הכללים של הספרייה, כי הן משפיעות על כל האפליקציות שמשתמשות בספרייה.
  • אל תשתמשו ב--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
    • -overloadaggressively
    • -renamesourcefileattribute
    • -ignorewarnings
    • -addconfigurationdebugging
    • -printconfiguration
    • -printmapping
    • -printusage
    • -printseeds
    • -applymapping
    • -obfuscationdictionary
    • -classobfuscationdictionary
    • -packageobfuscationdictionary

ספריות 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 (מתקדם)

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

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