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

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

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


-<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 בקבצים שנוצרו על ידי מהדר Kotlin, ולכן צריך להשתמש במילת המפתח public כמו בדוגמה הבאה:.class

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

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

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

אפשר להשתמש בכלל keep עם 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 שומר את constructor ברירת המחדל של המחלקה.

לדוגמה, אם כותבים -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 > דהקומפילציה).