הוספת כללי שמירה

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

יכול להיות ש-R8 יסיר או ישנה קוד באופן שגוי במצבים הבאים:

  • השתקפות (Reflection): קוד שגולשים ניגשים אליו באמצעות השתקפות, למשל באמצעות Class.forName() או Method.invoke(). בדרך כלל, R8 לא יכול לדעת לאילו שיטות או כיתות תהיה גישה בדרך הזו.
  • שרשור (serialization): יכול להיות ש-R8 תראה ששימוש בקטגוריות או בשדות שנדרשים לשרשור ולביטול שרשור (deserialization) לא נדרש (זוהי צורה אחרת של רפלקציה).
  • Java Native Interface‏ (JNI): שיטות Java שנקראות מקוד מקורי. R8 לא מנתח את הקוד המקורי כדי לראות לאילו פונקציות הוא עשוי להפעיל חזרה ב-Java.

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

איפה מוסיפים כללי שמירה

צריך להוסיף את הכללים לקובץ proguard-rules.pro שנמצא בתיקיית השורש של המודול (יכול להיות שהקובץ כבר נמצא שם, אבל אם לא, צריך ליצור אותו). כדי להחיל את הכללים בקובץ, צריך להצהיר על הקובץ בקובץ build.gradle.kts (או build.gradle) ברמת המודול, כפי שמתואר בקוד הבא:

Kotlin

android {
    buildTypes {
        release {
            isMinifyEnabled = true
            isShrinkResources = true

            proguardFiles(
                // Default file with default optimization rules.
                getDefaultProguardFile("proguard-android-optimize.txt"),

                // File with your custom rules.
                "proguard-rules.pro"
            )
            ...
        }
    }
    ...
}

Groovy

android {
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true

            proguardFiles(
                // Default file with default optimization rules.
                getDefaultProguardFile('proguard-android-optimize.txt'),

                // File with your custom rules.
                'proguard-rules.pro'
            )
        }
    }
    // ...
}

כברירת מחדל, סקריפט ה-build כולל גם את הקובץ proguard-android-optimize.txt. הקובץ הזה כולל כללים שנדרשים לרוב הפרויקטים ל-Android, לכן כדאי להשאיר אותו בסקריפט ה-build.

איך כותבים כללי שמירה

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

כדי להוסיף כלל שמאפשר לשמור את הקובץ, מוסיפים שורה -keep לקובץ proguard-rules.pro.

שמירה על התחביר של הכללים

בדרך כלל, כללי Keep בנויים בפורמט הזה:

-<KeepOption> [OptionalModifier,...] <ClassSpecification> [{ OptionalMemberSpecification }]

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

-keep class com.myapp.MyClass { *; }

דוגמאות נוספות זמינות בקטע הדוגמאות.

כך פועלים הרכיבים של כלל השמירה:

  • בעזרת <KeepOption> אפשר לציין אילו היבטים של הכיתה לשמור:

    האפשרות 'שמירה' תיאור

    -keep

    לשמור את הכיתה ואת התלמידים שמפורטים ב-[{ OptionalMemberSpecification }].

    -keepclassmembers

    מאפשרת לבצע אופטימיזציה של הכיתה. אם הכיתה נשמרת, גם המשתתפים שמפורטים ב-[{ OptionalMemberSpecification }] נשמרים.

    -keepnames

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

    -keepclassmembernames

    לאפשר הסרה של הכיתה והחברים, אבל לא להסתיר או לשנות את החברים בדרכים אחרות.

    -keepclasseswithmembers

    אין הסרה או ערפול של כיתות אם המשתתפים תואמים לדפוס שצוין.

    מומלץ להשתמש ב--keepclassmembers בעיקר, כי הוא מאפשר את רוב האופטימיזציות, ואז ב--keepnames אם יש צורך. -keep לא מאפשר אופטימיזציה, לכן כדאי להשתמש בו במשורה.

  • בעזרת [OptionalModifier],...] אפשר לרשום אפס או יותר משתני לשון של Java של כיתה, לדוגמה public או final.

  • בעזרת <ClassSpecification> אפשר לציין על איזו מחלקה (או על איזו מחלקה אב או על איזה ממשק מוטמע) יחול כלל השמירה. במקרה הפשוט ביותר, זוהי מחלקה אחת מוגדרת במלואה.

  • [{ OptionalMemberSpecification }] מאפשר לסנן את התנהגות השמירה כך שתחול רק על שיטות ועל כיתות שתואמות לדפוסים מסוימים. באופן כללי, מומלץ להשתמש באפשרות הזו ברוב כללי השמירה כדי למנוע שמירה של יותר מהמתוכנן.

שמירה של דוגמאות לכללים

ריכזנו כאן כמה דוגמאות לכללי שמירה שתוכננו היטב.

יצירה של מחלקה משנית

כלל השמירה הזה ארוז בתוך androidx.room:room-runtime כדי לשמור על היכולת ליצור מופעים של מגדירי מסדי נתונים באמצעות רפלקציה.

-keep class * extends androidx.room.RoomDatabase { void <init>(); }

השתקפות מ-Android Framework ObjectAnimator

כלל השמירה הזה ארוז בתוך androidx.vectordrawable:vectordrawable-animated כדי לאפשר ל-ObjectAnimator לבצע קריאה ל-getters או ל-setters מקוד מקומי באמצעות JNI. הערה: מערכות אנימציה חדשות יותר ב-Compose לא דורשות כללי שמירה כאלה, זו רק דוגמה למבנה של כלל.

-keepclassmembers class androidx.vectordrawable.graphics.drawable.VectorDrawableCompat$* {
   void set*(***);
   *** get*();
}

רישום JNI

כלל השמירה הזה ארוז בתוך androidx.graphics:graphics-path, ומשמשים לשמירת שיטות מקוריות. יכול להיות שתצטרכו לעשות זאת אם הספרייה שלכם רושמת באופן ידני שיטות מקוריות באמצעות env->RegisterNatives().

-keepclasseswithmembers class androidx.graphics.path.** {
    native <methods>;
}