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

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

התחביר הכללי של כלל שמירה הוא:


-<keep_option>[,<keep_option_modifier_1>,<keep_option_modifier_2>,...] <class_specification>

הדוגמה הבאה היא של כלל שמירה שמשתמש ב-keepclassmembers כאפשרות השמירה, ב-allowoptimization כמשתנה, ושומר את someSpecificMethod() מ-com.example.MyClass:

-keepclassmembers,allowoptimization class com.example.MyClass {
  void someSpecificMethod();
}

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

האפשרות keep היא החלק הראשון של כלל השמירה. הוא מציין אילו היבטים של מחלקה צריך לשמר. יש שש אפשרויות שונות לשמירה: keep,‏ keepclassmembers,‏ keepclasseswithmembers,‏ keepnames,‏ keepclassmembernames ו-keepclasseswithmembernames.

בטבלה הבאה מתוארות האפשרויות האלה:

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

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

הערה: המשמעות של האפשרות הזו לא תמיד ברורה, ולכן כדאי להשתמש באפשרות המקבילה -keepclassmembers,allowshrinking.
keepnames מונעת שינוי שם של מחלקות ושל החברים בהן, אבל לא מונעת הסרה מלאה שלהן אם הן לא בשימוש.

הערה: לעיתים קרובות המשמעות של האפשרות הזו לא מובנת כהלכה. כדאי להשתמש במקום זאת באפשרות המקבילה -keep,allowshrinking.
keepclasseswithmembernames מונע שינוי שם של מחלקות ושל חברים ספציפיים בהן, אבל רק אם החברים קיימים בקוד הסופי. היא לא מונעת הסרה של קוד.

הערה: המשמעות של האפשרות הזו לא תמיד מובנת, לכן מומלץ להשתמש במדיניות המקבילה -keepclasseswithmembers,allowshrinking.

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

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

האפשרות "שמירה" מצמצם את הכיתות ערפול קוד של מחלקות כיווץ חברים הסתרת חברים
keep
keepclassmembers
keepclasseswithmembers
keepnames
keepclassmembernames
keepclasseswithmembernames

שמירת מקש ה-Option

משתמשים במאפיין keep option modifier כדי לשלוט בהיקף ובאופן הפעולה של כלל keep. אתם יכולים להוסיף לכלל השמירה אפס או יותר משנים של אפשרויות שמירה.

בטבלה הבאה מתוארים הערכים האפשריים של משנה לאפשרות שמירה:

ערך תיאור
allowoptimization מאפשרת אופטימיזציה של הרכיבים שצוינו. עם זאת, השמות של הרכיבים שצוינו לא משתנים והם לא מוסרים.
allowobfuscation מאפשרת לשנות את השם של הרכיבים שצוינו. עם זאת, הרכיבים לא מוסרים או עוברים אופטימיזציה בדרך אחרת.
allowshrinking מאפשרת הסרה של הרכיבים שצוינו אם R8 לא מוצא הפניות אליהם. עם זאת, השמות של הרכיבים לא משתנים והם לא עוברים אופטימיזציה בדרך אחרת.
includedescriptorclasses ההוראה הזו גורמת ל-R8 לשמור את כל המחלקות שמופיעות בתיאורים של השיטות (סוגי הפרמטרים וסוגי ההחזרה) והשדות (סוגי השדות) שנשמרים.
allowaccessmodification מאפשרת ל-R8 לשנות (בדרך כלל להרחיב) את משני הגישה (public,‏ private,‏ protected) של מחלקות, שיטות ושדות במהלך תהליך האופטימיזציה.
allowrepackage מאפשר ל-R8 להעביר מחלקות לחבילות שונות, כולל חבילת ברירת המחדל (root).

מפרט הסוג

צריך לציין מחלקה (כולל ממשק, enum ומחלקות של אנוטציות) בכל כלל שמירה. אפשר גם להגביל את הכלל על סמך הערות, על ידי ציון מחלקה ראשית או ממשק מוטמע, או על ידי ציון משתנה הגישה של המחלקה. צריך לציין את כל המחלקות, כולל מחלקות ממרחב השמות java.lang כמו java.lang.String, באמצעות השם המלא שלהן ב-Java. כדי להבין באילו שמות צריך להשתמש, בודקים את קוד ה-bytecode באמצעות הכלים שמתוארים בקטע בדיקת שמות Java שנוצרו.

