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

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

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


-<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 להעביר מחלקות לחבילות שונות, כולל חבילת ברירת המחדל (הבסיסית).

מפרט הכיתה

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

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

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

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

-keep class com.google.android.material.button.MaterialButton { *; }

Subclasses and implementations

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

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

    package com.example
    internal class ImportantInternalClass {
      internal 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(...);
    }
    

מפרט המנוי

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

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

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

שיטות

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

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

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

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

סוגים

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

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

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

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