앱을 최대한 작고 빠르게 만들려면 isMinifyEnabled = true
를 사용하여 출시 빌드를 최적화하고 축소해야 합니다.
이렇게 하면 사용하지 않는 코드를 삭제하는 축소, 앱 클래스 및 멤버의 이름을 줄이는 난독화, 개선된 코드 최적화 전략을 적용하여 앱 크기를 줄이고 성능을 개선하는 최적화가 사용 설정됩니다. 이 페이지에서는 R8이 프로젝트에 대해 이러한 컴파일 시간 작업을 실행하는 방법과 이를 맞춤설정하는 방법을 설명합니다.
Android Gradle 플러그인 3.4.0 이상을 사용하여 프로젝트를 빌드하는 경우 플러그인은 더 이상 ProGuard를 사용하여 컴파일 시간 코드 최적화 작업을 하지 않습니다. 대신 플러그인은 R8 컴파일러를 이용하여 다음의 컴파일 시간 작업을 처리합니다.
- 코드 축소(또는 Tree Shaking): 앱 및 라이브러리 종속 항목에서 미사용 클래스, 필드, 메서드, 속성을 감지하여 안전하게 삭제합니다(64k 참조 제한을 해결하기 위한 유용한 도구). 예를 들어 라이브러리 종속 항목에서 몇 개의 API만 사용한다면 축소는 앱이 사용하지 않는 라이브러리 코드를 식별하고 앱에서 그 코드만 삭제할 수 있습니다. 자세히 알아보려면 코드 축소 방법에 관한 섹션을 참조하세요.
- 리소스 축소: 앱 라이브러리 종속 항목의 미사용 리소스를 포함하여 패키징된 앱에서 사용하지 않는 리소스를 삭제합니다. 리소스 축소는 코드 축소와 함께 사용하여 미사용 코드를 삭제하고 마찬가지로 더 이상 참조되지 않는 리소스도 안전하게 삭제할 수 있습니다. 자세히 알아보려면 리소스 축소 방법에 관한 섹션을 참조하세요.
- 최적화: 코드를 검사하고 다시 작성하여 런타임 성능을 개선하고 앱 DEX 파일의 크기를 더 줄입니다. 이렇게 하면 코드의 런타임 성능이 최대 30% 향상되어 시작 시간과 프레임 타이밍이 크게 개선됩니다. 예를 들어 주어진 if/else 구문의
else {}
분기가 전혀 사용되지 않음을 R8에서 감지한 경우 R8이else {}
분기 코드를 삭제합니다. 자세히 알아보려면 코드 최적화 섹션을 참조하세요. - 난독화 (또는 식별자 축소): 클래스와 멤버 이름을 줄여 DEX 파일 크기를 줄입니다. 자세한 내용은 코드 난독화 방법에 관한 섹션을 참고하세요.
앱의 출시 버전을 빌드할 때 R8은 위에서 설명한 컴파일 시간 작업을 자동으로 실행하도록 구성할 수 있습니다. ProGuard 규칙 파일을 통해 특정 작업을 중지하거나 R8의 동작을 맞춤설정할 수도 있습니다. 실제로 R8은 기존의 모든 ProGuard 규칙 파일과 호환되므로 R8을 사용하도록 Android Gradle 플러그인을 업데이트하면 기존 규칙을 변경할 필요가 없습니다.
축소, 난독화 및 최적화 사용
Android 스튜디오 3.4 또는 Android Gradle 플러그인 3.4.0 이상을 사용하는 경우 R8은 프로젝트의 자바 바이트 코드를 Android 플랫폼에서 실행되는 DEX 형식으로 변환하는 기본 컴파일러입니다. 그러나 Android 스튜디오를 이용하여 새 프로젝트를 만들 때 축소, 난독화 및 코드 최적화 기능이 기본으로 사용 설정되는 것은 아닙니다. 이러한 컴파일 시간 최적화로 인해 프로젝트의 빌드 시간이 늘어나고 개발자가 유지할 코드를 적절하게 맞춤설정하지 않았을 경우 버그가 발생할 수 있기 때문입니다.
따라서 게시 전에 테스트하는 앱의 최종 버전을 빌드할 때 이러한 컴파일 시간 작업을 사용 설정하는 것이 가장 좋습니다. 축소, 난독화, 최적화를 사용 설정하려면 프로젝트 수준의 빌드 스크립트에 다음을 포함합니다.
Kotlin
android { buildTypes { getByName("release") { // Enables code shrinking, obfuscation, and optimization for only // your project's release build type. Make sure to use a build // variant with `isDebuggable=false`. isMinifyEnabled = true // Enables resource shrinking, which is performed by the // Android Gradle plugin. isShrinkResources = true proguardFiles( // Includes the default ProGuard rules files that are packaged with // the Android Gradle plugin. To learn more, go to the section about // R8 configuration files. getDefaultProguardFile("proguard-android-optimize.txt"), // Includes a local, custom Proguard rules file "proguard-rules.pro" ) } } ... }
Groovy
android { buildTypes { release { // Enables code shrinking, obfuscation, and optimization for only // your project's release build type. Make sure to use a build // variant with `debuggable false`. minifyEnabled true // Enables resource shrinking, which is performed by the // Android Gradle plugin. shrinkResources true // Includes the default ProGuard rules files that are packaged with // the Android Gradle plugin. To learn more, go to the section about // R8 configuration files. proguardFiles getDefaultProguardFile( 'proguard-android-optimize.txt'), 'proguard-rules.pro' } } ... }
R8 구성 파일
R8은 ProGuard 규칙 파일을 사용하여 기본 동작을 수정하고 앱 코드의 진입점 역할을 하는 클래스와 같은 앱 구조를 더 잘 이해합니다. 규칙 파일의 일부를 수정할 수 있지만 일부 규칙은 AAPT2와 같은 컴파일 시간 도구에서 자동으로 생성되거나 앱 라이브러리 종속 항목에서 상속될 수 있습니다. 아래 표는 R8이 사용하는 ProGuard 규칙 파일의 소스를 설명합니다.
소스 | 위치 | 설명 |
Android 스튜디오 | <module-dir>/proguard-rules.pro
|
Android 스튜디오에서 새 모듈을 만들 때 IDE는 새 모듈의 루트 디렉터리에 proguard-rules.pro 파일을 만듭니다.기본적으로 이 파일은 규칙을 적용하지 않습니다. 따라서 맞춤설정 keep 규칙과 같이 직접 만든 ProGuard 규칙을 여기에 포함하세요. |
Android Gradle 플러그인 | 컴파일 시간에 Android Gradle 플러그인에서 생성됩니다. | Android Gradle 플러그인은 proguard-android-optimize.txt 를 생성하는데, 이 파일은 대부분의 Android 프로젝트에 유용한 규칙을 포함하고 @Keep* 주석을 사용합니다.기본적으로 Android 스튜디오를 사용하여 새 모듈을 만들 때 모듈 수준 빌드 스크립트는 출시 빌드에 이 규칙 파일을 포함합니다. 참고: Android Gradle 플러그인에는 추가로 사전에 정의한 ProGuard 규칙 파일이 포함되지만 |
라이브러리 종속 항목 |
AAR 라이브러리:
JAR 라이브러리: 이러한 위치 외에도 Android Gradle 플러그인 3.6 이상에서는 타겟팅된 축소 규칙도 지원합니다. |
자체 규칙 파일과 함께 AAR 또는 JAR 라이브러리를 게시하고 해당 라이브러리를 컴파일 시간 종속 항목으로 포함하는 경우 R8은 프로젝트를 컴파일할 때 자동으로 이러한 규칙을 적용합니다. Android Gradle 플러그인 3.6 이상은 기존 ProGuard 규칙 외에도 타겟팅된 축소 규칙도 지원합니다. 특정 축소기 (R8 또는 ProGuard)와 특정 축소기 버전을 타겟팅하는 규칙입니다. 라이브러리가 올바르게 작동하기 위해 특정 규칙이 필요한 경우, 즉 라이브러리 개발자가 문제 해결 단계를 따른 경우 라이브러리와 함께 패키징된 규칙 파일을 사용하는 것이 유용합니다. 그러나 규칙은 부가적이므로 라이브러리 종속 항목에 포함된 특정 규칙은 삭제할 수 없고 앱의 다른 부분을 컴파일하는 데 영향을 줄 수 있습니다. 예를 들어 라이브러리에 코드 최적화를 사용 중지하는 규칙이 포함된 경우 이 규칙은 전체 프로젝트의 최적화를 사용 중지합니다. |
Android Asset Package Tool 2(AAPT2) | minifyEnabled true :<module-dir>/build/intermediates/aapt_proguard_file/.../aapt_rules.txt 로 프로젝트를 빌드한 후 |
AAPT2는 앱의 매니페스트, 레이아웃 및 다른 앱 리소스의 클래스에 관한 참조를 기반으로 keep 규칙을 생성합니다. 예를 들어 AAPT2는 앱 매니페스트에 진입점으로 등록한 각 활동의 keep 규칙을 포함합니다. |
맞춤 구성 파일 | 기본적으로 Android 스튜디오에서 새 모듈을 만들 때 IDE는 자체 규칙을 추가할 수 있도록 <module-dir>/proguard-rules.pro 를 만듭니다. |
추가 구성을 포함할 수 있으며 R8은 컴파일 시간에 이 구성을 적용합니다. |
minifyEnabled
속성을 true
로 설정하면 R8은 위에 나열된 사용 가능한 모든 소스의 규칙을 결합합니다. 라이브러리 종속 항목을 비롯한 다른 컴파일 시간 종속 항목은 개발자가 모르는 R8 동작 변경을 야기할 수 있으므로 R8을 사용해서 문제를 해결할 때 이 점을 기억해야 합니다.
프로젝트를 빌드할 때 R8이 적용하는 모든 규칙의 전체 보고서를 출력하려면 모듈의 proguard-rules.pro
파일에 다음을 포함하세요.
// You can specify any path and filename.
-printconfiguration ~/tmp/full-r8-config.txt
타겟팅된 축소 규칙
Android Gradle 플러그인 3.6 이상은 특정 축소기 (R8 또는 ProGuard)와 특정 축소기 버전을 타겟팅하는 라이브러리 규칙을 지원합니다. 이를 통해 라이브러리 개발자는 새 압축 도구 버전을 사용하는 프로젝트에서 최적으로 작동하도록 규칙을 조정할 수 있으며, 이전 압축 도구 버전이 있는 프로젝트에서는 기존 규칙을 계속 사용할 수 있습니다.
타겟팅된 축소 규칙을 지정하려면 라이브러리 개발자가 아래에 설명된 대로 AAR 또는 JAR 라이브러리 내 특정 위치에 규칙을 포함해야 합니다.
In an AAR library:
proguard.txt (legacy location)
classes.jar
└── META-INF
└── com.android.tools (targeted shrink rules location)
├── r8-from-<X>-upto-<Y>/<R8-rules-file>
└── proguard-from-<X>-upto-<Y>/<ProGuard-rules-file>
In a JAR library:
META-INF
├── proguard/<ProGuard-rules-file> (legacy location)
└── com.android.tools (targeted shrink rules location)
├── r8-from-<X>-upto-<Y>/<R8-rules-file>
└── proguard-from-<X>-upto-<Y>/<ProGuard-rules-file>
즉, 타겟팅된 축소 규칙은 JAR의 META-INF/com.android.tools
디렉터리 또는 AAR의 classes.jar
내 META-INF/com.android.tools
디렉터리에 저장됩니다.
이 디렉터리 아래에는 r8-from-<X>-upto-<Y>
또는 proguard-from-<X>-upto-<Y>
형식의 이름을 가진 여러 디렉터리가 있을 수 있으며, 이는 디렉터리 내의 규칙이 어떤 버전의 어떤 압축 도구용으로 작성되었는지 나타냅니다.
-from-<X>
및 -upto-<Y>
부분은 선택사항이며, <Y>
버전은 배타적이며, 버전 범위는 연속적이어야 합니다.
예를 들어 r8-upto-8.0.0
, r8-from-8.0.0-upto-8.2.0
, r8-from-8.2.0
는 유효한 타겟팅 축소 규칙 집합을 형성합니다. r8-from-8.0.0-upto-8.2.0
디렉터리의 규칙은 버전 8.0.0부터 버전 8.2.0(제외)까지 R8에서 사용됩니다.
이 정보를 바탕으로 Android Gradle 플러그인 3.6 이상은 일치하는 R8 디렉터리에서 규칙을 선택합니다. 라이브러리가 타겟팅된 축소 규칙을 지정하지 않으면 Android Gradle 플러그인이 기존 위치 (AAR의 경우 proguard.txt
, JAR의 경우 META-INF/proguard/<ProGuard-rules-file>
)에서 규칙을 선택합니다.
라이브러리 개발자는 라이브러리에 타겟팅된 축소 규칙 또는 기존 ProGuard 규칙을 포함하거나, 3.6 이전의 Android Gradle 플러그인 또는 기타 도구와의 호환성을 유지하려는 경우 두 유형을 모두 포함할 수 있습니다.
추가 구성 포함
Android 스튜디오를 사용하여 새 프로젝트나 모듈을 만들 때 IDE에서는 자체 규칙을 포함할 수 있도록 <module-dir>/proguard-rules.pro
파일을 만듭니다. 또한 모듈의 빌드 스크립트에서 proguardFiles
속성에 추가하여 다른 파일의 추가 규칙을 포함할 수도 있습니다.
예를 들어, 빌드 변형에 상응하는 productFlavor
블록에 다른 proguardFiles
속성을 추가하여 각 빌드 변형의 전용 규칙을 추가할 수 있습니다. 아래 Gradle 파일은 flavor2
제품 버전에 flavor2-rules.pro
를 추가합니다.
이제 flavor2
는 세 개의 ProGuard 규칙을 모두 사용하며, 이는 release
블록의 규칙도 함께 적용되기 때문입니다.
또한 테스트 APK에만 포함된 ProGuard 파일 목록을 지정하는 testProguardFiles
속성을 추가할 수 있습니다.
Kotlin
android { ... buildTypes { getByName("release") { isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), // List additional ProGuard rules for the given build type here. By default, // Android Studio creates and includes an empty rules file for you (located // at the root directory of each module). "proguard-rules.pro" ) testProguardFiles( // The proguard files listed here are included in the // test APK only. "test-proguard-rules.pro" ) } } flavorDimensions.add("version") productFlavors { create("flavor1") { ... } create("flavor2") { proguardFile("flavor2-rules.pro") } } }
Groovy
android { ... buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), // List additional ProGuard rules for the given build type here. By default, // Android Studio creates and includes an empty rules file for you (located // at the root directory of each module). 'proguard-rules.pro' testProguardFiles // The proguard files listed here are included in the // test APK only. 'test-proguard-rules.pro' } } flavorDimensions "version" productFlavors { flavor1 { ... } flavor2 { proguardFile 'flavor2-rules.pro' } } }
코드 축소
minifyEnabled
속성을 true
로 설정하면 기본적으로 R8에서 코드 축소가 사용 설정됩니다.
코드 축소(Tree Shaking이라고도 함)는 런타임에 필요하지 않다고 R8이 판단한 코드를 삭제하는 프로세스입니다. 예를 들어 앱이 많은 라이브러리 종속 항목을 포함하지만 기능의 일부만 사용하는 경우 이 프로세스를 통해 앱 크기를 크게 줄일 수 있습니다.
앱 코드를 축소하기 위해 R8은 먼저 결합된 구성 파일 조합을 기반으로 앱 코드의 모든 진입점을 결정합니다. 이 진입점에는 Android 플랫폼이 앱의 활동 또는 서비스를 여는 데 사용할 수 있는 모든 클래스가 포함됩니다. R8은 각 진입점에서 시작하여 앱의 코드를 검사해 앱이 런타임에 액세스할 수 있는 모든 메서드, 멤버 변수, 기타 클래스의 그래프를 작성합니다. 그래프에 연결되지 않은 코드는 연결할 수 없는 것으로 간주되며 앱에서 삭제될 수 있습니다.
그림 1은 런타임 라이브러리 종속 항목을 가진 앱을 보여줍니다. 앱 코드를 검사하는 동안 R8은 MainActivity.class
진입점에서 foo()
, faz()
, bar()
메서드에 연결할 수 있다고 판단합니다. 그러나 런타임에 OkayApi.class
클래스 또는 baz()
메서드가 앱에서 전혀 사용되지 않으며 R8은 앱을 축소할 때 이 코드를 삭제합니다.
R8은 프로젝트의 R8 구성 파일의 -keep
규칙을 이용하여 진입점을 결정합니다. 즉, keep 규칙은 앱을 축소할 때 R8이 삭제하면 안 되는 클래스를 지정하고 R8은 이 클래스를 앱의 진입점으로 사용할 수 있다고 간주합니다. Android Gradle 플러그인과 AAPT2는 앱의 활동, 보기 및 서비스와 같이 대부분의 앱 프로젝트에서 필요한 keep 규칙을 자동으로 생성합니다. 그러나 추가적인 keep 규칙을 사용하여 이 기본 동작을 맞춤설정해야 하는 경우 유지할 코드를 맞춤설정하는 방법에 관한 섹션을 참조하세요.
대신 앱 리소스의 크기를 줄이는 데만 관심이 있다면 리소스 축소 방법에 관한 섹션을 참조하세요.
라이브러리 프로젝트가 축소되면 해당 라이브러리에 종속된 앱에 축소된 라이브러리 클래스가 포함됩니다. 라이브러리 APK에 누락된 클래스가 있는 경우 라이브러리 keep 규칙을 조정해야 할 수 있습니다. AAR 형식으로 라이브러리를 빌드하고 게시하는 경우 라이브러리가 종속되는 로컬 JAR 파일은 AAR 파일에서 축소되지 않습니다.
유지할 코드 맞춤설정
대부분의 상황에서는 기본 ProGuard 규칙 파일 (proguard-android-optimize.txt
)만 있으면 R8을 이용하여 미사용 코드를 삭제할 수 있습니다. 그러나 R8에서 정확하게 분석하기 어려운 상황도 있으며 실제로 앱에서 사용하는 코드를 삭제하는 경우도 발생할 수 있습니다. 다음은 코드를 잘못 삭제할 수 있는 몇 가지 예입니다.
- 앱이 자바 네이티브 인터페이스(JNI)에서 메서드를 호출하는 경우
- 앱이 런타임에 리플랙션 등을 사용하여 코드를 찾는 경우
앱을 테스트하면 잘못된 코드 삭제로 인한 오류가 나타나지만 삭제된 코드 보고서를 생성하여 삭제된 코드를 검사할 수도 있습니다.
오류를 수정하고 R8이 특정 코드를 유지하도록 하려면 ProGuard 규칙 파일에 -keep
줄을 추가합니다. 예:
-keep public class MyClass
또는 유지하려는 코드에 @Keep
주석을 추가할 수 있습니다. @Keep
을 클래스에 추가하면 전체 클래스가 그대로 유지됩니다. 이 주석을 메서드나 필드에 추가하면 메서드/필드 및 그 이름뿐만 아니라 클래스 이름도 그대로 유지됩니다. 참고로 이 주석은 축소 사용 방법에 관한 섹션에 설명한 대로 AndroidX 주석 라이브러리를 사용하고 Android Gradle 플러그인과 함께 패키징된 ProGuard 규칙 파일을 포함할 때만 사용할 수 있습니다.
-keep
옵션을 사용하려면 고려해야 하는 사항이 많습니다. 규칙 파일을 맞춤설정하는 방법에 관한 자세한 정보는 ProGuard 설명서를 참조하세요.
문제 해결 섹션에서는 코드를 제거할 때 발생할 수 있는 다른 일반적인 문제를 간략히 설명합니다.
네이티브 라이브러리 제거
기본적으로 네이티브 코드 라이브러리는 앱의 출시 빌드에서 제거됩니다. 제거되는 항목은 앱에서 사용하는 모든 네이티브 라이브러리에 포함된 기호표와 디버깅 정보입니다. 네이티브 코드 라이브러리를 제거하여 크기를 크게 줄일 수 있지만, 누락된 정보(예: 클래스 이름 및 함수 이름)로 인해 Google Play Console에서 비정상 종료를 진단할 수 없습니다.
네이티브 충돌 지원
Google Play Console은 Android vitals에서 네이티브 충돌을 보고합니다. 몇 단계만 거치면 앱의 네이티브 디버그 기호 파일을 생성하고 업로드할 수 있습니다. 이 파일로 Android vitals에서 기호화된 네이티브 비정상 종료 스택 트레이스(클래스 및 함수 이름 포함)를 사용 설정하여 프로덕션에서 앱을 디버그할 수 있습니다. 이러한 단계는 프로젝트에서 사용하는 Android Gradle 플러그인의 버전과 프로젝트의 빌드 출력에 따라 다릅니다.
Android Gradle 플러그인 버전 4.1 이상
프로젝트가 Android App Bundle을 빌드하는 경우 네이티브 디버그 기호 파일을 자동으로 포함할 수 있습니다. 이 파일을 출시 빌드에 포함하려면 앱의 build.gradle.kts
파일에 다음을 추가합니다.
android.buildTypes.release.ndk.debugSymbolLevel = { SYMBOL_TABLE | FULL }
다음에서 디버그 기호 수준을 선택합니다.
SYMBOL_TABLE
을 사용하여 Play Console의 기호화된 스택 트레이스에서 함수 이름을 가져옵니다. 이 수준은 Tombstone을 지원합니다.FULL
을 사용하여 Play Console의 기호화된 스택 트레이스에서 함수 이름, 파일, 행 번호를 가져옵니다.
프로젝트가 APK를 빌드하는 경우 이전에 보여준 build.gradle.kts
빌드 설정을 사용하여 네이티브 디버그 기호 파일을 별도로 생성합니다. Google Play Console에 수동으로 네이티브 디버그 기호 파일을 업로드합니다. 빌드 프로세스의 일부로 Android Gradle 플러그인은 다음 프로젝트 위치에 이 파일을 출력합니다.
app/build/outputs/native-debug-symbols/variant-name/native-debug-symbols.zip
Android Gradle 플러그인 버전 4.0 이하(및 기타 빌드 시스템)
빌드 프로세스의 일부로 Android Gradle 플러그인은 제거되지 않은 라이브러리의 사본을 프로젝트 디렉터리에 유지합니다. 이 디렉터리 구조는 다음과 유사합니다.
app/build/intermediates/cmake/universal/release/obj/
├── armeabi-v7a/
│ ├── libgameengine.so
│ ├── libothercode.so
│ └── libvideocodec.so
├── arm64-v8a/
│ ├── libgameengine.so
│ ├── libothercode.so
│ └── libvideocodec.so
├── x86/
│ ├── libgameengine.so
│ ├── libothercode.so
│ └── libvideocodec.so
└── x86_64/
├── libgameengine.so
├── libothercode.so
└── libvideocodec.so
다음 디렉터리의 콘텐츠를 압축합니다.
cd app/build/intermediates/cmake/universal/release/obj
zip -r symbols.zip .
Google Play Console에 수동으로
symbols.zip
파일을 업로드합니다.
리소스 축소
리소스 축소는 코드 축소와 함께 사용할 때만 작동합니다. 코드 축소기가 사용하지 않는 코드를 모두 삭제하면 리소스 축소기에서 아직 앱에 사용되는 리소스를 식별할 수 있습니다. 이는 리소스를 포함하는 코드 라이브러리를 추가하는 경우에 특히 그렇습니다. 사용하지 않는 라이브러리 코드를 삭제해야 라이브러리 리소스가 참조되지 않으며 리소스 축소기가 삭제할 수 있습니다.
리소스 축소를 사용하려면 빌드 스크립트에서 shrinkResources
속성을 true
로 설정합니다 (코드 축소의 경우 minifyEnabled
도 설정). 예를 들면 다음과 같습니다.
Kotlin
android { ... buildTypes { getByName("release") { isShrinkResources = true isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" ) } } }
Groovy
android { ... buildTypes { release { shrinkResources true minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } }
코드 축소의 경우 minifyEnabled
를 사용하여 앱을 아직 빌드하지 않았다면 shrinkResources
를 사용 설정하기 전에 먼저 빌드합니다. 리소스 제거를 시작하기 전에 동적으로 생성되거나 호출되는 클래스나 메서드를 유지하려면 proguard-rules.pro
파일을 수정할 필요가 있습니다.
유지할 리소스 맞춤설정
특정 리소스를 유지하거나 삭제하려는 경우, <resources>
태그로 프로젝트에서 XML 파일을 생성하고 tools:keep
속성에서 유지할 각 리소스를 지정하고 tools:discard
속성에서 삭제할 각 리소스를 지정합니다. 두 속성은 모두 쉼표로 구분된 리소스 이름 목록을 허용합니다. 별표 문자를 와일드카드로 사용할 수 있습니다.
예:
<?xml version="1.0" encoding="utf-8"?> <resources xmlns:tools="http://schemas.android.com/tools" tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*" tools:discard="@layout/unused2" />
이 파일을 프로젝트 리소스에 저장합니다(예: res/raw/my.package.keep.xml
에 저장). 빌드는 앱에 이 파일을 패키징하지 않습니다.
참고: keep
파일의 이름은 고유해야 합니다. 서로 다른 라이브러리가 서로 연결되면 keep 규칙이 충돌하여 무시된 규칙이나 불필요한 보관 리소스와 관련된 문제가 발생할 수 있습니다.
리소스를 삭제할 수 있는데도 불구하고 삭제할 리소스를 지정하는 것이 이상해 보일 수 있지만 빌드 변형을 사용할 때는 이 방법이 유용할 수 있습니다. 예를 들어 모든 리소스를 공통 프로젝트 디렉터리에 배치한 다음 특정 리소스가 코드에서 사용되는 것처럼 보이지만(따라서 축소기로 삭제되지 않음) 실제로는 특정 빌드 변형에 사용되지 않는다는 것을 알고 있을 때 빌드 변형마다 다른 my.package.build.variant.keep.xml
파일을 만들 수 있습니다. 빌드 도구가 리소스를 필요에 따라 잘못 식별했을 수도 있습니다. 이는 컴파일러가 리소스 ID를 인라인으로 추가한 후 리소스 분석기에서 실제로 참조된 리소스와 코드에서 동일한 값을 가진 정수 값의 차이를 알지 못할 수 있기 때문입니다.
엄격한 참조 확인 사용
일반적으로 리소스 축소기는 리소스의 사용 여부를 정확하게 판별할 수 있습니다. 그러나 코드가
Resources.getIdentifier()
를 호출하거나 임의 라이브러리가 호출을 실행하는 경우(예: AppCompat 라이브러리가 호출을 실행), 이 코드는 동적으로 생성된 문자열을 기반으로 리소스 이름을 찾습니다. 이렇게 하면 리소스 축소기는 기본적으로 방어적인 동작을 하며 매칭 이름 형식을 가진 모든 리소스를 잠재적으로 사용 중이며 삭제할 수 없는 리소스로 표시합니다.
예를 들어, 다음 코드는 img_
접두사가 있는 모든 리소스를 사용되는 리소스로 표시합니다.
Kotlin
val name = String.format("img_%1d", angle + 1) val res = resources.getIdentifier(name, "drawable", packageName)
자바
String name = String.format("img_%1d", angle + 1); res = getResources().getIdentifier(name, "drawable", getPackageName());
리소스 축소기는 또한 다양한 res/raw/
리소스와 코드에서 모든 문자열 상수를 찾고 file:///android_res/drawable//ic_plus_anim_016.png
와 유사한 형식의 리소스 URL을 찾습니다. 이 리소스와 유사한 문자열을 찾았거나 이와 같은 URL을 구성하는 데 사용될 수 있는 것처럼 보이는 문자열을 찾은 경우, 상응하는 리소스가 삭제되지 않습니다.
다음은 기본적으로 사용되는 안전 축소 모드의 예입니다.
그러나 '나중에 후회하는 것보다는 더 안전한' 처리를 사용 중지하고 리소스 축소기가 확실히 사용되는 리소스만 유지하도록 지정할 수 있습니다. 이를 위해서는 다음과 같이 keep.xml
파일에서 shrinkMode
를 strict
로 설정합니다.
<?xml version="1.0" encoding="utf-8"?> <resources xmlns:tools="http://schemas.android.com/tools" tools:shrinkMode="strict" />
위에서 보는 바와 같이 엄격한 축소 모드를 사용하고 코드가 동적으로 생성된 문자열이 있는 리소스를 참조한다면 tools:keep
속성을 사용하여 이 리소스를 반드시 수동으로 유지해야 합니다.
사용하지 않는 대체 리소스 삭제
Gradle 리소스 축소기는 앱 코드에서 참조하지 않는 리소스만 삭제하며 다른 기기 설정을 위한 대체 리소스는 삭제하지 않습니다. 필요한 경우 Android Gradle 플러그인의 resConfigs
속성을 사용하여 앱에 불필요한 대체 리소스 파일을 제거할 수 있습니다.
예를 들어 언어 리소스가 포함된 라이브러리(예: AppCompat 또는 Google Play 서비스)를 사용 중인 경우, 앱은 앱의 나머지 부분이 동일 언어로 번역되었는지와 상관없이 라이브러리의 메시지를 위해 번역된 모든 언어 문자열을 포함합니다. 앱에서 공식적으로 지원하는 언어만 유지하려면 resConfig
속성을 사용하여 언어를 지정할 수 있습니다. 지정되지 않은 언어의 리소스는 모두 삭제됩니다.
다음 스니펫은 언어 리소스를 영어와 프랑스어로 제한하는 방법을 보여줍니다.
Kotlin
android { defaultConfig { ... resourceConfigurations.addAll(listOf("en", "fr")) } }
Groovy
android { defaultConfig { ... resConfigs "en", "fr" } }
Android App Bundle 형식을 사용하여 앱을 출시하는 경우 기본적으로 앱을 설치할 때 사용자 기기에서 구성된 언어만 다운로드됩니다. 마찬가지로 기기의 화면 밀도와 일치하는 리소스 및 기기의 ABI와 일치하는 네이티브 라이브러리만 다운로드에 포함됩니다. 자세한 내용은 Android App Bundle 구성을 참고하세요.
APK로 출시되는 기존 앱(2021년 8월 이전에 생성됨)의 경우 각각 다른 기기 설정을 타겟팅하는 여러 APK를 빌드하여 APK에 포함할 화면 밀도 또는 ABI 리소스를 맞춤설정할 수 있습니다.
중복 리소스 병합
기본적으로 Gradle은 이름이 동일한 리소스를 병합합니다(예: 동일한 이름의 드로어블이 다른 리소스 폴더에 있는 경우). 코드가 찾고 있는 이름이 여러 리소스와 일치할 때 발생하는 오류를 피해야 하므로 이 동작은 shrinkResources
속성에 의해 제어되지 않으며 사용 중지할 수 없습니다.
리소스 병합은 둘 이상의 파일이 동일한 리소스 이름, 유형 및 한정자를 공유하는 경우에만 발생합니다. Gradle은 중복된 파일 중에서 가장 적합하다고 여겨지는 파일을 아래 설명한 우선순위에 따라 선택한 후 최종 아티팩트에 배포하기 위해 하나의 리소스만 AAPT에 전달합니다.
Gradle은 다음 위치에서 중복 리소스를 찾습니다.
- 기본 소스 세트와 연관된 기본 리소스. 일반적으로
src/main/res/
에 있습니다. - 빌드 유형 및 빌드 버전에서 파생한 변형 오버레이
- 라이브러리 프로젝트 종속 항목
Gradle은 다음과 같은 우선순위 단계에 따라 중복 리소스를 병합합니다.
종속 항목 → 기본 → 빌드 버전 → 빌드 유형
예를 들어, 중복 리소스가 기본 리소스와 빌드 버전 양쪽에 나타나는 경우 Gradle은 빌드 버전에 있는 리소스를 선택합니다.
동일한 소스 세트 안에 동일한 리소스가 있으면 Gradle이 리소스를 병합할 수 없으며 리소스 병합 오류가 발생합니다. 이러한 오류는 build.gradle.kts
파일의 sourceSet
속성에 여러 개의 소스 세트가 정의되는 경우에 발생할 수 있습니다(예: src/main/res/
및 src/main/res2/
에 동일한 리소스가 포함된 경우).
코드 난독화
난독화의 목적은 앱 클래스, 메서드 및 필드의 이름을 단축하여 앱 크기를 줄이는 것입니다. 다음은 R8을 사용한 난독화의 예입니다.
androidx.appcompat.app.ActionBarDrawerToggle$DelegateProvider -> a.a.a.b:
androidx.appcompat.app.AlertController -> androidx.appcompat.app.AlertController:
android.content.Context mContext -> a
int mListItemLayout -> O
int mViewSpacingRight -> l
android.widget.Button mButtonNeutral -> w
int mMultiChoiceItemLayout -> M
boolean mShowTitle -> P
int mViewSpacingLeft -> j
int mButtonPanelSideLayout -> K
난독화는 앱에서 코드를 삭제하지는 않지만 다수의 클래스, 메서드 및 필드의 색인을 생성하는 DEX 파일을 사용하는 앱은 크기를 크게 절약할 수 있습니다. 그러나 난독화는 코드의 다른 부분의 이름을 바꾸기 때문에 스택 트레이스와 같은 특정 작업에는 추가적인 도구가 필요합니다. 난독화 후 스택 트레이스를 알아보려면 난독화된 스택 트레이스를 디코딩하는 방법에 관한 섹션을 참고하세요.
또한, 코드가 앱 메서드와 클래스의 예측 가능한 이름 지정을 사용하고 있다면(예: 리플렉션 사용) 유지할 코드를 맞춤설정하는 방법에 관한 섹션에서 설명한 대로 서명을 진입점으로 취급하고 keep 규칙을 지정해야 합니다. 이러한 keep 규칙은 R8이 코드를 앱의 최종 DEX에 유지할 뿐만 아니라 원래 이름 지정을 유지하도록 합니다.
난독화된 스택 트레이스 디코딩
R8이 코드를 난독화한 후에는 클래스와 메서드 이름이 변경되었을 수 있기 때문에 스택 트레이스를 이해하는 것이 불가능하지는 않지만 어렵습니다. 원래 스택 트레이스를 가져오려면 스택 트레이스를 다시 추적해야 합니다.
코드 최적화
앱을 더 많이 최적화하기 위해 R8은 코드를 더 상세히 검사하여 사용하지 않는 코드를 추가로 삭제하거나 가능하다면 코드를 간결하게 다시 작성합니다. 다음은 이러한 최적화의 몇 가지 예입니다.
- 주어진 if/else 구문에서 코드가
else {}
분기를 전혀 사용하지 않는 경우 R8은else {}
분기 코드를 삭제할 수 있습니다. - 코드가 몇 군데에서만 메서드를 호출하는 경우 R8은 이 메서드를 삭제하고 몇 개의 호출 사이트에서 인라인으로 처리할 수 있습니다.
- 클래스에 고유한 서브클래스가 하나만 있고 클래스 자체가 인스턴스화되지 않는다고 판단되면(예: 하나의 구체적인 구현 클래스에서만 사용하는 추상적인 기본 클래스) R8은 두 개의 클래스를 결합하여 앱에서 클래스를 삭제할 수 있습니다.
- 자세히 알아보려면 제이크 와튼이 작성한 R8 최적화 블로그 게시물을 참조하세요.
R8은 임의의 최적화를 사용 중지 또는 사용 설정하거나 최적화 동작을 수정하는 것을 허용하지 않습니다. 사실상 R8은 -optimizations
및 -optimizationpasses
와 같이 기본 최적화를 수정하려고 시도하는 모든 ProGuard 규칙을 무시합니다. R8이 지속적으로 개선됨에 따라 최적화의 표준 동작을 유지하는 것은 Android 스튜디오팀이 사용자가 겪을 수 있는 모든 문제를 쉽게 해결하는 데 도움이 되기 때문에 이 제한은 중요합니다.
최적화를 사용 설정하면 애플리케이션의 스택 트레이스가 변경됩니다. 예를 들어 인라인 처리를 사용하면 스택 프레임이 삭제됩니다. 원래 스택 트레이스를 가져오는 방법은 재추적 섹션을 참고하세요.
런타임 성능에 미치는 영향
축소, 난독화, 최적화를 모두 사용 설정하면 R8은 코드의 런타임 성능 (UI 스레드의 시작 시간 및 프레임 시간 포함)을 최대 30% 향상시킵니다. 이러한 기능을 사용 중지하면 R8에서 사용하는 최적화 세트가 크게 제한됩니다.
R8이 사용 설정된 경우 시작 성능을 더욱 개선하기 위해 시작 프로필을 만들어야 합니다.
향상된 최적화 사용 설정
R8에는 ProGuard와 다르게 작동하는 추가 최적화('전체 모드'라고 함)가 포함되어 있습니다. 이러한 최적화는 Android Gradle 플러그인 버전 8.0.0부터 기본적으로 사용 설정됩니다.
프로젝트의 gradle.properties
파일에 다음 내용을 포함하여 추가적인 최적화를 사용 중지할 수 있습니다.
android.enableR8.fullMode=false
추가적인 최적화를 사용하면 R8이 ProGuard와 다르게 작동하기 때문에 ProGuard용으로 설계된 규칙을 사용하는 경우 런타임 문제를 방지하기 위해 ProGuard 규칙을 추가로 포함해야 할 수도 있습니다. 예를 들어 코드가 Java Reflection API를 통해 클래스를 참조한다고 가정해 보겠습니다. '전체 모드'를 사용하지 않는 경우 R8은 실제로 그렇게 코드를 작성하지 않았더라도 런타임에 클래스의 객체를 검사하고 조작하는 코드가 있다고 가정하고 클래스와 클래스의 정적 초기화 프로그램을 자동으로 유지합니다.
그러나 '전체 모드'를 사용하는 경우 R8은 이 가정을 하지 않으며, 코드가 런타임에 클래스를 사용하지 않는다고 R8이 어설션하면 앱의 최종 DEX에서 클래스가 삭제됩니다. 즉, 클래스와 정적 이니셜라이저를 유지하려면 규칙 파일에 keep 규칙을 포함해야 합니다.
R8의 'full mode'를 사용하는 동안 문제가 발생하면 R8 FAQ 페이지를 참조하여 가능한 해결책을 찾아보세요. 문제를 해결할 수 없다면 버그로 신고하세요.
stacktraces 재추적
R8에서 처리된 코드는 스택 트레이스를 더 이해하기 어렵게 할 수 있는 여러 방식으로 변경됩니다. 스택 트레이스가 소스 코드와 정확히 일치하지 않기 때문입니다. 디버깅 정보가 유지되지 않을 때 줄 번호를 변경하는 경우를 예로 들 수 있습니다. 인라인 처리와 아웃라인 처리와 같은 최적화 때문일 수 있습니다. 가장 큰 원인은 클래스와 메서드도 이름이 변경되는 난독화입니다.
원래 스택 트레이스를 복구하기 위해 R8은 명령줄 도구 패키지와 함께 번들로 제공되는 retrace 명령줄 도구를 제공합니다.
애플리케이션의 스택 트레이스 재추적을 지원하려면 모듈의 proguard-rules.pro
파일에 다음 규칙을 추가하여, 재추적할 충분한 정보를 빌드에 유지해야 합니다.
-keepattributes LineNumberTable,SourceFile
-renamesourcefileattribute SourceFile
LineNumberTable
속성은 메서드에 위치 정보를 유지하여 스택 트레이스에 이러한 위치가 출력되도록 합니다. SourceFile
속성은 모든 잠재적 런타임에 실제로 위치 정보가 출력되도록 합니다. -renamesourcefileattribute
지시어는 스택 트레이스의 소스 파일 이름을 SourceFile
로 설정합니다. 매핑 파일에 원본 소스 파일이 포함되어 있으므로 재추적할 때 실제 원본 소스 파일 이름은 필요하지 않습니다.
R8은 실행될 때마다 mapping.txt
파일을 만듭니다. 이 파일에는 스택 트레이스를 원래 스택 트레이스에 다시 매핑하는 데 필요한 정보가 포함되어 있습니다. Android 스튜디오는 파일을 <module-name>/build/outputs/mapping/<build-type>/
디렉터리에 저장합니다.
앱을 Google Play에 게시하는 경우 각 앱 버전의 mapping.txt
파일을 업로드할 수 있습니다. Android App Bundle을 사용하여 게시할 때 이 파일은 자동으로 App Bundle 콘텐츠의 일부로 포함됩니다. 그러면 Google Play는 사용자가 보고한 문제에서 수신한 스택 트레이스를 재추적하므로 Play Console에서 스택 트레이스를 검토할 수 있습니다. 자세한 내용은 비정상 종료 스택 트레이스를 가독화하는 방법에 관한 고객센터 도움말을 참고하세요.
R8 문제 해결
이 섹션은 R8에서 축소, 난독화 및 최적화를 사용할 때 발생하는 문제를 해결하기 위한 몇 가지 전략을 설명합니다. 아래에서 문제의 해결책을 찾지 못했다면 R8 FAQ 페이지와 ProGuard의 문제 해결 가이드를 참조하세요.
삭제된(혹은 유지된) 코드의 보고서 생성
R8이 앱에서 삭제한 모든 코드의 보고서를 확인하면 특정 R8 문제를 해결하는 데 도움이 될 수 있습니다. 이 보고서를 생성하려는 모듈마다 -printusage <output-dir>/usage.txt
를 맞춤 규칙 파일에 추가하세요. R8을 사용하여 앱을 빌드하면 R8은 지정된 경로 및 파일 이름이 포함된 보고서를 출력합니다. 삭제된 코드의 보고서는 다음과 유사합니다.
androidx.drawerlayout.R$attr
androidx.vectordrawable.R
androidx.appcompat.app.AppCompatDelegateImpl
public void setSupportActionBar(androidx.appcompat.widget.Toolbar)
public boolean hasWindowFeature(int)
public void setHandleNativeActionModesEnabled(boolean)
android.view.ViewGroup getSubDecor()
public void setLocalNightMode(int)
final androidx.appcompat.app.AppCompatDelegateImpl$AutoNightModeManager getAutoNightModeManager()
public final androidx.appcompat.app.ActionBarDrawerToggle$Delegate getDrawerToggleDelegate()
private static final boolean DEBUG
private static final java.lang.String KEY_LOCAL_NIGHT_MODE
static final java.lang.String EXCEPTION_HANDLER_MESSAGE_SUFFIX
...
대신 R8이 프로젝트의 keep 규칙에서 결정한 진입점과 관련된 보고서를 보려면 맞춤 규칙 파일에 -printseeds <output-dir>/seeds.txt
를 포함하세요. R8을 사용하여 앱을 빌드하면 R8은 지정된 경로 및 파일 이름이 포함된 보고서를 출력합니다. 유지된 진입점의 보고서는 다음과 유사합니다.
com.example.myapplication.MainActivity
androidx.appcompat.R$layout: int abc_action_menu_item_layout
androidx.appcompat.R$attr: int activityChooserViewStyle
androidx.appcompat.R$styleable: int MenuItem_android_id
androidx.appcompat.R$styleable: int[] CoordinatorLayout_Layout
androidx.lifecycle.FullLifecycleObserverAdapter
...
리소스 축소 문제 해결
리소스를 축소할 때 Build 창은 앱에서 삭제된 리소스의 요약을 보여줍니다. Gradle의 자세한 텍스트 출력을 표시하려면 먼저 창 왼쪽에서 Toggle view 를 클릭해야 합니다. 예:
:android:shrinkDebugResources
Removed unused resources: Resource data reduced from 2570KB to 1711KB: Removed 33%
:android:validateDebugSigning
Gradle은 또한 이름이 resources.txt
인 진단 파일을 <module-name>/build/outputs/mapping/release/
(ProGuard의 출력 파일과 동일한 폴더)에 생성합니다. 이 파일은 다른 리소스를 참조하는 리소스 및 사용되거나 삭제되는 리소스에 관한 세부정보를 포함합니다.
예를 들어 @drawable/ic_plus_anim_016
이 아직도 앱에 남아있는 이유를 알아보려면 resources.txt
파일을 열고 파일 이름을 검색합니다. 아래와 같이 다른 리소스에서 참조하는 것을 확인할 수 있습니다.
16:25:48.005 [QUIET] [system.out] @drawable/add_schedule_fab_icon_anim : reachable=true
16:25:48.009 [QUIET] [system.out] @drawable/ic_plus_anim_016
이제 @drawable/add_schedule_fab_icon_anim
이 검색되는 이유를 알아야 합니다. 위쪽으로 검색해 보면 '연결 가능한 루트 리소스' 아래에 나열된 리소스를 찾을 수 있습니다. 즉, add_schedule_fab_icon_anim
코드 참조가 있습니다(다시 말하면, 연결 가능 코드에서 R.drawable ID가 발견되었습니다).
엄격한 검사를 사용하지 않는 경우 동적으로 로드된 리소스의 이름을 구성하기 위해 사용될 수 있는 것처럼 보이는 문자열 상수가 있다면 리소스 ID가 연결 가능하다고 표시될 수 있습니다. 이 경우 리소스 이름의 빌드 출력을 검색하면 다음과 같은 메시지가 표시될 수 있습니다.
10:32:50.590 [QUIET] [system.out] Marking drawable:ic_plus_anim_016:2130837506
used because it format-string matches string pool constant ic_plus_anim_%1$d.
이 문자열 중 하나가 나타나는 경우 주어진 리소스를 동적으로 로드하기 위해 이 문자열이 사용되고 있지 않다는 확신이 들면, tools:discard
속성을 사용하여 이 문자열을 제거하라고 빌드 시스템에 알릴 수 있습니다. 자세한 내용은 유지할 리소스를 맞춤설정하는 방법에 관한 섹션을 참조하세요.