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