בדוגמה הבאה אפשר לראות איך צריך לציין את המחלקה MaterialButton:

  • נכון: com.google.android.material.button.MaterialButton
  • לא נכון: MaterialButton

מפרטי המחלקות גם מציינים את האיברים בתוך מחלקה שצריך לשמור. לדוגמה, הכלל הבא שומר את המחלקה MyClass ואת השיטה someSpecificMethod():

-keep class com.example.MyClass {
  void someSpecificMethod();
}

הגדרת מחלקות לפי אנוטציות

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

-keep class @com.example.MyAnnotation com.example.MyClass

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

-keep class @com.example.Annotation1 @com.example.Annotation2 *

הגדרת מחלקות משנה והטמעות

כדי לטרגט מחלקת משנה או מחלקה שמטמיעה ממשק, משתמשים ב-extend וב-implements, בהתאמה.

לדוגמה, אם יש לכם מחלקה Bar עם מחלקת משנה Foo באופן הבא:

class Foo : Bar()

כלל השמירה הבא שומר את כל מחלקות המשנה של Bar. שימו לב שכלל השמירה לא כולל את המחלקה הראשית Bar עצמה.

-keep class * extends Bar

אם יש לכם מחלקה Foo שמטמיעה את הממשק Bar:

class Foo : Bar

כלל השמירה הבא שומר את כל המחלקות שמטמיעות את Bar. שימו לב שכלל השמירה לא כולל את הממשק Bar עצמו.

-keep class * implements Bar

הגדרת מחלקות לפי רמות גישה

כדי לדייק את כללי השמירה, אפשר לציין משנים של גישה כמו public, private, static ו-final.

לדוגמה, הכלל הבא שומר את כל המחלקות public בחבילה api ובחבילות המשנה שלה, ואת כל החברים הציבוריים והמוגנים במחלקות האלה.

-keep public class com.example.api.** { public protected *; }

אפשר גם להשתמש בתווי שינוי לחברים בתוך מחלקה. לדוגמה, הכלל הבא שומר רק את השיטות public static של מחלקה Utils:

-keep class com.example.Utils {
  public static void *(...);
}

מגדירי גישה ספציפיים ל-Kotlin

‫R8 לא תומך בשינויים ספציפיים ל-Kotlin כמו internal ו-suspend. כדי לשמור שדות כאלה, צריך לפעול לפי ההנחיות הבאות.

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

    package com.example
    internal class ImportantInternalClass {
      internal val f: Int
      internal fun m() {}
    }
    

    המחלקות, השיטות והשדות של internal הם public בקובצי ה-.class שנוצרו על ידי המקמפל של Kotlin, לכן צריך להשתמש במילת המפתח public כמו שמוצג בדוגמה הבאה:

    -keepclassmembers public class com.example.ImportantInternalClass {
      public int f;
      public void m();
    }
    
  • כשמבצעים קומפילציה של איבר suspend, צריך להתאים את החתימה של קוד הבייט שעבר קומפילציה בכלל השמירה.

    לדוגמה, נניח שיש מחלקה קונקרטית של מאגר נתונים שמוגדרת ב-Kotlin באופן הבא:

    package com.example.repository
    
    import com.example.model.User
    
    class UserRepository {
        suspend fun fetchUser(id: String): User {
            // Implementation details...
        }
    }
    

    כשהקומפיילר של Kotlin מקמפל את המחלקה הזו לבייטקוד, suspend הפונקציות עוברות טרנספורמציה של Continuation-Passing Style‏ (CPS). המשתנה suspend מוסר, סוג ההחזרה משתנה ל-java.lang.Object, ופרמטר kotlin.coroutines.Continuation מצורף לחתימת השיטה כדי לנהל את מכונת המצבים האסינכרונית.

    חתימת המתודה שעברה קומפילציה בבייטקוד מופיעה כך:

    public final Object fetchUser(String id, Continuation<? super User> continuation);
    

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

    דוגמה שתואמת לחתימת ה-bytecode המקומפלת המדויקת:

    -keepclassmembers class com.example.repository.UserRepository {
      public java.lang.Object fetchUser(java.lang.String, kotlin.coroutines.Continuation);
    }
    

    דוגמה לשימוש ב-...:

    -keepclassmembers class com.example.repository.UserRepository {
      public java.lang.Object fetchUser(...);
    }
    

