앱 축소, 난독화 및 최적화

앱을 최대한 작게 만들려면 출시 빌드에 축소를 사용 설정하여 미사용 코드와 리소스를 삭제해야 합니다. 축소를 사용하면 앱 클래스와 멤버의 이름을 줄이는 난독화 및 앱 크기를 추가로 줄이는 더 공격적인 전략을 적용하는 최적화 기능을 활용할 수도 있습니다. 이 페이지에서는 R8이 프로젝트에서 컴파일 시간 작업을 하는 방법과 작업을 맞춤설정하는 방법을 설명합니다.

Android Gradle 플러그인 3.4.0 이상을 사용하여 프로젝트를 빌드하는 경우 플러그인은 더 이상 ProGuard를 사용하여 컴파일 시간 코드 최적화 작업을 하지 않습니다. 대신 플러그인은 R8 컴파일러를 이용하여 다음의 컴파일 시간 작업을 처리합니다.

  • 코드 축소(또는 Tree Shaking): 앱 및 라이브러리 종속 항목에서 미사용 클래스, 필드, 메서드, 속성을 감지하여 안전하게 삭제합니다(64k 참조 제한을 해결하기 위한 유용한 도구). 예를 들어 라이브러리 종속 항목에서 몇 개의 API만 사용한다면 축소는 앱이 사용하지 않는 라이브러리 코드를 식별하고 앱에서 그 코드만 삭제할 수 있습니다. 자세히 알아보려면 코드 축소 방법에 관한 섹션을 참조하세요.
  • 리소스 축소: 앱 라이브러리 종속 항목의 미사용 리소스를 포함하여 패키징된 앱에서 사용하지 않는 리소스를 삭제합니다. 리소스 축소는 코드 축소와 함께 사용하여 미사용 코드를 삭제하고 마찬가지로 더 이상 참조되지 않는 리소스도 안전하게 삭제할 수 있습니다. 자세히 알아보려면 리소스 축소 방법에 관한 섹션을 참조하세요.
  • 난독화: 클래스와 멤버 이름을 줄여 DEX 파일 크기를 줄입니다. 자세히 알아보려면 코드 난독화 방법에 관한 섹션을 참조하세요.
  • 최적화: 코드를 검사하고 다시 작성하여 앱 DEX 파일의 크기를 더 줄입니다. 예를 들어 주어진 if/else 구문의 else {} 분기가 전혀 사용되지 않음을 R8에서 감지한 경우 R8이 else {} 분기 코드를 삭제합니다. 자세히 알아보려면 코드 최적화 섹션을 참조하세요.

앱의 출시 버전을 빌드할 때 위에서 설명한 컴파일 시간 작업을 실행하도록 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

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

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 규칙 파일이 포함되어 있지만 proguard-android-optimize.txt를 사용하는 것이 좋습니다.

라이브러리 종속성 AAR 라이브러리: <library-dir>/proguard.txt

JAR 라이브러리: <library-dir>/META-INF/proguard/

자체 ProGuard 규칙 파일과 함께 AAR 라이브러리가 게시되어 있고 AAR을 컴파일 시간 종속 항목으로 포함하는 경우 R8은 프로젝트를 컴파일할 때 자동으로 규칙을 적용합니다.

라이브러리가 올바르게 작동하기 위해 특정 keep 규칙이 필요한 경우, 즉 라이브러리 개발자가 문제 해결 단계를 실행한 경우 AAR 라이브러리와 함께 패키징된 규칙 파일을 사용하는 것이 유용합니다.

그러나 ProGuard 규칙은 추가적이므로 AAR 라이브러리 종속 항목에 포함된 특정 규칙은 삭제할 수 없으며 앱의 다른 부분을 컴파일하는 데 영향을 미칠 수 있습니다. 예를 들어 라이브러리에 코드 최적화를 사용 중지하는 규칙이 포함된 경우 이 규칙은 전체 프로젝트의 최적화를 사용 중지합니다.

Android Asset Package Tool 2(AAPT2) minifyEnabled true로 프로젝트를 빌드한 후: <module-dir>/build/intermediates/proguard-rules/debug/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 스튜디오를 사용하여 새 프로젝트나 모듈을 만들 때 IDE에서는 자체 규칙을 포함할 수 있도록 <module-dir>/proguard-rules.pro 파일을 만듭니다. 모듈의 빌드 스크립트에서 proguardFiles 속성에 다른 파일의 추가 규칙을 추가하여 포함할 수도 있습니다.

