보관 규칙 사용 사례 및 예시

다음 예는 최적화를 위해 R8을 사용하지만 keep 규칙을 작성하기 위한 고급 안내가 필요한 일반적인 시나리오를 기반으로 합니다.

반사

일반적으로 최적의 성능을 위해 리플렉션을 사용하는 것은 권장되지 않습니다. 그러나 특정 시나리오에서는 피할 수 없을 수 있습니다. 다음 예는 리플렉션을 사용하는 일반적인 시나리오에서 keep 규칙에 관한 안내를 제공합니다.

이름으로 로드된 클래스를 사용하는 리플렉션

라이브러리는 클래스 이름을 String으로 사용하여 클래스를 동적으로 로드하는 경우가 많습니다. 그러나 R8은 이러한 방식으로 로드된 클래스를 감지할 수 없으며 사용되지 않는 것으로 간주되는 클래스를 삭제할 수 있습니다.

예를 들어 라이브러리와 라이브러리를 사용하는 앱이 있는 다음 시나리오를 살펴보세요. 코드는 앱에서 구현한 StartupTask 인터페이스를 인스턴스화하는 라이브러리 로더를 보여줍니다.

라이브러리 코드는 다음과 같습니다.

// 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()
    }
}

라이브러리를 사용하는 앱에는 다음 코드가 있습니다.

// 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")
}

이 시나리오에서 라이브러리는 다음 keep 규칙이 포함된 소비자 keep 규칙 파일을 포함해야 합니다.

-keep class * implements com.example.library.StartupTask {
    <init>();
}

이 규칙이 없으면 앱이 클래스를 직접 사용하지 않으므로 R8은 앱에서 PreCacheTask를 삭제하여 통합을 중단합니다. 이 규칙은 라이브러리의 StartupTask 인터페이스를 구현하는 클래스를 찾아 인수가 없는 생성자와 함께 보존하여 라이브러리가 PreCacheTask를 성공적으로 인스턴스화하고 실행할 수 있도록 합니다.

::class.java를 사용하는 리플렉션

라이브러리는 앱이 Class 객체를 직접 전달하도록 하여 클래스를 로드할 수 있습니다. 이는 이름으로 클래스를 로드하는 것보다 더 강력한 방법입니다. 이렇게 하면 R8이 감지할 수 있는 클래스에 대한 강력한 참조가 생성됩니다. 그러나 이렇게 하면 R8이 클래스를 삭제하지 못하도록 방지할 수 있지만 클래스가 리플렉션 방식으로 인스턴스화되고 생성자와 같이 리플렉션 방식으로 액세스되는 멤버를 보호하도록 선언하려면 keep 규칙을 사용해야 합니다.

예를 들어 라이브러리와 라이브러리를 사용하는 앱이 있는 다음 시나리오를 살펴보세요. 라이브러리 로더는 클래스 참조를 직접 전달하여 StartupTask 인터페이스를 인스턴스화합니다.

라이브러리 코드는 다음과 같습니다.

// 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()
    }
}

라이브러리를 사용하는 앱에는 다음 코드가 있습니다.

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

이 시나리오에서 라이브러리는 다음 keep 규칙이 포함된 소비자 keep 규칙 파일을 포함해야 합니다.

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