מפרט החבר

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

הגדרת חברים לפי אנוטציות

אתם יכולים להגדיר חברים לפי האנוטציות שלהם. בדומה למחלקות, מוסיפים את הקידומת @ לשם המלא של ההערה ב-Java. כך אפשר להשאיר רק את האיברים בכיתה שמסומנים בהערות ספציפיות. לדוגמה, כדי לשמור שיטות ושדות שמוגדרות להם הערות באמצעות @com.example.MyAnnotation:

-keep class com.example.MyClass {
  @com.example.MyAnnotation <methods>;
  @com.example.MyAnnotation <fields>;
}

אפשר לשלב את זה עם התאמת הערות ברמת הכיתה כדי ליצור כללים יעילים וממוקדים:

-keep class @com.example.ClassAnnotation * {
  @com.example.MethodAnnotation <methods>;
  @com.example.FieldAnnotation <fields>;
}

הפעולה הזו שומרת מחלקות עם הערות @ClassAnnotation, ובמחלקה הזו היא שומרת מתודות עם הערות @MethodAnnotation ושדות עם הערות @FieldAnnotation.

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

כדי להשתמש ברפלקציה בקוד שלכם, אתם יכולים גם להשתמש ב-Jetpack Keep. במקום לכתוב כללי שמירה שמטרגטים הערות, אתם מוסיפים הערות לקוד, ופלאגין של Gradle יוצר את כללי השמירה בשבילכם.

שיטות

התחביר לציון מתודה במפרט האיברים של כלל שמירה הוא:

[<access_modifier>] [<return_type>] <method_name>(<parameter_types>);

לדוגמה, כלל השמירה הבא שומר על מתודה ציבורית בשם getUserId() שמחזירה String.

-keep class com.example.model.UserData {
    public java.lang.String getUserId();
}

אפשר להשתמש ב-<methods> כקיצור דרך להתאמה לכל השיטות במחלקה, באופן הבא:

-keep class com.example.model.UserData {
    <methods>;
}

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

יצרנים

כדי לציין פונקציית constructor, משתמשים ב-<init>. התחביר לציון פונקציית constructor במפרט האיברים של כלל keep הוא:

[<access_modifier>] <init>(parameter_types);

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

-keep class com.example.ui.state.UserViewModel {
    public <init>(com.example.repository.UserDataRepository);
}

כדי לשמור את כל ה-constructors הציבוריים, אפשר להשתמש בדוגמה הבאה כהפניה:

-keep class com.example.ui.state.UserViewModel {
    public <init>(...);
}

שדות

התחביר לציון שדה במפרט החברים של כלל שמירה הוא:

[<access_modifier>...] [<type>] <field_name>;

לדוגמה, כלל השמירה הבא שומר שדה מחרוזת פרטי בשם userId ושדה מספר שלם סטטי ציבורי בשם STATUS_ACTIVE:

-keep class com.example.models.User {
    private java.lang.String userId;
    public static int STATUS_ACTIVE;
}

אפשר להשתמש ב-<fields> כקיצור דרך להתאמת כל השדות במחלקה, באופן הבא:

-keep class com.example.models.User {
    <fields>;
}

סוגים

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

סוגים פרימיטיביים

כדי לציין סוג פרימיטיבי, משתמשים במילת המפתח שלו ב-Java. ‫R8 מזהה את סוגי הנתונים הפרימיטיביים הבאים: boolean, ‏byte, ‏short, ‏char, ‏int, ‏long, ‏float, ‏double.

דוגמה לכלל עם סוג פרימיטיבי:

# Keeps a method that takes an int and a float as parameters.
-keepclassmembers class com.example.Calculator {
    public void setValues(int, float);
}

סוגים כלליים

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

לדוגמה, אם יש לכם את הקוד הבא עם סוג כללי לא מוגבל שמוגדר ב-Box.kt:

package com.myapp.data

class Box<T>(val item: T) {
    fun getItem(): T {
        return item
    }
}

אחרי מחיקת הסוג, T מוחלף ב-Object. כדי לשמור על ה-constructor וה-method של המחלקה, הכלל צריך להשתמש ב-java.lang.Object במקום ב-T הגנרי.

דוגמה לכלל שמירה:

# Keep the constructor and methods of the Box class.
-keep class com.myapp.data.Box {
    public init(java.lang.Object);
    public java.lang.Object getItem();
}

אם יש לכם את הקוד הבא עם סוג גנרי מוגבל ב-NumberBox.kt:

package com.myapp.data

// T is constrained to be a subtype of Number
class NumberBox<T : Number>(val number: T)

במקרה כזה, מחיקת הטיפוס מחליפה את T בגבול שלו, java.lang.Number.

דוגמה לכלל שמירה:

-keep class com.myapp.data.NumberBox {
    public init(java.lang.Number);
}

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

לדוגמה, בקוד הבא:

package com.myapp.data

data class UnpackOptions(val useHighPriority: Boolean)

// The generic Box class with UnpackOptions as the bounded type
class Box<T: UnpackOptions>(val item: T) {
}

אפשר להשתמש בכלל שמירה עם includedescriptorclasses כדי לשמור גם את המחלקה UnpackOptions וגם את השיטה של המחלקה Box באמצעות כלל אחד, באופן הבא:

-keep,includedescriptorclasses class com.myapp.data.Box {
    public <init>(com.myapp.data.UnpackOptions);
}

כדי לשמור פונקציה ספציפית שמעבדת רשימה של אובייקטים, צריך לכתוב כלל שתואם בדיוק לחתימה של הפונקציה. שימו לב: בגלל שסוגים גנריים נמחקים, פרמטר כמו List<Product> נראה כמו java.util.List.

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

package com.myapp.utils

import com.myapp.data.Product
import android.util.Log

class DataProcessor {
    // This is the function we want to keep
    fun processProducts(products: List<Product>) {
        Log.d("DataProcessor", "Processing ${products.size} products.")
        // Business logic ...
    }
}

// The data class used in the list (from the previous example)
package com.myapp.data
data class Product(val id: String, val name: String)

אפשר להשתמש בכלל השמירה הבא כדי להגן רק על הפונקציה processProducts:

-keep class com.myapp.utils.DataProcessor {
    public void processProducts(java.util.List);
}

סוגי מערכים

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

  • מערך כיתות חד-ממדי: java.lang.String[]
  • מערך פרימיטיבי דו-ממדי: int[][]

לדוגמה, אם יש לכם את הקוד הבא:

package com.example.data

class ImageProcessor {
  fun process(): ByteArray {
    // process image to return a byte array
  }
}

אפשר להשתמש בכלל השמירה הבא:

# Keeps a method that returns a byte array.
-keepclassmembers class com.example.data.ImageProcessor {
    public byte[] process();
}

דוגמאות

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

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

כדי לשמור רק את המחלקה, יחד עם ה-constructor שמוגדר כברירת מחדל, אבל לא איברים אחרים, משתמשים בקוד הבא:

-keep class com.myapp.MyClass

מומלץ תמיד לציין כמה חברים. לדוגמה, בקוד הבא שומרים את השדה הציבורי text ואת השיטה הציבורית updateText() במחלקה MyClass.

-keep class com.myapp.MyClass {
    public java.lang.String text;
    public void updateText(java.lang.String);
}

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

-keep public class com.example.api.ApiClient {
    public *;
}

דילוג על הגדרת החבר

אם לא מציינים את החבר, R8 שומר את constructor ברירת המחדל של המחלקה.

לדוגמה, אם כותבים -keep class com.example.MyClass או -keep class com.example.MyClass {}, ‏ R8 מתייחס אליהם כאילו כתבתם את הדברים הבאים:

-keep class com.example.MyClass{
  void <init>();
}

החרגת תבניות של שמות חברים

החל מ-Android Gradle Plugin‏ (AGP) 9.2.0, אפשר לשלול דפוסי שמות של חברים בכללי השמירה. כך אפשר לציין חברים שרוצים להשאיר או להחריג על סמך תבניות. כדי לשלול תבנית, מוסיפים סימן קריאה (!) לפני תבנית השם של החבר.

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

לדוגמה:

כדי לשמור את כל השיטות הציבוריות ב-com.example.MyClass, מלבד אלה שמסתיימות ב-ForTesting, משתמשים בכלל הבא:

-keepclassmembers class com.example.MyClass {
    public *** !*ForTesting(...);
}

פונקציות ברמת החבילה

