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

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

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


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

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

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

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

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

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

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

אפשרות השמירה מצמצם את הכיתות מטשטש כיתות הקטנת חברי הקבוצה טשטוש של חברים
keep
keepclassmembers
keepclasseswithmembers
keepnames
keepclassmembernames
keepclasseswithmembernames

שמירת ערך מקדם של אפשרות

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

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

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

מפרט הכיתה

בכל כלל שמירה צריך לציין מחלקה (כולל ממשק, enum ומחלקות הערות). אפשר גם להגביל את הכלל על סמך הערות, על ידי ציון מחלקת-על או ממשק שהוטמע, או על ידי ציון משנה הגישה למחלקה. כל המחלקות, כולל מחלקות ממרחב השמות java.lang כמו java.lang.String, צריכות להיות מוגדרות באמצעות השם המלא שלהן ב-Java. כדי להבין באילו שמות צריך להשתמש, בודקים את קוד הבייט באמצעות הכלים שמתוארים במאמר בדיקת שמות 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, צריך להתייחס אליהם כאל public. לדוגמה, נבחן את קוד המקור הבא ב-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 עובר קומפילציה, צריך להתאים את החתימה שעברה קומפילציה לכלל השמירה.

    לדוגמה, אם הגדרתם את הפונקציה fetchUser כמו בקטע הקוד הבא:

    suspend fun fetchUser(id: String): User
    

    אחרי ההידור, החתימה שלו בבייטקוד נראית כך:

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

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

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

    -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 משתמשת במנגנון הזה.

שיטות

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

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

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

-keep class com.example.MyView {
    public void setLabel(java.lang.String);
}

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

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

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

יצרנים

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

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

לדוגמה, כלל השמירה הבא שומר בנאי View מותאם אישית שמקבל Context ו-AttributeSet.

-keep class com.example.ui.MyCustomView {
    public <init>(android.content.Context, android.util.AttributeSet);
}

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

-keep class com.example.ui.MyCustomView {
    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) והשיטה של המחלקה, צריך להשתמש ב-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 { *; }

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

-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 שומר את בנאי ברירת המחדל של המחלקה.

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

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

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

כדי להפנות לפונקציית 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[];) }
    

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

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

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