이러한 규칙은 이러한 유형의 리플렉션과 완벽하게 작동하도록 설계되어 코드가 올바르게 작동하도록 하면서 최대한의 최적화를 허용합니다. 이 규칙을 사용하면 앱이 StartupTask 클래스를 사용하지 않는 경우 R8이 클래스 이름을 난독화하고 StartupTask 클래스의 구현을 축소하거나 삭제할 수 있습니다. 그러나 예에 사용된 PrecacheTask와 같은 구현의 경우 라이브러리가 호출해야 하는 기본 생성자 (<init>())를 보존합니다.

  • -keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask: 이 규칙은 인터페이스를 구현하는 모든 클래스를 타겟팅합니다.StartupTask
    • -keep class * implements com.example.library.StartupTask: This 인터페이스를 구현하는 모든 클래스 (*)를 보존합니다.
    • ,allowobfuscation: 이 규칙은 클래스를 유지하더라도 R8이 클래스의 이름을 바꾸거나 난독화할 수 있다고 R8에 지시합니다. 라이브러리가 클래스 이름에 의존하지 않고 Class 객체를 직접 가져오므로 안전합니다.
    • ,allowshrinking: 이 수정자는 사용되지 않는 경우 R8이 클래스를 삭제할 수 있다고 R8에 지시합니다. 이렇게 하면 R8이 TaskRunner.execute()에 전달되지 않는 StartupTask의 구현을 안전하게 삭제할 수 있습니다. 간단히 말해 이 규칙은 다음과 같이 의미합니다. 앱이 StartupTask를 구현하는 클래스를 사용하는 경우 R8은 클래스를 유지합니다. R8은 클래스 이름을 바꿔 크기를 줄일 수 있으며 앱이 클래스를 사용하지 않는 경우 삭제할 수 있습니다.
  • -keepclassmembers class * implements com.example.library.StartupTask { <init>(); }: 이 규칙은 첫 번째 규칙에서 식별된 클래스의 특정 멤버(이 경우 생성자)를 타겟팅합니다.
    • -keepclassmembers class * implements com.example.library.StartupTask: This preserves specific members (methods, fields) of the class that implements StartupTask interface, but only if the implemented class itself is being kept.
    • { <init>(); }: 멤버 선택기입니다. <init>은 Java 바이트 코드에서 생성자의 특수한 내부 이름입니다. 이 부분은 특히 인수가 없는 기본 생성자를 타겟팅합니다.
    • 이 규칙은 코드가 인수 없이 getDeclaredConstructor().newInstance()를 호출하여 기본 생성자를 리플렉션 방식으로 호출하기 때문에 중요합니다. 이 규칙이 없으면 R8은 코드가 new PreCacheTask()를 직접 호출하지 않는 것을 확인하고 생성자가 사용되지 않는다고 가정하여 삭제합니다. 이로 인해 런타임에 앱이 InstantiationException과 함께 비정상 종료됩니다.

메서드 주석을 기반으로 하는 리플렉션

라이브러리는 개발자가 메서드 또는 필드에 태그를 지정하는 데 사용하는 주석을 정의하는 경우가 많습니다. 그런 다음 라이브러리는 리플렉션을 사용하여 런타임에 이러한 주석이 추가된 멤버를 찾습니다. 예를 들어 @OnLifecycleEvent 주석은 런타임에 필요한 메서드를 찾는 데 사용됩니다.

예를 들어 라이브러리와 라이브러리를 사용하는 앱이 있는 다음 시나리오를 살펴보세요. 이 예는 @OnEvent 주석이 추가된 메서드를 찾고 호출하는 이벤트 버스를 보여줍니다.

라이브러리 코드는 다음과 같습니다.

@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) { /* ... */ }
            }
        }
    }
}

라이브러리를 사용하는 앱에는 다음 코드가 있습니다.

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

라이브러리는 주석을 사용하는 메서드를 자동으로 보존하는 소비자 keep 규칙 파일을 포함해야 합니다.

-keepattributes RuntimeVisibleAnnotations
-keep @interface com.example.library.OnEvent;
-keepclassmembers class * {
    @com.example.library.OnEvent <methods>;
}
  • -keepattributes RuntimeVisibleAnnotations: 이 규칙은 런타임에 읽을 수 있도록 주석을 보존합니다.
  • -keep @interface com.example.library.OnEvent: 이 규칙은 OnEvent 주석 클래스 자체를 보존합니다.
  • -keepclassmembers class * {@com.example.library.OnEvent <methods>;}: 이 규칙은 클래스가 사용 중이고 클래스에 이러한 멤버가 포함되어 있는 경우에만 클래스와 특정 멤버를 보존합니다.
    • -keepclassmembers: 이 규칙은 클래스가 사용 중이고 클래스에 이러한 멤버가 포함되어 있는 경우에만 클래스와 특정 멤버를 보존합니다.
    • class *: 이 규칙은 모든 클래스에 적용됩니다.
    • @com.example.library.OnEvent <methods>;: 메서드 (<methods>)가 하나 이상 있는 클래스를 보존하고 주석이 추가된 메서드 자체도 보존합니다.@com.example.library.OnEvent