כדי להפנות לפונקציה של Kotlin שמוגדרת מחוץ למחלקה (בדרך כלל נקראת פונקציה ברמה העליונה), חשוב להשתמש בשם שנוצר ב-Java למחלקה שנוספה באופן משתמע על ידי המהדר של Kotlin. שם המחלקה הוא שם הקובץ של Kotlin עם התוספת Kt. לדוגמה, אם יש לכם קובץ Kotlin בשם MyClass.kt שמוגדר כך:

package com.example.myapp.utils

// A top-level function not inside a class
fun isEmailValid(email: String): Boolean {
    return email.contains("@")
}

כדי לכתוב כלל שמירה לפונקציה isEmailValid, מפרט המחלקה צריך לטרגט את המחלקה שנוצרה MyClassKt:

-keep class com.example.myapp.utils.MyClassKt {
    public static boolean isEmailValid(java.lang.String);
}

תווים כלליים לחיפוש

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

תו כללי לחיפוש רלוונטי לכיתות או לחברים תיאור
** שניהם הכי רווחיים. התאמה לכל שם של סוג, כולל כל מספר של מפרידי חבילות. האפשרות הזו שימושית להתאמה של כל המחלקות בחבילה ובחבילות המשנה שלה.
* שניהם במפרטי מחלקות, התאמה לכל חלק של שם סוג שלא מכיל מפרידי חבילות (.)
במפרטי איברים, התאמה לכל שם של שיטה או שדה. כשמשתמשים בו לבד, הוא גם שם חלופי ל-**.
? שניהם התאמה לתו בודד כלשהו בשם של מחלקה או של חבר.
*** חברים תואם לכל סוג, כולל סוגים פרימיטיביים (כמו int), סוגי מחלקות (כמו java.lang.String) וסוגי מערכים בכל מימד (כמו byte[][]).
... חברים תואם לכל רשימת פרמטרים של שיטה.
% חברים תואם לכל סוג פרימיטיבי (כמו int,‏ float,‏ boolean או אחרים).

הנה כמה דוגמאות לשימוש בתווים הכלליים המיוחדים:

  • אם יש לכם כמה שיטות עם אותו שם שמקבלות סוגים פרימיטיביים שונים כקלט, אתם יכולים להשתמש ב-% כדי לכתוב כלל שמירה שישמור את כולן. לדוגמה, למחלקה DataStore הזו יש כמה שיטות setValue:

    class DataStore {
        fun setValue(key: String, value: Int) { ... }
        fun setValue(key: String, value: Boolean) { ... }
        fun setValue(key: String, value: Float) { ... }
    }
    

    כלל השמירה הבא שומר את כל השיטות:

    -keep class com.example.DataStore {
        public void setValue(java.lang.String, %);
    }
    
  • אם יש לכם כמה כיתות עם שמות ששונים זה מזה רק בתו אחד, אפשר להשתמש ב-? כדי לכתוב כלל שמירה שישמור את כולן. לדוגמה, אם יש לכם את המחלקות הבאות:

    com.example.models.UserV1 {...}
    com.example.models.UserV2 {...}
    com.example.models.UserV3 {...}
    

    כלל השמירה הבא שומר את כל המחלקות:

    -keep class com.example.models.UserV?
    
  • כדי להתאים למחלקות Example ו-AnotherExample (אם הן היו מחלקות ברמת הבסיס), אבל לא למחלקה com.foo.Example, משתמשים בכלל השמירה הבא:

    -keep class *Example
    
  • אם משתמשים ב-*, הוא פועל ככינוי ל-**. לדוגמה, כללי השמירה הבאים שקולים:

    -keepclasseswithmembers class * { public static void main(java.lang.String[];) }
    
    -keepclasseswithmembers class ** { public static void main(java.lang.String[];) }
    

כללי שמירה מותנים

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

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

התחביר הכללי של כלל שמירה מותנה הוא:

-if <class_specification_if> <keep_rule>

אם מפרט המחלקה בתנאי -if מכיל תווים כלליים לחיפוש (כמו * או **), רצף התווים שתואם לתו הכללי לחיפוש נלכד. אפשר להפנות למחרוזות האלה בכלל השמירה הבא באמצעות הפניות לאחור: <1> מפנה למחרוזת שנתפסה על ידי התו הכללי הראשון, <2> מפנה למחרוזת שנתפסה על ידי התו הכללי השני וכן הלאה.

