본문 바로가기

android

18. Android) R8

이 글은 공식문서와 Chat GPT를 사용한 결과 입니다. 틀린 내용있으면 댓글 달아주세요.

 

이 글은 R8이 프로젝트에서 컴파일 시간 작업을 하는 방법과 작업을 맞춤설정하는 방법을 설명한다.

 

컴파일 시간 작업

  1. 코드 축소 (Tree Shaking)
    1. 앱 및 라이브러리 종속 항목에서 미사용 클래스, 필드, 메서드, 속성을 감지하여 안전하게 삭제한다.
    2. 예를 들어 라이브러리 종속항목에서 몇개의 API만 사용한다면 축소는 앱이 사용하지않는 라이브러리 코드를 식별하고 앱에서 그 코드만 삭제할수있다.
  2. 리소스 축소
    1. 앱 라이브러리 종속 항목의 미사용 리소스를 포함하여 패키징된 앱에서 사용하지 않는 리소스를 삭제한다.
    2. 더 이상 참조되지 않는 리소스도 안전하게 삭제할 수 있다.
  3. 난독화
    1. 클래스와 멤버 이름을 줄여 DEX 파일 크기를 줄인다.
  4. 최적화
    1. 코드를 검사하고 다시 작성하여 앱 DEX 파일의 크기를 더 줄입니다. 
    2. 예를 들어 주어진 if/else 구문의 else{} 분기가 전혀 사용되지 않음을 R8에서 감지하면 R8이 else{} 분기 코드를 삭제한다.
  5. 특징
    1. 앱의 출시버전을 빌드할 때 기본적으로 R8은 위에서 설명한 컴파일 시간 작업을 자동으로 진행한다.
    2. ProGuard 규칙 파일을 사용하여 특정작업을 중지하거나 R8의 맞춤설정할 수 있습니다.
    3. R8은 기존의 모든 ProGuard 규칙 파일과 호환되므로 R8을 사용하도록 Android Gradle 플러그인을 업데이트하면 기존 규칙을 변경할 필요가 없다.

설정