예를 들어, 빌드 변형에 상응하는 productFlavor 블록에 다른 proguardFiles 속성을 추가하여 각 빌드 변형의 전용 규칙을 추가할 수 있습니다. 아래 Gradle 파일은 flavor2 제품 버전에 flavor2-rules.pro를 추가합니다. 이제 flavor2는 세 개의 ProGuard 규칙을 모두 사용하며, 이는 release 블록의 규칙도 함께 적용되기 때문입니다.

또한 testProguardFiles 속성을 추가할 수 있습니다. 이 속성은 테스트 APK에만 포함된 ProGuard 파일 목록을 지정합니다.

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은 앱을 축소할 때 이 코드를 삭제합니다.

그림 1. 컴파일 시간에 R8은 프로젝트의 결합된 keep 규칙을 기반으로 그래프를 작성하여 연결할 수 없는 코드를 결정합니다.

R8은 프로젝트의 R8 구성 파일-keep 규칙을 이용하여 진입점을 결정합니다. 즉, keep 규칙은 앱을 축소할 때 R8이 삭제하면 안 되는 클래스를 지정하고 R8은 이 클래스를 앱의 진입점으로 사용할 수 있다고 간주합니다. Android Gradle 플러그인과 AAPT2는 앱의 활동, 보기 및 서비스와 같이 대부분의 앱 프로젝트에서 필요한 keep 규칙을 자동으로 생성합니다. 그러나 추가적인 keep 규칙을 사용하여 이 기본 동작을 맞춤설정해야 하는 경우 유지할 코드를 맞춤설정하는 방법에 관한 섹션을 참조하세요.

대신 앱 리소스의 크기를 줄이는 데만 관심이 있다면 리소스 축소 방법에 관한 섹션을 참조하세요.

유지할 코드 맞춤설정

대부분의 상황에서는 기본 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
  1. 다음 디렉터리의 콘텐츠를 압축합니다.

    cd app/build/intermediates/cmake/universal/release/obj
    zip -r symbols.zip .
    
  2. 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/keep.xml에 저장). 빌드는 앱에 이 파일을 패키징하지 않습니다.

리소스를 삭제할 수 있는데도 불구하고 삭제할 리소스를 지정하는 것이 이상해 보일 수 있지만 빌드 변형을 사용할 때는 이 방법이 유용할 수 있습니다. 예를 들어 어떤 리소스가 코드에서 사용되는 것처럼 보이지만(이에 따라 축소기에서 삭제하지 않음) 실제로는 주어진 빌드 변형에서 사용하지 않는다는 것을 알고 있다면 모든 리소스를 공용 프로젝트 디렉터리에 넣고 각 빌드 변형을 위해 다른 keep.xml 파일을 생성할 수 있습니다. 또한 컴파일러가 리소스 ID를 인라인으로 추가한 후 리소스 분석기가 실제 참조된 리소스와 같은 값을 가진 코드의 정숫값을 구별하지 못한 경우 빌드 도구가 필요에 따라 리소스를 잘못 식별할 수도 있습니다.

엄격한 참조 확인 사용

일반적으로 리소스 축소기는 리소스의 사용 여부를 정확하게 판별할 수 있습니다. 그러나 코드가 Resources.getIdentifier()를 호출하거나 임의 라이브러리가 호출을 실행하는 경우(예: AppCompat 라이브러리가 호출을 실행), 이 코드는 동적으로 생성된 문자열을 기반으로 리소스 이름을 찾습니다. 이렇게 하면 리소스 축소기는 기본적으로 방어적인 동작을 하며 매칭 이름 형식을 가진 모든 리소스를 잠재적으로 사용 중이며 삭제할 수 없는 리소스로 표시합니다.

예를 들어, 다음 코드는 img_ 접두사가 있는 모든 리소스를 사용되는 리소스로 표시합니다.

Kotlin

val name = String.format("img_%1d", angle + 1)
val res = resources.getIdentifier(name, "drawable", packageName)

Java

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 파일에서 shrinkModestrict로 설정합니다.

<?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에는 일련의 추가 최적화('전체 모드'라고 함)가 포함되어 있어 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: Binary 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] &#64;drawable/add_schedule_fab_icon_anim : reachable=true
16:25:48.009 [QUIET] [system.out]     &#64;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 속성을 사용하여 이 문자열을 제거하라고 빌드 시스템에 알릴 수 있습니다. 자세한 내용은 유지할 리소스를 맞춤설정하는 방법에 관한 섹션을 참조하세요.