לדוגמה, רכיב Jetpack Navigation יוצר מחלקות NavArgs להעברת ארגומנטים עם מניעת שגיאות הקלדה בין יעדים. כשמשתמשים ב-delegate של NavArgsLazy, הוא משתמש ב-reflection כדי למצוא ולהפעיל שיטה סטטית של fromBundle במחלקה NavArgs שנוצרה, כדי לבטל את ההמשגה של הארגומנטים. אם האפליקציה משתמשת ב-NavArgs, צריך לשמור רק את השיטה fromBundle למחלקות שמטמיעות את הממשק NavArgs.

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

# If a class implements NavArgs...
-if public class ** implements androidx.navigation.NavArgs
# ...then keep the fromBundle method of that matched class (<1>).
-keepclassmembers public class <1> {
    public static ** fromBundle(android.os.Bundle);
}

בדוגמה הזו, ** הוא התו הכללי הראשון והיחיד בתנאי -if. הוא תואם לשם הכיתה של כל כיתה שמטמיעה את androidx.navigation.NavArgs. המחרוזת שתואמת ל-** (במקרה הזה, שם הכיתה) נלכדת, ואפשר להפנות אליה באמצעות <1> בכלל הבא. לכן הכלל -keepclassmembers חל על כל מחלקה שמטמיעה את androidx.navigation.NavArgs שהותאמה על ידי התנאי -if. אם R8 לא מוצא מחלקות שמיישמות את NavArgs, הוא מתעלם מכלל השמירה הזה.

תרחיש נפוץ נוסף הוא שימוש בספריות סריאליזציה של JSON כמו Gson. אם המחלקות של מודל הנתונים משתמשות בהערה @SerializedName של Gson בשדה כלשהו, אפשר להשתמש בכלל מותנה כדי להגן על כל מחלקה כזו ועל האיברים שלה ש-Gson צריך בשביל רפלקציה:

# If a class has fields annotated with @SerializedName...
-if class ** { @com.google.gson.annotations.SerializedName <fields>; }
# ...then keep that class (<1>), its @SerializedName fields,
# and its constructors for Gson.
-keep class <1> {
    @com.google.gson.annotations.SerializedName <fields>;
    <init>(...);
}

הפניות חוזרות לוכדות מחרוזות, שיכולות להיות מחרוזות משנה של שמות מחלקות אם התו הכללי מתאים רק לחלק מהשם. לדוגמה, אם משתמשים ב--if class com.example.*X*, ‏R8 מתעד את מחרוזת המשנה שלפני X בתור <1> ואת מחרוזת המשנה שאחרי X בתור <2>. הכלל הבא משתמש בזה כדי למצוא שם כיתה שמכיל X ולשמור כיתה תואמת שבה X מוחלף ב-Y:

# If a class like com.example.PrefixXPostfix exists...
-if class com.example.*X*
# ...keep com.example.PrefixYPostfix.
-keep class com.example.<1>Y<2>

כללי שמירה מותנים ל-reflection

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

ספריית הניווט של Jetpack משתמשת ברפלקציה באמצעות הנציג NavArgsLazy כדי לקרוא לשיטה סטטית fromBundle במחלקות NavArgs שנוצרו להעברת ארגומנטים בטוחה מסוג. כדי לוודא שהשיטה הזו נשמרת רק להטמעות של NavArgs ולא לכל מחלקה, Jetpack Navigation כולל את כלל השמירה המותנה הבא:

# If a class implements NavArgs...
-if public class ** implements androidx.navigation.NavArgs
# ...then keep the fromBundle method of that matched class (<1>).
-keepclassmembers public class <1> {
    public static ** fromBundle(android.os.Bundle);
}

הכלל הזה שומר את fromBundle רק לכיתות שצריכות אותו, במקום לשמור אותו בכל הכיתות או לדרוש מכם לציין ידנית אילו כיתות צריכות אותו.

בדיקת שמות Java שנוצרו

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

  • APK Analyzer
  • פותחים את קובץ המקור של Kotlin ובודקים את קוד הביניים באמצעות Tools > Kotlin > Show Kotlin Bytecode > Decompile (כלים > Kotlin > הצגת קוד הביניים של Kotlin > פירוק).