android {
    buildTypes {
        getByName("release") {
           // 프로젝트의 릴리스 빌드 유형에 대해서만 코드 축소, 난독화 및 최적화를 활성화합니다.
            isMinifyEnabled = true

            // Android Gradle 플러그인에서 수행되는 리소스 축소를 활성화합니다.
            isShrinkResources = true

            // Android Gradle 플러그인과 함께 패키지된 기본 ProGuard 규칙 파일을 포함합니다.
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    ...
}
  1. R8 사용하기 위한 설정
    1. R8은 프로젝트의 자바 바이트 코드를 Android 플랫폼에서 실행되는 DEX 형식으로 변환하는 기본 컴파일러이다.
    2. R8은 기본적으로 사용설정되지 않는다.
    3. 개발자가 유지할 코드를 적절하게 맞춤설정하지 않았을 때 버그가 발생할 수 있다.
    4. 따라서 위의 설정을 해주어야한다.

 

R8 구성 파일

  1. R8은 ProGuard 규칙 파일을 사용하여 기본 동작을 수정하고 앱 코드의 진입점역할을 하는 클래스와 앱 구조를 더 잘 이해합니다.
  2. 규칙의 일부를 수정할 수 있지만 일부규칙은 AAPT2와 같은 컴파일 시간 도구에서 자동으로 생성되거나 앱 라이브러리 종속항목에서 상속될 수 있습니다.
  3. 다음 내용은 R8이 사용하는 ProGuard 규칙 파일의 소스를 설명한다.

 

@Keep
public void foo() {
    ...
}
  1. proguard-rules.pro
    1. 소스 : Android Studio
    2. 위치 : <module-dir>/proguard-rules.pro
    3. 설명 
      1. Android Studio에서 새 모듈을 만들때 IDE는 새 모듈의 루트 디렉터리에 proguard-rules.pro 파일을 만든다.
      2. 기본적으로 이 파일은 규칙을 적용하지 않는다.
      3. 이 글의 유지할 코드 맞춤설정과 같은 직접만든 ProGuard 규칙을 여기에 포함하자.
  2. proguard-android-optimize.txt
    1. 소스 : Android Gradle Plugin
    2. 위치 : 컴파일 시간에 Android Gradle Plugin에서 생성된다.
    3. 설명
      1. Android Gradle Plugin은 proguard-android-optimize.txt를 생성하는데 
      2. 이 파일은 대부분의 Android 프로젝트에 유용한 규칙을 포함하고 @keep* 주석을 사용한다.
        1. @keep* 주석
          1. 이 주석은 코드 축소 시 특정요소(메서드나 클래스)를 제거하지 않도록 지시하는 데 사용된다.
          2. 이는 주로 reflection을 통해서만 접근되는 메소드나 클래스에 사용된다.
            1. reflection : 런타임에 객체의 클래스 정보를 조사하고 수정할 수 있는 기능을 말한다.
          3. 컴파일러는 이러한 리플렉션을 통해 접근되는 코드를 사용되지 앟는 코드로 오인할 수 있기 때문에 코드가 축소되는 과정에서 제거하지 않도록 만들어줌
      3. 기본적으로 Android Studio를 사용하여 새 모듈을 만들 때 모듈 수준 build.gradle 파일은 출시 빌드에 이 규칙 파일을 포함한다.
  3. AAR 라이브러리
    1. 소스 : 라이브러리 종속 항목
    2. 위치 
      1. AAR library : <library-dir>/progruard.txt
      2. JAR library : <library-dir>/META-INF/proguard/ 
    3. 설명
      1. AAR library는 'proguard.txt'라는 자체 ProGuard 규칙 파일을 포함할 수 있습니다.
        1. AAR library
          1. 안드로이드 스튜디오에서 사용하는 라이브러리 포맷으로 자바 클래스, 안드로이드 리소스, 매니페스트 파일등을 포함할 수 있다.
          2. 프로젝트에 AAR library를 포함하는 것은 그 라이브러이가 제공하는 코드나 리소스를 사용하기 위해 필요하다.
      2. AAR library를 프로젝트에 컴파일 시간 종속 항목으로 포함할 경우 R8은 라이브러리와 함께 제공된 ProGuard 규칙을 자동으로 적용한다.
        1. 종속 항목 : 프로그램이 올바르게 작동하기 위해 필요한 외부 라이브러리나 모듈
      3. AAR library에 포함된 ProGuard 규칙은 삭제할 수 없으며, 앱의 다른 부분에도 영향을 미칠 수 있다.
      4. 예를 들어 라이브러이에 코드 최적화를 중지하는 규칙이 포함되어있으면 이 규칙은 전체 프로젝트의 코드 최적화에 영향을 줄 수 있다.
  4. AAPT2
    1. 소스 : Android Asset Package Tool 2 (AAPT2)
    2. 위치 : minifyEnabled true: <module-dir>/build/intermediates/proguard-rules/debug/aapt_rules.txt
    3. 설명 
      1. AAPT2
        1. 안드로이드 앱의 리소스(레이아웃 파일, 이미지 등)를 컴파일하고 패키징하는 데 사용됩니다.
        2. 앱의 리소스를 더 효율적으로 처리하여 빌드시간을 단축하고 앱의 성능을 향상시키는 역할을 한다.
      2. keep 규칙
        1. AAPT2는 빌드 과정에서 앱의 매니페스트. 레이아웃 및 기타 리소스에서 참조하는 클래스에 대한 keep 규칙을 자동으로 생성한다.
        2. 예를 들어, 앱 메니페스트 파일에서 선언된 각 Activity에 대해 AAPT2는 해당 클래스를 난독화하는 과정에서 제외하는 keep 규칙을 포함한다. 이는 앱의 정상적인 작동을 보장하기 위해 필요
          1. 안드로이드 시스템이 액티비티 같은 것을 정확하게 식별하고 엑세스할 수 있어야하기 때문이다. 
          2. 난독화 과정에서 클래스 이름이 변경되면 시스템이 해당 액티비티를 정확하게 찾을 수 없다.
          3. 액티비티의 이름은 난독화 되지 않지만 액티비티의 내부 구현은 난독화될 수 있다.
  5. 맞춤구성파일
    1. 소스 : 맞춤구성파일
    2. 위치 : 기본적으로 Android Studio에서 새 모듈을 만들 때 IDE는 자체 규칙을 추가할 수 있도록 <module-dir>/proguard-rules.pro를 만든다.
    3. 설명 
      1. 이 파일은 개발자가 앱에 대한 추가적인 ProGuard 또는 R8 최적화 규칙을 정의할 수 있도록 한다.
      2. 앱의 최종바이너리에 영향을 미친다.
// minify
// You can specify any path and filename.
-printconfiguration ~/tmp/full-r8-config.txt

 

 

추가 구성 포함

  1. 기본 규칙 파일 생성
    1. Android Studio는 새로운 프로젝트나 모듈을 만들 때 기본적으로 
    2. <module-dir>/proguard-rules.pro 파일을 생성한다.
    3. 이 파일은 개발자가 앱에 대한 추가적인 ProGuard/R8 최적화 규칙을 정의할 수 있도록 한다.
  2. build.gradle
    1. build.gradle 파일에서 proguardFiles 속성을 이용하여 추가 ProGuard/R8 규칙 파일을 지정할 수 이싿.
    2. 이를 통해 기본 규칙외에 추가적인 규칙을 적용할 수 있다.
    3. 빌드 변형 및 productFlavor 설정
      1. productFlavor
        1. Android Studio에서는 여러 poductFlavor를 정의하여 다양한 빌드 변형을 관리할 수 있다.
        2. 이를 통해 각각의 빌드 변형에 대해 다른 설정을 적용할 수 있다.
      2. flavor별 proguard 규칙
        1. 예를 들어, 특정 플레이버에(예: flavor2)에 대해 특별한 ProGuard/R8 규칙을 적용하고 싶다면, 해당 플레이버에 proguardFiles 속성에 해당 규칙 파일을 추가할 수 있다.

 

android {
    ...
    // 앱의 빌드 유형을 정의 
    // release 빌드 유형에 대한 설정이 포함됨
    buildTypes {
        getByName("release") {
            isMinifyEnabled = true
            //프로가드에 사용할 규칙 파일을 지정한다.
            proguardFiles(
            //안드로이드 기본 프로가드 설정을 불러온다.
                getDefaultProguardFile("proguard-android-optimize.txt"),
                // 개별 모듈 레벨에서 정의된 추가 프로가드 규칙을 포함한다.
                //안드로이드 스튜디오는 각 모듈의 루트 디렉터리에 기본적으로 빈 규칙파일을 생성한다.
                //여기에 사용자가 정의한 추가 규칙을 추가할 수 있다.
                "proguard-rules.pro"
            )
        }
    }
    //productFlavor의 차원을 정의 
    //version이라는 차원이 추가됨
    flavorDimensions.add("version")
    productFlavors {
    //flavor1, flavor2라는 두가지 플레이버를 정의했다.
        create("flavor1") {
            ...
        }
        create("flavor2") {
        //flavor2-rules.pro에는 flavor2 플레이버에 특화된 규칙이 포함될 수 있다.
            proguardFile("flavor2-rules.pro")
        }
    }
}
  • productFlavor
    • 앱의 다양한 변형을 정의하는 방법
    • 이를 통해 하나의 코드베이스에서 여러 버전의 앱을 생성할 수 있다.
    • 예를 들어 무료버전과 유료버전의 앱, 다양한 시장을 대상으로 하는 앱등을 하나의 프로젝트에서 관리할 수 있다.
    • 각 플레이버는 다음과 같은 측면에서 차이를 가진다.
      • 리소스(이미지, 문자열)
      • 소스 코드(특정기능 활성화/비활성화)
      • 의존성(특정라이브러리만 포함)
      • 앱 ID, 버전 정보등
  • Flavor Dimensions(플레이버 차원)
    • 플레이버 차원은 서로 다른 종류의 플레이버 그룹을 구분하는데 사용된다.
    • 플레이버 차원을 사용하면 각 차원에 속한 플레이버를 결합하여 다양한 빌드 변형을 생성할 수 있다.
    • 예를 들어 하나의 차원이 버전이라면(예: 유료,무료), 다른 차원은 '시장'일수 있습니다.

 

코드 축소

  1. minifyEnabled 속성을 true로 설정하면 기본적으로 R8에서 코드 축소가 사용설정 됩니다.
  2. 코드 축소는 런타임에 필요하지 않다고 R8이 판단한 코드를 삭제하는 프로세스입니다.
  3. 앱 코드를 축소하기 위해 R8은 먼저 결합된 구성 파일 조합(위의 R8 구성 파일 참조)을 기반으로 앱 코드의 모든 진입점을 결정한다.
  4. 이 진입점에는 Android 플랫폼이 앱의 액티비티 또는 서비스를 여는 데 사용할 수 있는 모든 클래스가 포함된다.
  5. R8은 각 진입점에서 시작하여 앱의 코드를 검사해 앱이 런타임에 엑세스할 수 있는 모든 메서드, 멤버 변수, 기타 클래스의 그래프를 작성합니다.
  6. 그래프에 연결되지 않은 코드는 연결할 수 없는 것으로 간주되면 앱에서 삭제된다.
  7. R8 프로젝트의 R8 구성 파일의 -keep 규칙을 이용하여 진입점을 결정한다.
  8. keep 규칙은 앱을 축소할 때 R8이 삭제하면  안되는 클래스를 지정하고 R8은 이 클래스를 앱의 진입점으로 사용할 수 있다고 간주합니다.
  9. Android Gradle plugin과 AAPT2는 앱의 액티비티, 뷰 및 서비스와 같이 대부분의 앱 프로젝트에서 필요한 keep 규칙을 자동으로 생성합니다.
  10. 그러나 추가적인 keep 규칙을 사용하여 이 기본 동작을 맞춤 설정해야하는 경우 유지할 코드를 맞춤설정하는 방법에 관한 섹션을 참조하세요.

 

유지할 코드를 맞춤설정

  1. 대부분의 상황에서는 기본 ProGuard 규칙 파일(proguard-android-optimze.txt)만 있으면 R8을 이용하여 미사용 코드르 삭제할 수 있습니다.
  2. 그러나 R8에서 정확하게 분석하기 어려운 상황도 있으며 실제로 앱에서 사용하는 코드를 삭제하는 경우도 발생할 수 있습니다.
  3. 코드를 잘못 삭제할 수 잇는 몇가지 예이다.
    1. 앱이 자바 네이티브 인터페이스(JNI)에서 메서드를 호출하는 경우
    2. 앱이 런타임에 리플랙션등을 사용하여 코드를 찾는 경우
  4. 앱을 테스트하면 잘못된 코드 삭제로 인한 오류가 나타나지만 삭제된 코드 보고서를 생성하여 삭제된 코드를 검사할수도 있습니다.
  5. 오류를 수정하고 R8이 특정 코드를 유지하도록 하려면 ProGuard 규칙 파일에 -keep 줄을 추가한다. 밑의 코드 참조
  6. 또는 유지하려는 코드에 @Keep 주석을 추가할 수 있다.
  7. @Keep 클래스에 추가하면 전체 클래스가 그대로 유지된다.
  8. 이 주석을 메서드나 필드에 추가하면 메서드/필드 및 그 이름뿐만 아니라 클래스 이름도 그대로 유지됩니다.
  9. 참고로 이 주석은 축소 사용방법에서 설명한 대로 AndroidX 주석 라이브러리를 사용항고 Android Gradle Plugin과 함께 패키징된 ProGuard 규칙파일을 포함할 때만 사용가능하다.
  10. 규칙 파일을 맞춤 설명하는  방법에 대한 정보는 ProGuard 설명서  참고(https://www.guardsquare.com/en/products/proguard/manual/usage)
-keep public class MyClass

 

삭제된 코드 보고서 생성 방법

android {
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'

            // 삭제된 코드 보고서 활성화
            buildConfigField "boolean", "GENERATE_UNUSED_CODE_REPORT", "true"
        }
    }
}
  1. R8이 코드 최적화 및 난독화 과정에서 잘못 삭제한 코드를 추적하려면 삭제된 코드 보고서를 생성할 수 있습니다.
  2. 어떤 코드가 최적화 중에 삭제 되었는지 확인할 수 있습니다.
  3. R8은 빌드시 삭제된 코드에 대한 보고서를 자동으로 생성할 수 있습니다.
  4. 이를 위해 build.gradle 파일에서 minifyEnabled가 true로 설정되어 있어야 하며, 추가적으로 shrinkResource 옵션을 활성화 하고 -printconfiguration 플래그를 사용해야 합니다.
  5. 위의 설정을 통해 삭제된 코드에 대한 보고서를 생성할 수 있습니다.
  6. 그런 다음 R8은 로그에서 -unused 플래그를 사용하여 삭제된 코드를 보고할 수 있습니다.
    1. 플래그
      1. 삭제된 코드를 보고하는 방식과 관련이 있음
      2. -printConfiguration
        1. R8의 작업이 끝난 후, 최적화된 코드와 난독화 정보에 대한 상세 보고서를 생성합니다.
        2. 이 보고서는 최적화 및 난독화 처리 중 어떤 코드가 삭제 되었는지, 사용되지 않은 코드는 무엇인지 등을 보여줍니다.
  7. 빌드를 완료한 후 

 

 

네이티브 라이브러리 제거

  • 개요
    • 네이티브 코드 라이브러리는 앱의 출시 빌드에서 제거됩니다.
      • 네이티브 코드 라이브러리 
        • 그래픽 처리, 오디오 처리, 물리 계산 등 특정한 기능을 구현하는데 사용된다.
    • 제거 되는 항목은 앱에서 사용하는 모든 네이티브 라이브러리에 포함된 기호표와 디버깅 정보이다.
    • 네이티브 코드 라이브러리를 제거하여 크기를 크게 줄일수 있지만 누락된 정보 (예: 클래스 이름 및 함수 이름)로 인해 Google Play Console에서 비정상종료를 진단할 수 없습니다.
  • 네이티브 충돌 지원
    • 개요
      • Google Play Console은 Android vitals에서 네이티브 충돌을 보고한다.
      • 몇 단계만 거치면 앱의 네이티브 디버그 기호 파일을 생성하고 업로드할 수 있다.
      • 이 파일로 Android vitals에서 기호화된 네이티브 비정상 종료 스택 트레이스(클래스 및 함수 이름 포함)를 사용 설정하여 프로덕션에서 앱을 디버그할 수 있습니다.
      • 이러한 단계는 프로젝트에서 사용하는 Android Gradle 플러그인의 버전과 프로젝트의 빌드 출력에 따라 다르다. 
    • Android Gradle Plugin 버전 4.1 이상
      • 프로젝트가 Android App Bundle을 빌드하는 경우 네이티브 디버그 기호 파일을 자동으로 포함할 수 있습니다.
      • 이 파일을 출시 빌드에 포함하려면 앱의 build.gradle 파일에 다음을 추가합니다.
      • android.buildTypes.release.ndk.debugSymbolLevel = {SYMBOL_TABLE | FULL}
      • 다음에서 디버그 기호 수준을 선택합니다.
        • SYMBOL_TABLE을 사용하여 Play Console의 기호화된 스택 트레이스에서 함수 이름을 가져옵니다. 이 수준은 TombStone(https://source.android.com/devices/tech/debug?hl=ko)을 지원합니다.
        • FULL을 사용하여 Play Console의 기호화된 스택 트레이스에서 함수 이름, 파일, 행 번호를 가져옵니다.
      • 프로젝트가 APK를 빌드하는 경우 이저에 보여준 build.gradle 빌드 설정을 사용하여 네이티브 디버그 기호 파일을 별도로 생성한다.
      • Google Play Console에 수동으로 네이티브 디버그 기호 파일을 업로드 한다.(https://support.google.com/googleplay/android-developer/answer/9848633?hl=ko#upload_file)
      • 빌드 프로세스의 일부로 Android Gradle 플러그 인은 다음 프로젝트 위치에 이 파일을 출력한다.
      • app/build/outputs/native-debug-symbols/variant-name/native-debug-symbols.zip
    • Android Gradle Plugin 버전 4.0이하(및 기타 빌드 시스템)
      • 빌드 프로세스의 일부로 Android Gradle 플러그인은 제거되지 않은 라이브러리의 사본을 프로젝트 디렉터리에 유지한다.
      • 아래에 디렉터리 구조를 첨부한다.
      • 방법
      • 주의
        • 디버그 기호 파일은 300MB로 제한합니다.
        • 파일이 너무 큰 경우 .so 파일에 기호 테이블(함수이름) 및 DWARF 디버깅 정보(파일 이름 및 코드 줄)가 포함되어 있기 때문일 수 있다.
        • 이러한 테이블 및 정보는 코드를 기호화하는 데 필요하지 않다.
        • 아래의 명령어를 사용하여 삭제할 수 있다.
//디렉터리 콘텐츠를 압축하기 위한 명령어
cd app/build/intermediates/cmake/universal/release/obj
zip -r symbols.zip .

 

//디렉토리 구조
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

 

 

//DWARF 디버깅 정보 삭제 명령어
$OBJCOPY --strip-debug lib.so lib.so.sym
//여기서 $OBJCOPY는 제거할 ABI의 특정버전을 가리킨다.
//예: nbk-bundle/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-objcopy

 

 

리소스 축소

android {
    ...
    buildTypes {
        getByName("release") {
            isShrinkResources = true
            isMinifyEnabled = true
            proguardFiles(
                getDefaultProguardFile("proguard-android.txt"),
                "proguard-rules.pro"
            )
        }
    }
}
  • 리소스 축소는 코드 축소와 함께 사용할 때만 작동한다.
  • 코드 축소기가 사용하지 않는 코드를 모두 삭제하면 리소스 축소기에서 아직 앱에 사용되는 리소스를 식별할 수 있습니다.
  • 이는 리소스를 포함하는 코드 라이브러리를 추가하는 경우에 특히 그렇습니다.
  • 사용하지 않는 라이브러리 코드는 삭제해야 라이브러리 리소스가 참조되지 않으며 리소스 축소기가 삭제할 수 있다.
  • 리소스 축소를 사용하려면 build.gradle 파일에서 shrinkResource 속성을 true로 설정합니다.
  • 코드 축소의 경우 minifyEnabled도 설정
  • 코드 축소의 경우 minifyEnabled를 사용하여 앱을 아직 빌드하지 않았다면 shrinkResource를 사용하기 전에 먼저 빌드한다.
  • 리소스 제거를 시작하기 전에 동적으로 생성되거나 호출되는 클래스나 메서드를 유지하려면 proguard-rules.pro 파일을 수정할 필요가 있습니다.

유지할 리소스 맞춤 설정

 

<?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" />
  • 특정 리소스를 유지하거나 삭제하려는 경우, <resources> 태그로 프로젝트에서 XML 파일을 생성하고 tools:keep 속성에서 유지할 각 리소스를 지정하고 tools:discard 속성에서 삭제할 각 리소스를 지정합니다.
  • 두 속성은 모두 쉼표로 구분된 리소스 이름 목록을 허용합니다.
  • *를 와일드 카드로 사용할 수 있다.
  • 이 파일을 프로젝트 리소스에 저장한다.
  • 예: res/raw/keep.xml 에 저장
  • 빌드는 앱에 이 파일을 패키징 하지 않는다.
  • 리소스를 삭제할 수 있는데도 삭제할 리소스를 지정하는 것이 이상해 보일 수 있지만 빌드 변형을 사용할 때는 이 방법이 유용할 수 있습니다.
  • 예를 들어 어떤 리소스가 코드에서 사용되는 것 처럼 보이지만(이에 따라 축소기에서 삭제하지 않음) 실제로는 주어진 빌드 변형(예: debug, release)에서 사용하지 않는다는 것을 알고 있다면 모든 리소스를 공용 프로젝트 디렉터리에 넣고 각 빌드 변형을 위해 다른 keep.xml 파일을 생성할 수 있다.
  • 또한 컴파일러가 리소스 ID를 코드에 직접 삽입하는 '인라인'처리를 할 때 리소스 분석기가 이러한 인라인 처리된 정수값을 실제 리소스 참조와 구별하지 못한다. 이로 인해 빌드 도구가 리소스를 잘못 식별하고 필요한 리소스를 삭제할 위험이 있다.
    • 컴파일러가 리소스 ID를 인라인 처리하면, 원래의 R.id 형식의 참조가 코드 내에서 정수 값으로 바뀐다. 
    • 이 때문에 리소스 분석기가 해당 정수 값을 리소스와 관련된 참조로 정확히 인식을 못한다.
    • 리소스 분석기가 이러한 인라인 처리된 정숫값을 정확히 리소스 참조로 인식하지 못하면, 실제로 사용되고 있는 리소스임에도 불구하고 불필요한 리소스로 오인하여 제거할 수 있다.

 

엄격한 참조 확인

  • 일반적으로 리소스 축소기는 리소스의 사용 여부를 정확하게 판별할 수 있다.
  • 그러나 코드가 Resources.getIdentifier()를 호출하거나 임의 라이브러리가 호출을 실행하는 경우에(예: AppCompat 라이브러리가 호출을 실행), 이코드는 동적으로 생성된 문자열을 기반으로 리소스 이름을 찾는다.
  • 이렇게 하면 리소스 축소기는 기본적으로 방어적인 동작을 하며 매칭 이름 형식을 가진 모든 리소스를 잠재적으로 사용중이며 삭제할 수 없는 리소스로 표시한다.
val name = String.format("img_%1d", angle + 1)
val res = resources.getIdentifier(name, "drawable", packageName)
  • 예를 들어, 다음 코드는 img_ 접두사가 있는 모든 리소스를 사용되는 리소스로 표시합니다.
  • 리소스 축소기는 또한 다양한 res/raw/ 리소스와 코드에서 모든 문자열 상수를 찾고 file:///android_res/drawble//ic_plus_anim016.png와 유사한 형식의 리소스 URL을 찾습니다.
  • 이러한 URL이 코드에서 참조되는 경우, 해당 리소스는 삭제되지 않습니다.
  • 앱의 리소스를 참조하는 문자열또는 리소스 URL을 구성하는데 사용될 수 있는 문자열이 발견되면 해당 리소스는 보존된다.
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:shrinkMode="strict" />

엄격한 축소 모드

  • 안전모드는 잠재적으로 사용되는 리소스를 보수적으로 보존한다. 
  • 더 엄격한 리소스 관리를 원하는 경우 엄격한 축소모드(strict mode)를 사용할 수 있습니다.
  • keep.xml 파일에서 shrinkMode를 strict로 설정함으로써, 리소스 축소가 확실하게 사용되는 리소스만을 보존하도록 지정할 수 있다.
<resources xmlns:tools="http://schemas.android.com/tools">
    <tools:keep>
        <!-- 여기에 유지하고자 하는 리소스의 이름을 명시합니다. -->
        @drawable/dynamically_referenced_image
    </tools:keep>
</resources>
  • tools:keep 속성을 keep.xml 파일에 사용하여 동적으로 참조되는 리소스를 축소 과정에서 제외시킬 수 있다.

 

사용하지 않는 대체 리소스 삭제

android {
    defaultConfig {
        ...
        resourceConfigurations.addAll(listOf("en", "fr"))
    }
}

 

  • Gradle 리소스 축소기는 앱 코드에서 참조하지 않는 리소스만 삭제하며 다른 기기 설정을 위한 대체 리소스는 삭제하지 않는다.
    • 대체리소스
      • 다양한 기기 설정과 환경에 맞게 앱이 적절히 반응하도록 설계된 특정 리소스
      • 다양한 화면 크기, 해상도, 언어 설정
  • 필요한 경우 Android Gradle Plugin의 resConfigs 속성을 사용하여 앱에 불필요한 대체 리소스 파일을 제거할 수 있다.
  • 예를 들어 언어 리소스가 포함된 라이브러리 (예: AppCompat 또는 Google Play 서비스)를 사용 중인 경우 앱은 앱의 나머지 부분이 동일 언어로 번역되었는지와 상관 없이 라이브러리의 메시지를 위해 번역된 모든 언어 문자열을 포함합니다. 
  • 앱에서 공식적으로 지원하는 언어만 유지하려면 resConfig 속성을 사용하여 언어를 지정할 수 있습니다.
  • 지정되지 않은 언어의 리소스는 모두 삭제됩니다.
  • Android App Bundle 형식을 사용하여 앱을 출시하는 경우 기본적으로 앱을 설치할 때 사용자 기기에서 구성된 언어만 다운로드 됩니다.
  • 마찬가지로 기기의 화면 밀도와 일치하는 리소스 및 기기의 ABI와 일치하는 네이티브 라이브러리만 다운로드에 포함된다.
  • APK로 출시되는 기종 앱의 경우 각각 다른 기기 설정을 타겟팅하는 여러 APK를 빌드하여 APK에 포함할 화면 밀도 또는 ABI 리소스를 맞춤설정할 수 있습니다. 

 

중복 리소스 병합

  • 기본적으로 Gradle은 이름이 동일한 리소스를 병합한다.
  • 예: 동일한 이름의 드로어블이 다른 리소스 폴더에 있는 경우
  • 코드가 찾고 있는 이름이 여러 리소스와 일치할 때 발생하는 오류를 피해야 하므로 이 동작은 shrinkResources 속성에 관계없이 항상 수행되며 사용자가 이 동작을 중지할 수 는 없습니다. 
  • 리소스 병합은 둘 이상의 파일이 동일한 리소스 이름, 유형 및 한정자를 공유하는 경우에만 발생한다.
  • Gradle은 중복된 파일 중에서 가장 적합하다고 여겨지는 파일을 아래 설명한 우선순위에 따라 선택한 후 최종 아티팩트에 배포하기 위해 하나의 리소스만 AAPT에 전달한다.
    • Artifact
      • 아티팩트는 소프트웨어 개발 및 빌드 과정에서 생성되는 결과물을 의미
      • 소스코드, 컴파일된 코드, 라이블러리, 실행가능파일, 패키징된 애플리케이션등 다양한 형태를 가질 수 있다.
      • 안드로이드 앱 개발에서 가장 일반적인 예는 APK 파일이다.
    • AAPT
      • 안드로이드 앱 개발 과정에서 리소스를 관리하고 앱의 리소스와 소스코드를 APK 파일로 패키징하는데 사용되는 도구이다.
  • 중복리소스 병합 과정
    • 리소스 위치
      • 중복 리소스는 다음 위치에서 찾을 수 있다.
        • 기본 소스 세트와 연관된 리소스(일반적으로 src/main/res/ 에 위치)
        • 빌드 유형 및 빌드 버전에서 파생된 변형 오버레이
          • 빌드 유형(예: debug, release) 및 제품 플레이버(예: free, paid)에 따라 다른 리소스를 정의할 수 있습니다.
          • 이러한 변형 오버레이는 특정 빌드 유형이나 플레이버에 맞춰진 리소스를 포함할 수 있고 기본 리소스를 오버라이드 할 수 있다.
        • 라이브러리 프로젝트 종속 항목
          • 라이브러리 프로젝트는 자체 리소스를 포함할 수 있고 이 것들도 최종 앱 아티팩트에 포함된다.
    • 우선 순위 단계
      • 종속 항목
        • 프로젝트가 의존하는 외부 라이브러리나 모듈을 의미한다.
        • 이들 종속 항목에 포함된 리소스가 중복된 경우, 우선 순위가 가장 낮다.
      • 기본
        • 프로젝트의 주 리소스 세트
        • src/main/res 디렉토리에 위치한 리소스이다.
      • 빌드 버전
        • 특정 빌드 버전 또는 제품 플레이버에 해당하는 리소스를 말한다.
      • 빌드 유형
        • 빌드 유형
          • debug나 release 같은 빌드 설정을 의미한다.
          • 가장 높은 우선순위를 가진다.
    • 리소스 병합 오류
      • 동일 소스 세트 내 중복
        • 동일한 소스 세트 내에서 동일한 리소스가 중복되면 Gradle은 이를 병합할 수 없으며 리소스 병합 오류가 발생
        • 오류 발생 조건: build.gradle 파일의 sourceSets 속성에 여러개의 소스 세트가 정의되어 있고, 같은 이름의 리소스가 여러 세트에 포함된 경우에 이 오류를 발생할 수 있습니다.

 

코드 난독화

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 파일(안드로이드 앱의 실행파일) 크기를 줄일 수 있다.
  • 난독화와 스택 트레이스
    • 스택 트레이스
      • 프로그램이 실행되는 동안 특정 시점에 호출 스택(call stack)의 상태를 나타내는 정보입니다.
      • 호출 스택은 프로그램에서 함수 또는 메서드가 호출되어 실행되는 순서와 구조를 추적하는데 사용된다.
      • 오류로 프로그램이 중단될 때, 스택 트레이스는 오류가 발생한 지점과 그 이전에 실행된 함수를 보여준다.
    • 난독화된 앱에서 오류가 발생하면, 스택 트레이스도 난독화된 이름을 사용합니다. 
    • 이로 인해 스택 트레이스를 해석하기 어려워질 수 있다.
    • 해결 방법
      • 난독화된 스택 트레이스를 원래 형태로 되돌리기 위해서는 mapping file이 필요하다.
      • 이 파일은 난독화 과정에서 생성되며, 원래 이름과 난독화된 이름간의 매핑 정보를 담고 있다.
  • 리플렉션과 Keep 규칙
    • 리플렉션
      • 리플렉션은 프로그램이 자기 자신의 구조(클래스, 인터페이스, 필드, 메서드 등)에 대한 정보를 검사하고 조작할 수 있게 하는 프로그래밍 기능
      • java, kotlin에서 리플렉션은 java.lang.reflect 패키지를 통해 제공된다.
      • 사용 사례
        • 런타임에 동적으로 객체를 생성하거나 메서드를 호출할 때 사용한다.
        • 특정 클래스 이름이나 메서드 이름을 미리 알지 못하는 경우에 유용
        • 많은 프레임워크와 라이브러리는 사용자 정의 클래스와 메서드를 자동으로 찾아내고 사용하기 위해 리플렉션을 사용합니다.
        • 예를 들어 Java의 Spring 프레임 워크는 리플렉션을 사용하여 객체의 의존성을 주입한다.
      • 리플렉션은 런타임에 클래스의 메타데이터를 조사하고, 클래스의 메서드나 필드에 접근하는 방법입니다.
      • 리플렉션을 사용하는 코드는 예측 가능한 이름 지정을 필요로 합니다.
    • Keep 규칙의 필요성
      • 난독화 과정에서 리플렉션을 사용하는 클래스나 메서드의 이름이 변경되면, 리플렉션 호출이 실패할 수 있습니다.
      • 따라서 난독화 도구(ProGuard, R8)에 이러한 클래스나 메서드의 원래 이름을 유지하도록 지시하는 keep규칙을 설정해야합니다.
  • 난독화된 스택 트레이스 디코딩
    • 밑에 스택트레이스 다시 추적이라는 글 참고

 

코드 최적화

class MyClass {
    fun myMethod() {
        println("Hello, World!")
    }

    fun callMethod() {
        myMethod()
    }
}

class MyClass {
    fun callMethod() {
        println("Hello, World!") // myMethod의 내용이 여기로 인라인 됨
    }
}

 

abstract class BaseClass {
    abstract fun doSomething()
}

class ConcreteClass : BaseClass() {
    override fun doSomething() {
        println("Do something")
    }
}

class ConcreteClass {
    fun doSomething() {
        println("Do something")
    }
}

 

  • 앱을 더 많이 축소하기 위해 R8은 코드를 더 상세히 검사하여 사용하지 않는 코드를 추가로 삭제하거나 가능하다면 코드를 간결하게 다시 작성합니다. 
  • 최적화의 예시
    • 주어진 if/else 구문에서 코드가 else{} 분기를 전혀 사용하지 않는 경우 R8은 else{}분기 코드를 삭제할 수 있습니다.
    • 코드가 한 곳에서만 메서드를 호출하는 경우 R8은 이 메서드를 삭제하고 단일 호출 사이트에서 인라인으로 처리할 수 있습니다.
    • 클래스에서 고유한 서브클래스가 하나만 있고 클래스 자체가 인스턴스화 되지 않는다고 판단되면(예: 하나의 구체적인 구현 클래스에서만 사용하는 추상적인 기본 클래스) R8은 두 개의 클래스를 결합하여 앱에서 클래스를 삭제할 수 있습니다.
  • R8의 최적화 제한
    • 사용자 정의 최적화 제한
      • R8은 사용자가 임의로 최적화를 조정하거나 수정하는 것을 허용하지 않습니다.
      • ProGuard에서 사용되는 -optimizations 또는 -optimizationpasses와 같은 설정을 통해 기본 최적화 과정을 변경하는 것을 R8은 무시합니다.
      • 왜냐하면 최적화의 일관성 유지와 개발자가 직면할 수 있는 문제를 쉽게 해결하기 위함
  • 스택 트레이스의 변경
    • 최적화와 스택 트레이스
      • R8의 최적화 과정 중 일부는 스택 트레이스를 변경할 수 있습니다.
      • 예를 들어, 메서드 인라인 최적화는 스택 트레이스에서 해당 메서드 호출을 제거합니다.
      • 이는 오류 추적과 디버깅을 복잡하게 만들 수 있습니다.
      • ReTrace(재추적)
        • 난독화 및 최적화된 앱의 스택트레이스를 원래 형태로 복원하기 위해 ReTrace 도구를 사용할 수 있다.
        • 이 도구는 난독화 매핑 파일을 사용하여 난독화된 스택 트레이스를 원래의 가독성 있는 형태로 되돌립니다.
  • 더 적극적인 최적화 사용
    • R8은 기본적으로 사용하지 않는 추가적인 최적화 조합을 포함합니다.
    • 프로젝트의 gradle.properties 파일에 다음 내용을 포함하여 추가적인 최적화를 사용할 수 있습니다.
    • android.enableR8.fullMode=true
    • 추가적인 최적화를 사용하면 R8이 ProGuard와 다르게 동작하기 때문에 런타임 문제를 피하고자 ProGuard 규칙을 추가로 포함해야 할 수 도 있습니다.
    • 예를 들어 코드가 Java Reflection API를 통해 클래스를 참조한다고 가정한다.
    • 기본적으로 실제로 그렇게 코드를 작성하지 않았더라도 R8은 런타임에 클래스의 객체를 검사하고 조작하는 코드가 있다고 가정하고 자동으로 클래스와 클래스의 정적 초기화 프로그램을 유지한다.
    • 그러나 full mode를 사용하면 R8은 이러한 가정을 하지 않으며 런타임에 코드가 클래스를 사용하지 않는다고 판단되면 앱의 최종 DEX에서 클래스를 삭제한다.
    • R8은 정적 초기화 블록을 자동으로 보존하지 않는다. 그래서 keep 규칙을 설정해야한다.
    • 즉, 클래스와 클래스의 정적 초기화 프로그램을 유지하려면 규칙파일에 keep 규칙을 포함해야합니다.

 

 

stacktraces 재추적

//proguard-rules.pro 파일에 위의 규칙을 추가하여 재추적할 충분한 정보를 빌드에 유지해야한다.
-keepattributes LineNumberTable,SourceFile
-renamesourcefileattribute SourceFile
  • R8에서 처리된 코드는 스택 트레이스를 더 이해하기 어렵게 할 수 있는 여러 방식으로 변경됩니다.
  • 스택 트레이스가 소스 코드와 정확히 일치하지 않기 때문입니다.
  • 디버깅 정보가 유지되지 않을 때 줄 번호를 변경하는 경우를 예로 들 수 있습니다.
  • 코드가 축소되고 난독화되면 줄 번호가 변경된다.
  • 원래 스택 트레이스를 복구하기 위해 R8은 명령줄 도구 패키지와 함께 번들로 제공되는 retrace 명령줄 도구를 제공합ㄴ디ㅏ.
  • 앱의 스택트레이스 재추적을 지원하려면 모듈의 proguard-rules.pro 파일에 위의 규칙을 추가하여 재추적할 충분한 정보를 빌드에 유지해야한다.
  • LineNumberTable 속성은 바이트코드 내의 각 명령어가 원래 소스 파일에서 어느 중에 위치하는지에 대한 정보를 제공합니다.
  • SourceFile 속성은 클래스 파일과 연결된 원본 소스 파일의 이름을 저장합니다.
  • renamesourcefileattribute 속성
    • 난독화 과정에서 모든 소스 파일 이름을 SourceFile이라는 단일 이름으로 변경합니다.
    • 난독화된 스택 트레이스에서 소스 파일 이름을 익명화 항여 보안을 강화하는데 도움이 된다.
    • 원본 소스 파일의 이름은 매핑 파일에 저장되어 있으므로, 난독화 과정에서 소스파일 이름을 변경해도 재추적(retrace) 도구를 사용하여 원본 스택 트레이스를 복구할 수 있습니다.
  • 앱을 Google Play에 게시하는 경우 각 앱 버전의 mapping.txt 파일을 업로드할 수 있습니다.
  • Android App Bundle을 사용하여 게시할 때 이 파일은 자동으로 App Bundle 콘텐츠의 일부로 포함됩니다.
  • 그러면 Google Play는 사용자가 보고한 문제에서 수신한 스택 트레이스를 재추적하므로 Play Console에서 스택 트레이스를 검토할 수 있습니다.
  • 비정상 종료 스택 트레이스를 가독화(https://support.google.com/googleplay/android-developer/answer/6295281?hl=ko)

 

 

R8 문제 해결

 

keep 규칙을 적용해야 하는 이유

  1. 리플렉션을 사용하는 코드
    1. 예시 : 클래스, 메서드, 필드 이름이 리플렉션을 통해 런타임에 사용될 때
    2. 이유 : 난독화로 인해 리플렉션을 사용하는 코드가 실패할 수 있습니다.
  2. JNI(Java Native Interface)를 사용하는 코드
    1. 예시 : 네이티브 코드(C/C++)에서 호출되는 Java 메서드
    2. 이유 : JNI 호출은 문자열 기반으로 메서드 이름을 참조하기 때문에, 이러한 메서드의 이름은 변경되면 안됨
  3. 엔트리 포인트
    1. 예시 : Acitivity, Service, BroadcastReceiver, ContentProvider
    2. 이유 : 이러한 클래스는 앱의 메니페스트에 명시되며, 안드로이드 시스템에 의해 집접 호출 됩니다.
  4. 프레임워크 또는 라이브러리에 의존하는 코드
    1. 예시: 특정 라이브러리에 의해 사용되는 콜백 메서드나 인터페이스
    2. 이유: 라이브러리 내부에서 기대하는 이름과 서명을 유지해야 올바르게 작동함
  5. XML 파일에서 참조되는 리소스
    1. 예시 : 레이아웃 XML 파일에서 참조되는 클래스( 예: 사용자 정의 뷰)
    2. 이유 : XML 파일에서 참조되는 클래스 이름은 변경되어서는 안된다.
  6. Serializable 객체
    1. 예시 : 직렬화/ 역직렬화 과정에서 사용되는 클래스
    2. 이유 : 객체의 직렬화 형식은 클래스 이름에 의존합니다.
    3. 직렬화
      1. 객체를 바이트 스트림으로 변환합니다.
      2. 변환된 바이트 스트림은 파일 시스템, 네트워크등 다양한 저장 매체에 저장되거나 전송됩니다.
    4. 역직렬화
      1. 바이트 스트림을 읽어서 원래 객체의 상태로 복원한다.
      2. 복원된 객체는 원래의 속성과 값을 가집니다.

 

R8,  Proguard 지시어

  • -keepattributes Signature
    • 중요성
      • Signature 속성은 클래스, 인터페이스, 메서드 및 필드에 대한 제네릭 타입 정보를 포함한다.
      • java나 kotlin에서 제네릭 타입은 컴파일 타임에만 타입정보를 제공하고, 런타임에는 타입소거로 인해 이 정보는 일반적으로 사용할 수 없습니다.
      • 하지만 Signature 속성은 이 정보를 런타임에도 유지하도록 해준다.
    • 적용 대상
      • 앱의 모든 클래스 파일에 적용된다.
  • -dontwarn org.conscrypt.**
    • org.conscrypt 패키지와 그 하위 패키지에 대한 모든 경고를 무시합니다.
  • -keep class androidx.**{*;}
    • -keep class androidx.**
      • androidx 패키지 및 그 하위 패키지에 있는 모든 클래스를 유지하라는 지시이다.
    • {*;}
      • 클래스 내의 모든 멤버(필드, 메서드 등)을 포함하라는 의미이다.
      • *은 모든 멤버를 나타내며 ;는 명령어의 끝을 표시합니다.
    • 클래스 이름과 클래스 구현을 난독화하지 않는다.
      • 메니페스트에 있는 액티비티
        • 이름은 난독화 안되고 구현은 난독화 됨
  • -keepnames class androidx.**
    • 클래스 이름은 유지하지만, 클래스의 멤버(메서드와 필드)는 난독화 대상이 될수 있다. 즉 멤버 이름은 변경될수 있다.
  • keepclassmembers
    • 이 지시어는 클래스 자체의 이름은 보존하지 않지만, 그 안에 있는 멤버드른 보호합니다.
-keepclassmembers class com.example.MyClass {
    public <methods>;
}
// MyClass의 모든 public 메서드를 보존하려면 다음과 같이 작성합니다.