클래스 주석을 기반으로 하는 리플렉션

라이브러리는 리플렉션을 사용하여 특정 주석이 있는 클래스를 검색할 수 있습니다. 이 경우 작업 실행기 클래스는 리플렉션을 사용하여 ReflectiveExecutor 주석이 추가된 모든 클래스를 찾고 execute 메서드를 실행합니다.

예를 들어 라이브러리와 라이브러리를 사용하는 앱이 있는 다음 시나리오를 살펴보세요.

라이브러리에는 다음 코드가 있습니다.

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

라이브러리를 사용하는 앱에는 다음 코드가 있습니다.

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

라이브러리는 리플렉션을 사용하여 특정 클래스를 리플렉션 방식으로 가져오므로 라이브러리는 다음 keep 규칙이 포함된 소비자 keep 규칙 파일을 포함해야 합니다.

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

이 구성은 R8에 보존할 항목을 정확하게 알려주므로 매우 효율적입니다.

선택적 종속 항목을 지원하는 리플렉션

리플렉션의 일반적인 사용 사례는 핵심 라이브러리와 선택적 애드온 라이브러리 간에 소프트 종속 항목을 만드는 것입니다. 핵심 라이브러리는 애드온이 앱에 포함되어 있는지 확인할 수 있으며 포함되어 있는 경우 추가 기능을 사용 설정할 수 있습니다. 이렇게 하면 핵심 라이브러리가 애드온 모듈에 직접 종속되지 않도록 하면서 애드온 모듈을 제공할 수 있습니다.

핵심 라이브러리는 리플렉션 (Class.forName)을 사용하여 이름으로 특정 클래스를 찾습니다. 클래스가 발견되면 기능이 사용 설정됩니다. 그렇지 않으면 정상적으로 실패합니다.

예를 들어 핵심 AnalyticsManager가 선택적 VideoEventTracker 클래스를 확인하여 동영상 분석을 사용 설정하는 다음 코드를 살펴보세요.

핵심 라이브러리에는 다음 코드가 있습니다.

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

선택적 동영상 라이브러리에는 다음 코드가 있습니다.

package com.example.analytics.video

class VideoEventTracker {
    // This constructor must be kept for the reflection call to succeed.
    init { /* ... */ }
}

선택적 라이브러리의 개발자는 필요한 소비자 keep 규칙을 제공해야 합니다. 이 keep 규칙은 선택적 라이브러리를 사용하는 모든 앱이 핵심 라이브러리가 찾는 데 필요한 코드를 보존하도록 합니다.

# In the video library's consumer keep rules file
-keep class com.example.analytics.video.VideoEventTracker {
    <init>();
}

이 규칙이 없으면 R8은 해당 모듈의 어떤 항목도 직접 사용하지 않으므로 선택적 라이브러리에서 VideoEventTracker를 삭제할 가능성이 높습니다. keep 규칙은 클래스와 생성자를 보존하여 핵심 라이브러리가 성공적으로 인스턴스화할 수 있도록 합니다.

비공개 멤버에 액세스하는 리플렉션

리플렉션을 사용하여 라이브러리의 공개 API에 포함되지 않은 비공개 또는 보호된 코드에 액세스하면 심각한 문제가 발생할 수 있습니다. 이러한 코드는 예고 없이 변경될 수 있으며 이로 인해 애플리케이션에서 예기치 않은 동작이나 비정상 종료가 발생할 수 있습니다.

비공개 API에 리플렉션을 사용하는 경우 다음과 같은 문제가 발생할 수 있습니다.

  • 업데이트 차단됨: 비공개 또는 보호된 코드가 변경되면 더 높은 라이브러리 버전으로 업데이트하지 못할 수 있습니다.
  • 혜택 누락됨: 새로운 기능, 중요한 비정상 종료 수정 또는 필수 보안 업데이트를 놓칠 수 있습니다.

R8 최적화 및 리플렉션

