R8 FAQ
- R8 은 ProGuard 와 동일한 문법을 사용
- 즉 ProGuard 와 호환 되도록 최대화
- 하지만 다른 부분도 있음. R8 은 최적화 모드가 다름 (2가지 제공)
- Compatibility 모드 와 Full 모드를 제공
- Compatibility 모드가 default
R8 uses the same configuration specification language as ProGuard, and tries to be compatible with ProGuard. However as R8 has different optimizations it can be necessary to change the configuration when switching to R8. R8 provides two modes, R8 compatibility mode and R8 full mode. R8 compatibility mode is default in Android Studio and is meant to make the transition to R8 from ProGuard easier by limiting the optimizations performed by R8.
R8 full mode
- 비-호환 모드 라고 일컬으며, full 모드 라고도 한다
- 좀 더 공격적인 최적화를 진행한다
- 추가적인 ProGuard rules 설정이 필요할 수도 있다는 것. (비호환)
- android.enableR8.fullMode=true 를 gradle.properties 에 추가
주요 차이점:
- 클래스가 유지되더라도 기본 생성자(<init>() ) 는 암묵적으로 유지되지 않습니다.
- ldc , instanceof , checkcast 와 함께 사용 되는 타입에 대해 기본 생성자(<init>()) 는 암묵적으로 유지 되지 않습니다. 1️⃣
- -keepclassmembers 규칙은 특정 클래스의 필드나 메서드를 보호 하기는 하지만, 그 클래스 자체가 인스턴스화 되지는 않을 수 있음. 만약 클래스가 리플랙션(reflection)을 통해서만 인스턴스화된다면, 그 클래스는 -keep 규칙을 사용해 명시적으로 보호해야 함. 그렇지 않으면 R8 이 해당 클래스를 제거할 수 있음 2️⃣
- 인터페이스 에서 기본적으로 구현된 메서드는 Java8 이후로 도입되었으며, 인터페이스를 구현하는 클래스가 이 메서드를 반드시 구현하지 않아도 되도록 합니다. 하지만 R8 은 추상 메서드와 기본메서드를 다르게 취급하여 기본 메서드는 추상 메서드와 동일하게 취급되지 않기 떄문에 명시적으로 보호하지 않으면 제거될 수 있음 (”default method” 가 제거되면 이를 사용하는 클래스가 정상적으로 동작하지 않을 수 있음) 3️⃣
- Signature 같은 속성(attrivute) 이나 Annotation 은 해당 클래스, 메서드, 필드가 -keep 규칙에 의해 보호되고 있을 때만 유지된다. 단순히 -keepattributes 규칙을 사용한다고 해서 모든 속성(attribute) 이나 Annotation 이 자동으로 보호 되지는 않는다. 4️⃣
- attribute 와 annotation 을 보호하기 위한 가장 최소한의 규칙은 -keep[classmembers], allowshrinking, allowoptimization , allowobfuscation, allowaccessmodification class-specification 이며 해당 규칙은 shrinking, optimization, obfuscation, access modification 을 허용합니다.
- full 모드 에서는 클래스 간의 관계를 설명하는 속성 ex) InnerClass 와 EnclosingMethod 와 같은 속성은 양쪽 클래스가 모두 보호되어야 함. 그렇지 않으면 해당 속성들이 제거될 수 있음
- 최적화와 최소화 에서 SourceFile 속성은 항상 파일 이름을 다시 작성 (파일 이름을 모두 SourceFile 로) 합니다. 즉 원본 소스파일 이름을 알 수 없게 처리됩니다. -renamesourcefileattribute 옵션을 사용하면 SourceFile 속성에 사용자 정의 이름을 설정가능하고 이 옵션을 통해 원본 소스 파일 이름을 대신할 값을 지정할 수 있음.
- 이때 항상 mapping 파일이 생성되고 이 파일에는 원본소스 파일 이름과 난독화된 파일 이름 간의 매핑 정보가 포함됩니다. 5️⃣
1️⃣ 타입 검사에 사용되는 클래스의 기본 생성자 보호
아래 예시 코드 에서 Example 클래스는 타입 검사 에만 활용 되므로 기본 생성자를 생성할 필요가 없습니다. 이 경우 R8 공격적 최적화가 진행되어 기본 생성자가 제거 됩니다.
class Example
fun main() {
// 'Example' 클래스는 실제로 인스턴스가 생성되지 않고 타입 체크에만 사용됨
val obj: Any = "Test String"
if (obj is Example) {
println("obj는 Example 타입입니다.")
} else {
println("obj는 Example 타입이 아닙니다.")
}
}
2️⃣ Reflection 으로만 생성되는 클래스 보호
아래 예시 에서 MyClass 는 리플렉션을 통해서만 인스턴스화하고 있음. 일반적으로 코드에서 직접 인스턴스화 하지 않았기 떄문에, R8 은 이 클래스를 불필요하다고 판단하고 제거할 수 있다. 즉 -keep 옵션을 지정해야함.
이 경우 ClassNotFoundException 이 발생 할 수 있다.
class MyClass {
val myField: String = "Hello"
fun myMethod() {
println("This is myMethod.")
}
}
fun main() {
val clazz = Class.forName("MyClass")
val instance = clazz.getDeclaredConstructor().newInstance()
// 리플렉션을 통해 메서드 호출
val method = clazz.getDeclaredMethod("myMethod")
method.invoke(instance)
}
즉 아래 코드 만으로는 클래스가 유지되지 않을 수 있다.
-keepclassmembers class MyClass {
<fields>;
<methods>;
}
아래 추가 규칙을 정의하면 안전할 것이다.
-keep class MyClass
3️⃣ interface 의 default 메서드 보호
아래 코드에서 MyInterface 는 두가지 메서드를 포함하는데, 하나는 추상, 하나는 default 메서드 이다.
interface MyInterface {
fun abstractMethod()
fun defaultMethod() {
println("This is the default method.")
}
}
class MyClass : MyInterface {
override fun abstractMethod() {
println("Implementing the abstract method.")
}
}
fun main() {
val myClass = MyClass()
myClass.abstractMethod() // 이 메서드는 구현되어 있음
myClass.defaultMethod() // 이 메서드는 인터페이스에서 기본적으로 구현됨
}
R8 최적화 단계에서 defaultMethod() 가 불필요 하다고 판단되어 제거될 가능성이 있음. 추상 메서드는 기본적으로 제거 대상이 아니라 안전하지만 기본 메서드는 명시적으로 보호하지 않으면 위험할 수 있다.
-keep interface MyInterface {
void defaultMethod();
}
위 규칙을 추가하여 방어할 수 있다.
4️⃣ Inner 클래스와 Outer 클래스 관계 보호
class OuterClass {
inner class InnerClass {
fun innerMethod() {
println("This is an inner method.")
}
}
fun outerMethod() {
println("This is an outer method.")
}
}
위 코드 에서 InnerClass 는 OuterClass 의 내부 클래스로 해당 정보는 EnclosingMethod 와 InnerClass 속성으로 바이트 코드에 기록된다.
-keepclassmembers class OuterClass$InnerClass {
<init>();
void innerMethod();
}
-keepattributes InnerClasses,EnclosingMethod
위 규칙으로 인해
- keepclassmembers 규칙은 OuterClass$InnerClass 클래스의 생성자와 innerMethod 메서드를 유지합니다.
- keepattributes InnerClasses,EnclosingMethod는 InnerClasses와 EnclosingMethod 속성을 보호하여 InnerClass와 OuterClass 사이의 관계를 유지합니다.
만약 OuterClass가 제거된다면, InnerClass와의 관계를 나타내는 EnclosingMethod 속성도 무의미해지므로 제거될 수 있습니다. 비호환 모드(non-compat mode)에서는 이러한 관계를 유지하기 위해 양쪽 클래스가 모두 보호되어야 한다.
5️⃣ 파일명 변수 사용
// MySource.kt
class MyClass {
fun greet() {
println("Hello, World!")
}
}
fun main() {
val myClass = MyClass()
myClass.greet()
}
위 코드가 컴파일 되면 바이트 코드에는 해당 파일이 MySource.kt 라는 이름에서 유래되었다는 정보가 SourceFile 속성에 포함되게 됩니다.
-renamesourcefileattribute MyCustomSource
위 옵션을 사용하면 SourceFile 명칭을 MyCustomSource 으로 변경할 수 있다. 사용하는 이유는 디버깅 시 일관된 파일명을 가지도록 하기 위함이다. 실제 디버깅 시에는 매핑 파일을 사용 하므로 문제는 없다.
-keepattributes SourceFile, SourceDir
다만 해당 옵션을 사용하려면 SourceFile 이 존재 해야 하므로 위 옵션을 추가하여 함께 보존해야 해당 옵션이 효과를 발휘할 수 있다.
In non-compat mode, also called “full mode”, R8 performs more aggressive optimizations, meaning additional ProGuard configuration rules may be required. Full mode can be enabled by adding android.enableR8.fullMode=true in the gradle.properties file. The main differences compared to R8 compatibility mode are:
- The default constructor (<init>()) is not implicitly kept when a class is kept.
- The default constructor (<init>()) is not implicitly kept for types which are only used with ldc, instanceof or checkcast.
- The enclosing classes of fields or methods that are matched by a keepclassmembers rule are not implicitly considered to be instantiated. Classes that are only instantiated using reflection should be kept explicitly with a keep rule.
- Default methods are not implicitly kept as abstract methods.
- Attributes (such as Signature) and annotations are only kept for classes, methods and fields which are matched by keep rules even when keepattributes is specified. The weakest rule that will keep annotations and attributes is keep[classmembers],allowshrinking,allowoptimization,allowobfuscation,allowaccessmodification class-specification Additionally, for attributes describing a relationship such as InnerClass and EnclosingMethod, non-compat mode requires both endpoints being kept.
- When optimizing or minifying the SourceFile attribute will always be rewritten to SourceFile unless renamesourcefileattribute is used in which case the provided value is used. The original source file name is in the mapping file and when optimizing or minifying a mapping file is always produced.
References
https://r8.googlesource.com/r8/+/refs/heads/master/compatibility-faq.md
'java' 카테고리의 다른 글
자바 1회 완료 (0) | 2018.02.05 |
---|---|
[java] import 키워드 (외부 패키지 클래스 사용하는법) (2) | 2018.01.25 |