Poniższe przykłady są oparte na typowych scenariuszach, w których używasz R8 do optymalizacji, ale potrzebujesz zaawansowanych wskazówek dotyczących tworzenia reguł keep.
Odbicie
Ogólnie rzecz biorąc, aby uzyskać optymalną wydajność, nie zalecamy używania odbicia. W niektórych sytuacjach może to być jednak nieuniknione. Poniższe przykłady zawierają wskazówki dotyczące reguł zachowywania w typowych scenariuszach, w których używane jest odbicie.
Odbicie z klasami wczytanymi według nazwy
Biblioteki często wczytują klasy dynamicznie, używając nazwy klasy jako String
.
R8 nie wykrywa jednak klas wczytywanych w ten sposób i może usunąć klasy, które uzna za nieużywane.
Rozważmy na przykład sytuację, w której masz bibliotekę i aplikację, która z niej korzysta. Kod pokazuje moduł wczytujący bibliotekę, który tworzy instancję interfejsu StartupTask
zaimplementowanego przez aplikację.
Kod biblioteki wygląda tak:
// The interface for a task that runs once.
interface StartupTask {
fun run()
}
// The library object that loads and executes the task.
object TaskRunner {
fun execute(className: String) {
// R8 won't retain classes specified by this string value at runtime
val taskClass = Class.forName(className)
val task = taskClass.getDeclaredConstructor().newInstance() as StartupTask
task.run()
}
}
Aplikacja, która korzysta z biblioteki, ma ten kod:
// The app's task to pre-cache data.
// R8 will remove this class because it's only referenced by a string.
class PreCacheTask : StartupTask {
override fun run() {
// This log will never appear if the class is removed by R8.
Log.d("AppTask", "Warming up the cache...")
}
}
fun onCreate() {
// The library is told to run the app's task by its name.
TaskRunner.execute("com.example.app.PreCacheTask")
}
W takim przypadku biblioteka powinna zawierać plik reguł przechowywania danych konsumentów z tymi regułami:
-keep class * implements com.example.library.StartupTask {
<init>();
}
Bez tej reguły R8 usunie PreCacheTask
z aplikacji, ponieważ nie używa ona tej klasy bezpośrednio, co spowoduje przerwanie integracji. Reguła znajduje klasy, które implementują interfejs StartupTask
biblioteki, i zachowuje je wraz z konstruktorem bez argumentów, co umożliwia bibliotece pomyślne utworzenie instancji i wykonanie PreCacheTask
.
Odczucia: ::class.java
Biblioteki mogą wczytywać klasy, przekazując bezpośrednio obiekt Class
. Jest to bardziej niezawodna metoda niż wczytywanie klas według nazwy. Spowoduje to utworzenie silnego odwołania do klasy, które R8 może wykryć. Chociaż zapobiega to usunięciu klasy przez R8, nadal musisz użyć reguły zachowywania, aby zadeklarować, że klasa jest tworzona przez odbicie, i chronić elementy, do których uzyskuje się dostęp przez odbicie, takie jak konstruktor.
Rozważmy na przykład taką sytuację, w której masz bibliotekę i aplikację, która jej używa. Loader biblioteki tworzy instancję interfejsu StartupTask
, przekazując bezpośrednio odwołanie do klasy.
Kod biblioteki wygląda tak:
// The interface for a task that runs once.
interface StartupTask {
fun run()
}
// The library object that loads and executes the task.
object TaskRunner {
fun execute(taskClass: Class<out StartupTask>) {
// The class isn't removed, but its constructor might be.
val task = taskClass.getDeclaredConstructor().newInstance()
task.run()
}
}
Aplikacja, która korzysta z biblioteki, ma ten kod:
// The app's task is to pre-cache data.
class PreCacheTask : StartupTask {
override fun run() {
Log.d("AppTask", "Warming up the cache...")
}
}
fun onCreate() {
// The library is given a direct reference to the app's task class.
TaskRunner.execute(PreCacheTask::class.java)
}
W takim przypadku biblioteka powinna zawierać plik reguł przechowywania danych konsumentów z tymi regułami:
# Allow any implementation of StartupTask to be removed if unused.
-keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask
# Keep the default constructor, which is called via reflection.
-keepclassmembers class * implements com.example.library.StartupTask {
<init>();
}
Te reguły zostały zaprojektowane tak, aby idealnie współpracować z tym typem odbicia, co pozwala na maksymalną optymalizację przy jednoczesnym zapewnieniu prawidłowego działania kodu. Reguły umożliwiają zaciemnianie nazwy klasy przez R8 oraz kompresowanie lub usuwanie implementacji klasy StartupTask
, jeśli aplikacja nigdy jej nie używa. W przypadku każdej implementacji, np. PrecacheTask
użytej w przykładzie, zachowują one jednak domyślny konstruktor (<init>()
), który musi wywołać Twoja biblioteka.
-keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask
: ta reguła jest kierowana na każdą klasę, która implementuje interfejsStartupTask
.-keep class * implements com.example.library.StartupTask
: zachowuje to każdą klasę (*
), która implementuje Twój interfejs.,allowobfuscation
: informuje to kompilator R8, że pomimo zachowania klasy może on zmienić jej nazwę lub ją zaciemnić. Jest to bezpieczne, ponieważ biblioteka nie korzysta z nazwy klasy, tylko bezpośrednio pobiera obiektClass
.,allowshrinking
: ten modyfikator informuje R8, że może usunąć klasę, jeśli nie jest używana. Dzięki temu R8 może bezpiecznie usunąć implementacjęStartupTask
, która nigdy nie jest przekazywana doTaskRunner.execute()
. W skrócie ta reguła oznacza, że jeśli aplikacja używa klasy, która implementujeStartupTask
, R8 zachowuje tę klasę. R8 może zmienić nazwę klasy, aby zmniejszyć jej rozmiar, lub usunąć ją, jeśli aplikacja jej nie używa.
-keepclassmembers class * implements com.example.library.StartupTask { <init>(); }
: Ta reguła kieruje reklamy na konkretnych członków klas zidentyfikowanych w pierwszej regule, w tym przypadku na konstruktora.-keepclassmembers class * implements com.example.library.StartupTask
: zachowuje określone elementy (metody, pola) klasy, która implementuje interfejsStartupTask
, ale tylko wtedy, gdy zachowywana jest sama zaimplementowana klasa.{ <init>(); }
: to selektor członków.<init>
to specjalna wewnętrzna nazwa konstruktora w kodzie bajtowym Javy. Ta część jest skierowana w szczególności na konstruktor domyślny bez argumentów.- Ta reguła jest kluczowa, ponieważ Twój kod wywołuje funkcję
getDeclaredConstructor().newInstance()
bez argumentów, co wywołuje domyślny konstruktor. Bez tej reguły R8 widzi, że żaden kod nie wywołuje bezpośrednio funkcjinew PreCacheTask()
, zakłada, że konstruktor nie jest używany, i usuwa go. Powoduje to awarię aplikacji w czasie działania z błędemInstantiationException
.
Odbicie na podstawie adnotacji metody
Biblioteki często definiują adnotacje, których deweloperzy używają do tagowania metod lub pól.
Biblioteka używa następnie odbicia, aby znaleźć te elementy z adnotacjami w czasie działania programu. Na przykład adnotacja @OnLifecycleEvent
służy do znajdowania wymaganych metod w czasie działania programu.
Rozważmy na przykład taką sytuację: masz bibliotekę i aplikację, która z niej korzysta. Przykład pokazuje magistralę zdarzeń, która znajduje i wywołuje metody oznaczone adnotacją @OnEvent
.
Kod biblioteki wygląda tak:
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class OnEvent
class EventBus {
fun dispatch(listener: Any) {
// Find all methods annotated with @OnEvent and invoke them
listener::class.java.declaredMethods.forEach { method ->
if (method.isAnnotationPresent(OnEvent::class.java)) {
try {
method.invoke(listener)
} catch (e: Exception) { /* ... */ }
}
}
}
}
Aplikacja, która korzysta z biblioteki, ma ten kod:
class MyEventListener {
@OnEvent
fun onSomethingHappened() {
// This method will be removed by R8 without a keep rule
Log.d(TAG, "Event received!")
}
}
fun onCreate() {
// Instantiate the listener and the event bus
val listener = MyEventListener()
val eventBus = EventBus()
// Dispatch the listener to the event bus
eventBus.dispatch(listener)
}
Biblioteka powinna zawierać plik reguł zachowywania konsumenta, który automatycznie zachowuje wszystkie metody korzystające z jej adnotacji:
-keepattributes RuntimeVisibleAnnotations
-keep @interface com.example.library.OnEvent;
-keepclassmembers class * {
@com.example.library.OnEvent <methods>;
}
-keepattributes RuntimeVisibleAnnotations
: ta reguła zachowuje adnotacje, które mają być odczytywane w czasie działania programu.-keep @interface com.example.library.OnEvent
: ta reguła zachowuje samą klasę adnotacjiOnEvent
.-keepclassmembers class * {@com.example.library.OnEvent <methods>;}
: ta reguła zachowuje klasę i określonych członków tylko wtedy, gdy klasa jest używana i zawiera tych członków.-keepclassmembers
: ta reguła zachowuje klasę i określonych członków tylko wtedy, gdy klasa jest używana i zawiera tych członków.class *
: reguła ma zastosowanie do dowolnej klasy.@com.example.library.OnEvent <methods>;
: zachowuje każdą klasę, która ma co najmniej jedną metodę (<methods>
) z adnotacją@com.example.library.OnEvent
, a także same metody z adnotacjami.
Refleksja na podstawie adnotacji do klasy
Biblioteki mogą używać refleksji do skanowania klas, które mają określoną adnotację. W tym przypadku klasa wykonująca zadania znajduje wszystkie klasy oznaczone adnotacją ReflectiveExecutor
za pomocą odbicia i wykonuje metodę execute
.
Rozważmy na przykład sytuację, w której masz bibliotekę i aplikację, która z niej korzysta.
Biblioteka zawiera ten kod:
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class ReflectiveExecutor
class TaskRunner {
fun process(task: Any) {
val taskClass = task::class.java
if (taskClass.isAnnotationPresent(ReflectiveExecutor::class.java)) {
val methodToCall = taskClass.getMethod("execute")
methodToCall.invoke(task)
}
}
}
Aplikacja, która korzysta z biblioteki, ma ten kod:
// In consumer app
@ReflectiveExecutor
class ImportantBackgroundTask {
fun execute() {
// This class will be removed by R8 without a keep rule
Log.e("ImportantBackgroundTask", "Executing the important background task...")
}
}
// Usage of ImportantBackgroundTask
fun onCreate(){
val task = ImportantBackgroundTask()
val runner = TaskRunner()
runner.process(task)
}
Biblioteka korzysta z odbicia, aby uzyskać dostęp do określonych klas, dlatego powinna zawierać plik reguł zachowywania dla konsumentów z tymi regułami:
# Retain annotation metadata for runtime reflection.
-keepattributes RuntimeVisibleAnnotations
# Keep the annotation interface itself.
-keep @interface com.example.library.ReflectiveExecutor
# Keep the execute method in the classes which are being used
-keepclassmembers @com.example.library.ReflectiveExecutor class * {
public void execute();
}
Ta konfiguracja jest bardzo wydajna, ponieważ dokładnie informuje R8, co należy zachować.
Odbicie do obsługi opcjonalnych zależności
Częstym zastosowaniem refleksji jest tworzenie słabych zależności między biblioteką podstawową a opcjonalną biblioteką dodatkową. Biblioteka podstawowa może sprawdzić, czy dodatek jest uwzględniony w aplikacji, a jeśli tak, może włączyć dodatkowe funkcje. Dzięki temu możesz dostarczać moduły dodatkowe bez wymuszania bezpośredniej zależności biblioteki podstawowej od tych modułów.
Biblioteka podstawowa używa refleksji (Class.forName
), aby wyszukać konkretną klasę według nazwy. Jeśli zajęcia zostaną znalezione, funkcja zostanie włączona. W przeciwnym razie nie powoduje to błędu.
Rozważmy na przykład ten kod, w którym podstawowy tag AnalyticsManager
sprawdza, czy występuje opcjonalna klasa VideoEventTracker
, aby włączyć analizę filmów.
Biblioteka podstawowa zawiera ten kod:
object AnalyticsManager {
private const val VIDEO_TRACKER_CLASS = "com.example.analytics.video.VideoEventTracker"
fun initialize() {
try {
// Attempt to load the optional module's class using reflection
Class.forName(VIDEO_TRACKER_CLASS).getDeclaredConstructor().newInstance()
Log.d(TAG, "Video tracking enabled.")
} catch (e: ClassNotFoundException) {
Log.d(TAG,"Video tracking module not found. Skipping.")
} catch (e: Exception) {
Log.e(TAG, e.printStackTrace())
}
}
}
Opcjonalna biblioteka filmów ma ten kod:
package com.example.analytics.video
class VideoEventTracker {
// This constructor must be kept for the reflection call to succeed.
init { /* ... */ }
}
Deweloper biblioteki opcjonalnej odpowiada za podanie niezbędnej reguły zachowania konsumenta. Ta reguła zachowywania zapewnia, że każda aplikacja korzystająca z opcjonalnej biblioteki zachowuje kod potrzebny bibliotece podstawowej do wyszukiwania.
# In the video library's consumer keep rules file
-keep class com.example.analytics.video.VideoEventTracker {
<init>();
}
Bez tej reguły R8 prawdopodobnie usunie VideoEventTracker
z biblioteki opcjonalnej, ponieważ nic w tym module nie używa go bezpośrednio. Reguła zachowywania zachowuje klasę i jej konstruktor, dzięki czemu biblioteka podstawowa może ją utworzyć.
Reflection do uzyskiwania dostępu do prywatnych elementów członkowskich
Używanie odbicia do uzyskiwania dostępu do kodu prywatnego lub chronionego, który nie jest częścią publicznego interfejsu API biblioteki, może powodować poważne problemy. Taki kod może ulec zmianie bez powiadomienia, co może prowadzić do nieoczekiwanego działania lub awarii aplikacji.
Jeśli używasz odbicia w przypadku niepublicznych interfejsów API, możesz napotkać te problemy:
- Zablokowane aktualizacje: zmiany w kodzie prywatnym lub chronionym mogą uniemożliwić aktualizację do wyższych wersji biblioteki.
- Utracone korzyści: możesz nie mieć dostępu do nowych funkcji, ważnych poprawek błędów powodujących awarie lub istotnych aktualizacji zabezpieczeń.
Optymalizacje R8 i odzwierciedlenie
Jeśli musisz odzwierciedlić prywatny lub chroniony kod biblioteki, zwróć szczególną uwagę na optymalizacje R8. Jeśli nie ma bezpośrednich odwołań do tych elementów, R8 może uznać, że nie są one używane, a następnie je usunąć lub zmienić ich nazwy.
Może to prowadzić do awarii podczas działania, często z wprowadzającymi w błąd komunikatami o błędach, takimi jak NoSuchMethodException
lub NoSuchFieldException
.
Rozważmy na przykład taką sytuację, która pokazuje, jak można uzyskać dostęp do prywatnego pola z klasy biblioteki.
Biblioteka, która nie należy do Ciebie, ma ten kod:
class LibraryClass {
private val secretMessage = "R8 will remove me"
}
Aplikacja zawiera ten kod:
fun accessSecretMessage(instance: LibraryClass) {
// Use Java reflection from Kotlin to access the private field
val secretField = instance::class.java.getDeclaredField("secretMessage")
secretField.isAccessible = true
// This will crash at runtime with R8 enabled
val message = secretField.get(instance) as String
}
Dodaj w aplikacji regułę -keep
, aby zapobiec usunięciu pola prywatnego przez R8:
-keepclassmembers class com.example.LibraryClass {
private java.lang.String secretMessage;
}
-keepclassmembers
: zachowuje określonych członków klasy tylko wtedy, gdy sama klasa jest zachowywana.class com.example.LibraryClass
: wskazuje dokładną klasę zawierającą pole.private java.lang.String secretMessage;
: identyfikuje konkretne pole prywatne według nazwy i typu.
Java Native Interface (JNI)
Optymalizacje R8 mogą powodować problemy podczas pracy z wywołaniami zwrotnymi z kodu natywnego (C/C++) do Javy lub Kotlina. Chociaż odwrotna sytuacja też może powodować problemy (wywołania zwrotne z kodu Java lub Kotlin do kodu natywnego), domyślny plik proguard-android-optimize.txt
zawiera tę regułę, aby wywołania zwrotne działały prawidłowo. Ta reguła zapobiega przycinaniu metod natywnych.
-keepclasseswithmembernames,includedescriptorclasses class * {
native <methods>;
}
Interakcja z kodem natywnym za pomocą interfejsu Java Native Interface (JNI)
Gdy aplikacja używa JNI do wywoływania zwrotnego z kodu natywnego (C/C++) do Javy lub Kotlin, R8 nie może zobaczyć, które metody są wywoływane z kodu natywnego. Jeśli w aplikacji nie ma bezpośrednich odwołań do tych metod, R8 błędnie zakłada, że nie są one używane, i usuwa je, co powoduje awarię aplikacji.
Poniższy przykład pokazuje klasę Kotlin z metodą, która ma być wywoływana z biblioteki natywnej. Biblioteka natywna tworzy instancję typu aplikacji i przekazuje dane z kodu natywnego do kodu Kotlin.
package com.example.models
// This class is used in the JNI bridge method signature
data class NativeData(val id: Int, val payload: String)
package com.example.app
// In package com.example.app
class JniBridge {
/**
* This method is called from the native side.
* R8 will remove it if it's not kept.
*/
fun onNativeEvent(data: NativeData) {
Log.d(TAG, "Received event from native code: $data")
}
// Use 'external' to declare a native method
external fun startNativeProcess()
companion object {
init {
// Load the native library
System.loadLibrary("my-native-lib")
}
}
}
W takim przypadku musisz poinformować R8, aby zapobiec optymalizacji typu aplikacji. Jeśli metody wywoływane z kodu natywnego używają w swoich sygnaturach własnych klas jako parametrów lub typów zwracanych, musisz też sprawdzić, czy te klasy nie zostały zmienione.
Dodaj do aplikacji te reguły zachowywania:
-keepclassmembers,includedescriptorclasses class com.example.JniBridge {
public void onNativeEvent(com.example.model.NativeData);
}
-keep class NativeData{
<init>(java.lang.Integer, java.lang.String);
}
Te reguły zachowywania uniemożliwiają R8 usunięcie lub zmianę nazwy metody onNativeEvent
oraz – co najważniejsze – jej typu parametru.
-keepclassmembers,includedescriptorclasses class com.example.JniBridge{ public void onNativeEvent(com.example.model.NativeData);}
: Zachowuje określonych członków klasy tylko wtedy, gdy klasa jest najpierw tworzona w kodzie Kotlin lub Java. Informuje R8, że aplikacja używa klasy i powinna zachować określonych członków klasy.-keepclassmembers
: zachowuje tylko określonych członków klasy, jeśli klasa jest najpierw tworzona w kodzie Kotlin lub Java. Informuje R8, że aplikacja używa klasy i powinna zachować określonych członków klasy.class com.example.JniBridge
: wskazuje dokładną klasę zawierającą pole.includedescriptorclasses
: ten modyfikator zachowuje też wszystkie klasy znalezione w sygnaturze lub deskryptorze metody. W tym przypadku zapobiega to zmianie nazwy lub usunięciu klasycom.example.models.NativeData
przez R8, ponieważ jest ona używana jako parametr. JeśliNativeData
zostanie zmieniona (np. naa.a
), sygnatura metody przestanie pasować do oczekiwań kodu natywnego, co spowoduje awarię.public void onNativeEvent(com.example.models.NativeData);
: określa dokładny podpis Java metody, która ma zostać zachowana.
-keep class NativeData{<init>(java.lang.Integer, java.lang.String);}
:includedescriptorclasses
dba o to, aby klasaNativeData
została zachowana, ale wszystkie elementy (pola lub metody) w klasieNativeData
dostępne bezpośrednio z natywnego kodu JNI wymagają własnych reguł zachowywania.-keep class NativeData
: ten selektor kieruje reklamy na klasę o nazwieNativeData
, a blok określa, których członków klasyNativeData
należy zachować.<init>(java.lang.Integer, java.lang.String)
: jest to sygnatura konstruktora. jednoznacznie identyfikuje konstruktor, który przyjmuje 2 parametry: pierwszy toInteger
, a drugi toString
.
Połączenia z platformy pośredniej
Przenoszenie danych za pomocą implementacji Parcelable
Platforma Androida używa odbicia do tworzenia instancji obiektów Parcelable
. W nowoczesnym programowaniu w Kotlinie należy używać kotlin-parcelize
wtyczki, która automatycznie generuje niezbędną implementację Parcelable
, w tym pole CREATOR
i metody wymagane przez framework.
Rozważmy na przykład sytuację, w której wtyczka kotlin-parcelize
służy do utworzenia klasy Parcelable
:
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
// Add the @Parcelize annotation to your data class
@Parcelize
data class UserData(
val name: String,
val age: Int
) : Parcelable
W tym przypadku nie ma zalecanej reguły przechowywania. Wtyczka Gradle automatycznie generuje wymagane reguły zachowywania dla klas, które oznaczysz adnotacją @Parcelize
.kotlin-parcelize
Upraszcza to proces, ponieważ zapewnia, że wygenerowane CREATOR
i konstruktory są zachowywane na potrzeby wywołań refleksji w ramach Androida.
Jeśli ręcznie napiszesz klasę Parcelable
w Kotlinie bez użycia @Parcelize
, musisz zadbać o pole CREATOR
i konstruktor, który akceptuje Parcel
. Jeśli o tym zapomnisz, aplikacja ulegnie awarii, gdy system spróbuje zdeserializować obiekt. Używanie wartości @Parcelize
to standardowa i bezpieczniejsza metoda.
Korzystając z wtyczki kotlin-parcelize
, pamiętaj o tych kwestiach:
- Wtyczka automatycznie tworzy
CREATOR
pola podczas kompilacji. - Plik
proguard-android-optimize.txt
zawiera niezbędnekeep
reguły, które pozwalają zachować te pola w celu zapewnienia prawidłowego działania. - Deweloperzy aplikacji muszą sprawdzić, czy wszystkie wymagane
keep
reguły są obecne, zwłaszcza w przypadku implementacji niestandardowych lub zależności od innych firm.