라이브러리의 비공개 또는 보호된 코드를 리플렉션해야 하는 경우 R8의 최적화에 주의하세요. 이러한 멤버에 대한 직접 참조가 없으면 R8은 이러한 멤버가 사용되지 않는다고 가정하고 삭제하거나 이름을 바꿀 수 있습니다. 이로 인해 런타임 비정상 종료가 발생할 수 있으며 종종 NoSuchMethodException 또는 NoSuchFieldException과 같은 오해의 소지가 있는 오류 메시지가 표시됩니다.

예를 들어 라이브러리 클래스에서 비공개 필드에 액세스하는 방법을 보여주는 다음 시나리오를 살펴보세요.

소유하지 않은 라이브러리에는 다음 코드가 있습니다.

class LibraryClass {
    private val secretMessage = "R8 will remove me"
}

앱에는 다음 코드가 있습니다.

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
}

앱에 -keep 규칙을 추가하여 R8이 비공개 필드를 삭제하지 못하도록 합니다.

-keepclassmembers class com.example.LibraryClass {
    private java.lang.String secretMessage;
}
  • -keepclassmembers: 클래스 자체가 유지되는 경우에만 클래스의 특정 멤버를 보존합니다.
  • class com.example.LibraryClass: 필드가 포함된 정확한 클래스를 타겟팅합니다.
  • private java.lang.String secretMessage;: 이름과 유형으로 특정 비공개 필드를 식별합니다.

Java 네이티브 인터페이스 (JNI)

R8의 최적화는 네이티브 (C/C++ 코드)에서 Java 또는 Kotlin으로의 업콜을 사용할 때 문제가 있을 수 있습니다. Java 또는 Kotlin에서 네이티브 코드로의 다운콜에도 문제가 있을 수 있지만 기본 파일 proguard-android-optimize.txt에는 다운콜이 작동하도록 유지하는 다음 규칙이 포함되어 있습니다. 이 규칙은 네이티브 메서드가 트리밍되지 않도록 보호합니다.

-keepclasseswithmembernames,includedescriptorclasses class * {
  native <methods>;
}

Java 네이티브 인터페이스 (JNI)를 통한 네이티브 코드와의 상호작용

앱이 JNI를 사용하여 네이티브 (C/C++) 코드에서 Java 또는 Kotlin으로 업콜을 실행하는 경우 R8은 네이티브 코드에서 호출되는 메서드를 확인할 수 없습니다. 앱에 이러한 메서드에 대한 직접 참조가 없으면 R8은 이러한 메서드가 사용되지 않는다고 잘못 가정하고 삭제하여 앱이 비정상 종료됩니다.

다음 예는 네이티브 라이브러리에서 호출할 메서드가 있는 Kotlin 클래스를 보여줍니다. 네이티브 라이브러리는 애플리케이션 유형을 인스턴스화하고 네이티브 코드에서 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")
        }
    }
}

이 경우 R8에 애플리케이션 유형이 최적화되지 않도록 알려야 합니다. 또한 네이티브 코드에서 호출되는 메서드가 서명에서 자체 클래스를 매개변수 또는 반환 유형으로 사용하는 경우 이러한 클래스의 이름이 바뀌지 않았는지도 확인해야 합니다.

앱에 다음 keep 규칙을 추가합니다.

-keepclassmembers,includedescriptorclasses class com.example.JniBridge {
    public void onNativeEvent(com.example.model.NativeData);
}

-keep class NativeData{
        <init>(java.lang.Integer, java.lang.String);
}

이러한 keep 규칙은 R8이 onNativeEvent 메서드와 중요한 매개변수 유형을 삭제하거나 이름을 바꾸지 못하도록 합니다.

  • -keepclassmembers,includedescriptorclasses class com.example.JniBridge{ public void onNativeEvent(com.example.model.NativeData);}: 이 규칙은 클래스가 Kotlin 또는 Java 코드에서 먼저 인스턴스화되는 경우에만 클래스의 특정 멤버를 보존합니다. 앱이 클래스를 사용하고 클래스의 특정 멤버를 보존해야 한다고 R8에 알립니다.
    • -keepclassmembers: 이 규칙은 클래스가 Kotlin 또는 Java 코드에서 먼저 인스턴스화되는 경우에만 클래스의 특정 멤버를 보존합니다. 앱이 클래스를 사용하고 클래스의 특정 멤버를 보존해야 한다고 R8에 알립니다.
    • class com.example.JniBridge: 필드가 포함된 정확한 클래스를 타겟팅합니다.
    • includedescriptorclasses: 이 수정자는 메서드의 서명 또는 설명자에 있는 클래스도 보존합니다. 이 경우 R8이 매개변수로 사용되는 com.example.models.NativeData 클래스의 이름을 바꾸거나 삭제하지 못하도록 합니다. NativeData의 이름이 바뀌면 (예: a.a) 메서드 서명이 더 이상 네이티브 코드에서 예상하는 것과 일치하지 않아 비정상 종료가 발생합니다.
    • public void onNativeEvent(com.example.models.NativeData);: 보존할 메서드의 정확한 Java 서명을 지정합니다.
  • -keep class NativeData{<init>(java.lang.Integer, java.lang.String);}: includedescriptorclassesNativeData 클래스 자체가 보존되도록 하지만 네이티브 JNI 코드에서 직접 액세스되는 NativeData 내의 멤버 (필드 또는 메서드)에는 자체 keep 규칙이 필요합니다.
    • -keep class NativeData: NativeData라는 클래스를 타겟팅하고 블록은 NativeData 클래스 내에서 유지할 멤버를 지정합니다.
    • <init>(java.lang.Integer, java.lang.String): 생성자의 서명입니다. 두 개의 매개변수를 사용하는 생성자를 고유하게 식별합니다. 첫 번째는 Integer이고 두 번째는 String입니다.

간접 플랫폼 호출

Parcelable 구현으로 데이터 전송

Android 프레임워크는 리플렉션을 사용하여 Parcelable 객체의 인스턴스를 만듭니다. 최신 Kotlin 개발에서는 프레임워크에 필요한 CREATOR 필드 및 메서드를 포함하여 필요한 Parcelable 구현을 자동으로 생성하는 kotlin-parcelize 플러그인을 사용해야 합니다.

예를 들어 kotlin-parcelize 플러그인을 사용하여 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

이 시나리오에서는 권장되는 keep 규칙이 없습니다. kotlin-parcelize Gradle 플러그인은 @Parcelize 주석이 추가된 클래스에 필요한 keep 규칙을 자동으로 생성합니다. 생성된 CREATOR 및 생성자가 Android 프레임워크의 리플렉션 호출을 위해 보존되도록 복잡성을 처리합니다.

Parcelable 클래스를 @Parcelize를 사용하지 않고 Kotlin에서 수동으로 작성하는 경우 CREATOR 필드와 Parcel을 허용하는 생성자를 유지해야 합니다. 이렇게 하지 않으면 시스템이 객체를 역직렬화하려고 할 때 앱이 비정상 종료됩니다. @Parcelize를 사용하는 것이 표준적이고 더 안전한 방법입니다.

kotlin-parcelize 플러그인을 사용할 때는 다음 사항에 유의하세요.

  • 플러그인은 컴파일 중에 CREATOR 필드를 자동으로 만듭니다.
  • proguard-android-optimize.txt 파일에는 적절한 기능을 위해 이러한 필드를 유지하는 데 필요한 keep 규칙이 포함되어 있습니다.
  • 앱 개발자는 특히 모든 맞춤 구현 또는 서드 파티 종속 항목에 필요한 모든 keep 규칙이 있는지 확인해야 합니다.

리플렉션 또는 바이트 코드 변환을 사용하는 라이브러리는 런타임에 코드를 동적으로 액세스합니다. R8이 이러한 방식으로 액세스되는 클래스, 필드 또는 메서드를 삭제하거나 이름을 바꾸면 앱이 비정상 종료될 수 있습니다.

그러나 Gson, Retrofit, Kotlinx 직렬화와 같은 인기 있는 서드 파티 라이브러리는 자체 R8 소비자 keep 규칙을 자동으로 번들로 제공합니다. 이러한 라이브러리의 최신 버전을 사용하는 경우 프로젝트에 수동 keep 규칙을 추가할 필요가 없습니다.

Gson

Gson은 리플렉션에 크게 의존하는 JSON 직렬화 및 역직렬화 라이브러리입니다. 전체 모드를 사용하여 앱을 최적화하면 명시적으로 지시하지 않는 한 일반 유형 서명, 기본 생성자, 주석이 추가되지 않은 필드가 삭제됩니다.

Gson이 올바르게 작동하도록 하려면 데이터 모델 클래스에서 비일시적 필드를 유지하고 TypeToken 계층 구조를 보존하는 특정 규칙을 추가하세요.

# Preserve generic type information required for deserialization
-keepattributes Signature

# Keep all non-transient fields in your data model classes for reflection
-keepclassmembers class com.example.models.** {
    !transient <fields>;
}

# Keep TypeToken itself and any anonymous classes extending it
-keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken { *; }
-keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken

transient 수정자가 표시된 필드는 직렬화 및 역직렬화 중에 Gson에서 무시되므로 keep 규칙은 특히 비일시적 필드 (!transient)를 타겟팅합니다.

Retrofit

Retrofit은 리플렉션을 사용하여 네트워크 요청을 구성하고 응답을 변환하는 HTTP 주석 (예: @GET 또는 @POST)이 추가된 서비스 인터페이스 메서드를 검사하는 네트워킹 라이브러리입니다.

Retrofit은 런타임에 Proxy.newProxyInstance()를 사용하여 API 인터페이스의 구현을 동적으로 생성합니다. R8은 이러한 인터페이스를 정적으로 구현하는 클래스를 볼 수 없으므로 메서드 또는 일반 반환 유형을 삭제할 수 있습니다.

번들 keep 규칙

Retrofit은 런타임 리플렉션을 사용하여 일반 매개변수, 메서드 주석, 매개변수 주석을 검사합니다. 적절한 구성이 없으면 R8 전체 모드는 반환 유형, Kotlin 연속, 응답 클래스에서 일반 서명을 완전히 삭제하거나 Retrofit 인터페이스가 프록시로 동적으로 인스턴스화되므로 인터페이스 값을 null로 바꿀 수도 있습니다.

Retrofit 2.10.0부터 라이브러리는 주석 기본값, 서비스 메서드 매개변수, 필요한 클래스 메타데이터를 보존하는 데 필요한 공식 keep 규칙을 자동으로 번들로 제공합니다. 자세한 내용은 Retrofit에서 사용되는 규칙을 참고하세요.

일반 반환 유형 보존

Retrofit은 반환 유형의 일반 서명 (예: Observable<Data>)을 검사하여 네트워크 응답을 올바르게 역직렬화합니다. R8이 일반 서명을 삭제하면 Retrofit은 인스턴스화된 객체를 null로 바꿉니다.

R8 전체 모드가 반환 유형의 일반 서명을 삭제하지 못하도록 하려면 다음 조건부 규칙을 사용하세요.

# Preserve generic type information for Call/Observable return types
-keepattributes Signature

# If an interface has a Retrofit HTTP annotation, keep its return type (class <3>)
-if interface * {
    @retrofit2.http.* public *** *(...);
}
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>

반환되는 실제 데이터 모델 클래스 (예: DataObservable<Data>)도 변환기 (예: Gson)에 의해 리플렉션 방식으로 구성되므로 유지해야 합니다.

코루틴

Kotlin 코루틴을 사용하면 Kotlin 컴파일러는 컴파일된 메서드 서명에 Continuation 매개변수를 추가하여 suspend 함수를 변환합니다.

Retrofit과 같은 라이브러리가 suspend 함수의 일반 서명을 리플렉션 방식으로 읽을 때 해당 Continuation 매개변수에 의존합니다. 전체 모드를 사용하는 경우 Signature 속성은 명시적으로 유지되는 클래스에만 유지됩니다. Continuation은 합성 매개변수이므로 R8은 기본적으로 서명을 삭제하여 리플렉션을 중단합니다.

서명 삭제를 방지하고 전체 모드에서 런타임 호환성을 보장하려면 다음 규칙을 포함하세요.

# Keep the signature attribute globally
-keepattributes Signature

# Explicitly keep the Continuation class so its signature is not stripped
-keep class kotlin.coroutines.Continuation