R8로 플랫폼 코드 및 리소스 최적화

Android 플랫폼 빌드 시스템 (Soong)은 Android 앱 (android_app), 테스트(android_test), compile_dex: true가 있는 설치 가능한 Java 라이브러리 (예: services.jar)를 비롯하여 DEX 바이트 코드를 컴파일하는 타겟에서 R8 컴파일러를 실행하여 사용되지 않는 코드와 리소스를 축소, 최적화, 제거합니다. 정적 라이브러리 (android_library 및 정적 java_library)에서 Soong은 R8을 직접 실행하지 않고 optimize 속성 블록을 사용하여 소비자 유지 규칙 (export_proguard_flags_files: true)을 정적으로 연결하는 다운스트림 DEX 타겟에 연결하고 전파합니다.

시스템 이미지 또는 공급업체 패키지를 빌드하는 플랫폼 엔지니어의 경우 지나치게 광범위한 유지 규칙을 제거하면 시스템 상태에 직접적인 이점이 있습니다.

  • 더 작은 시스템 파티션 공간: R8은 APK 및 JAR을 /system, /system_ext, /product, /vendor에 패키징하기 전에 사용되지 않는 클래스, 메서드, 리소스를 삭제합니다.
  • 더 작은 컴파일된 아티팩트: DEX 메서드가 적으면 빌드 시간 또는 기기 내 컴파일 중에 dex2oat에서 생성되는 .odex 및 .vdex 파일이 더 작아집니다.
  • 런타임 메모리 압박 감소: Android는 요청 페이징 mmap 호출을 사용하여 .odex, .vdex, APK 리소스 테이블을 프로세스 메모리에 매핑하므로 바이너리가 작을수록 앱 시작 중에 주요 페이지 오류가 줄어들고 프로세스 전반에서 상주 코드 및 리소스 메모리가 줄어듭니다. 컴파일된 앱 코드가 기기 메모리에 미치는 영향에 관한 자세한 내용은 앱 코드는 메모리를 참고하세요.
  • 더 효과적인 전체 프로그램 최적화: 좁은 유지 제약조건을 사용하면 R8이 메서드를 인라인하고, 인터페이스 및 가상 호출을 가상화 해제하고, 사용되지 않는 필드를 프루닝하고, 클래스 전반에 상수를 전파할 수 있습니다.

Soong에서 R8 최적화 구성

Android.bp 파일에서 android_app 및 설치 가능한 java_library 타겟 (또는 android_library 및 정적 java_library 모듈에서 소비자 유지 규칙 내보내기)의 optimize 속성 블록을 사용하여 R8 최적화를 구성합니다.

android_app {
    name: "MySystemApp",
    srcs: ["src/**/*.java"],
    optimize: {
        obfuscate: true,
        shrink_resources: true,
    },
}

다음 표에는 Soong에서 가장 일반적인 optimize 속성(build/soong/java/dex.go에 정의됨)이 요약되어 있습니다.

속성 설명
enabled 타겟에서 R8이 실행되는지 여부를 제어합니다. 모든 android_app 타겟의 기본값은 true입니다.
shrink 트리 흔들기를 제어하여 연결할 수 없는 클래스, 필드, 메서드를 삭제합니다. android_app 타겟의 기본값은 true입니다 (독립형 java_library 및 테스트 모듈의 경우 false이며 명시적으로 설정하지 않는 한 -dontshrink를 전달함).
optimize 메서드 인라인, 클래스 병합, 상수 전파, 데드 브랜치 삭제와 같은 바이트 코드 최적화를 제어합니다. android_app 타겟의 경우 기본값은 true입니다 (RELEASE_R8_OPTIMIZE_BY_DEFAULT 출시 빌드 플래그로 제어됨).
obfuscate 식별자 축소 및 이름 바꾸기를 제어합니다. 이전 버전과의 호환성을 위해 기본값은 false이지만 가능하면 true로 설정하여 DEX 크기를 줄이고 더 심층적인 최적화를 사용 설정하세요 (가능한 경우 난독화 사용 설정 참고).
shrink_resources 코드 축소 후 패키징된 APK에서 사용되지 않는 리소스 (res/개 항목)를 삭제합니다. 기본값은 false입니다.
optimized_shrink_resources 통합 R8 코드 및 리소스 축소 도구 파이프라인을 실행하여 R8이 한 번에 코드 및 리소스 참조를 공동으로 추적합니다. shrink_resources: true가 설정된 경우 기본값은 RELEASE_USE_OPTIMIZED_RESOURCE_SHRINKING_BY_DEFAULT입니다(표준 플랫폼 빌드에서는 true).
proguard_flags_files 커스텀 유지 규칙이 포함된 모듈별 .flags 또는 .pro 파일을 나열합니다. 표준 주석이나 기본값으로 충분한 경우 커스텀 파일을 추가하지 마세요.
export_proguard_flags_files 이 라이브러리의 proguard_flags_files를 정적으로 종속되는 다운스트림 모듈에 전파합니다.
trace_references_from 바이트코드가 이 타겟을 참조하는 직접 DEX 컴파일 종속 타겟 (instrumentation_for 또는 libs를 통해 연결됨)을 나열합니다. R8은 이를 자동으로 추적하고 유지합니다 (override_android_app 및 override_android_test 변형 전반에 전파되며 .impl 분할과 페어링된 경우 종속되지 않는 호환 Java 라이브러리에 지원됨).
allow_traced_references instrumentation_for 또는 libs를 통해 연결된 직접 DEX 컴파일 종속 타겟 (예: trace_references_from에 나열할 수 없는 교차 저장소 또는 공급업체 테스트)이 trace_references: true를 설정하여 이 타겟에 대한 바이트 코드 참조를 추적하도록 선택할 수 있습니다.
trace_references allow_traced_references: true가 있는 타겟의 직접 DEX 컴파일 종속 항목에서 true로 설정하면 이 모듈에서 R8 tracereferences을 실행하고 생성된 유지 규칙을 타겟 모듈에 전달합니다.
protect_api_surface --protect-api-surface를 R8에 전달합니다. java_sdk_library에 설정하면 사용 설정된 모든 API 범위 (공개, 시스템, module-lib, system-server, 테스트)에서 Metalava API 유지 규칙을 자동으로 생성하고 구현 라이브러리에 연결합니다.

정적 라이브러리 (android_library, java_library, AAR 가져오기)는 Android.bp에서 required_downstream_optimization: { enabled: true, optimize: true }를 설정하여 정적으로 연결하는 다운스트림 DEX 컴파일 타겟 (직접 또는 전이적)이 R8 축소 및 바이트 코드 최적화를 사용 설정하도록 요구할 수도 있습니다. Soong은 androidx.compose.runtime_runtime에서 이를 자동으로 사용 설정하므로 Jetpack Compose를 연결하는 타겟은 R8로 최적화됩니다.

가능한 경우 난독화 사용 설정

Soong에서 obfuscate는 이전 버전과의 호환성을 위해 기본적으로 false로 설정되지만 가능하면 android_app 및 독립형 DEX 타겟에서 obfuscate: true를 명시적으로 설정해야 합니다.

  • 더 작은 DEX 및 .vdex 공간: 패키지, 클래스, 필드, 메서드의 이름을 짧은 식별자 (a, b)로 바꾸면 DEX 문자열 풀(string_ids 및 string_data_item)과 유형 설명자가 축소됩니다. Android는 .vdex 및 .odex 파일을 프로세스 메모리에 매핑하므로 기호 테이블이 작을수록 시스템 파티션 크기와 런타임 메모리 사용량이 직접적으로 줄어듭니다.
  • 더 심층적인 전체 프로그램 최적화: R8에서 식별자 이름을 바꾸도록 허용하면 이름 충돌을 방지하기 위해 R8이 건너뛰어야 하는 패키지 전반에서 클래스 병합, 패키지 평면화, 합성 클래스 중복 삭제가 가능해집니다.
  • 전체 스택 트레이스 기호화: Soong에서 난독화를 사용 설정해도 디버그 가능성이 줄어들지 않습니다. 모든 R8 컴파일 타겟에 대해 Soong은 모듈의 중간 디렉터리에 proguard_dictionary 매핑 파일을 내보내고, 모든 모듈 사전을 빌드의 proguard-dict.zip 아티팩트에 번들로 묶고, 매핑 해시 (--map-id-template)를 DEX 헤더에 삽입하고, 클래스 SourceFile 속성 (--source-file-template)을 다시 작성하여 retrace가 스택 트레이스를 자동으로 기호화할 수 있도록 합니다.

다음의 경우에만 obfuscate: false를 유지합니다 (또는 공개 API 이름을 명시적으로 보존).

  • bootclasspath 또는 system_server classpath의 공유 라이브러리: 런타임 시 다른 모듈 (예: framework.jar, services.jar 또는 <uses-library> 타겟)에 의해 동적으로 연결된 외부 API 노출 영역을 노출하는 모듈 (java_library 또는 java_sdk_library)은 obfuscate: false를 유지하거나 protect_api_surface: true, @KeepForApi 또는 -keep 규칙을 사용하여 API 노출 영역을 명시적으로 보존해야 합니다. 그래야 스텁에 대해 컴파일된 호출자가 런타임 시 클래스 및 멤버 이름을 확인할 수 있습니다. java_sdk_library 모듈에서 protect_api_surface: true를 설정하면 사용 설정된 모든 API 범위 (공개, 시스템, module-lib, system-server, 테스트)에서 Metalava ProGuard 유지 규칙이 자동으로 생성되어 구현 라이브러리에 연결됩니다. 업데이트할 수 없는 bootclasspath 타겟 (framework-minus-apex, ext, framework-location)에서 일반화된 R8 최적화가 사용 설정되면 활성 boot/hiddenapi/*.txt 파일에 나열된 멤버를 유지하기 위해 framework-hiddenapi-proguard-rules (hiddenapi_to_proguard에서 생성됨)도 포함합니다 (hiddenapi-max-target-o.txt 생략).

애플리케이션에서 obfuscate: true를 사용 설정하기 전에 다음 마이그레이션 기본 요건을 확인하세요.

  • 외부 테스트 APK 종속 항목: 외부 android_test APK가 instrumentation_for: "MyApp"를 설정하고 MyApp의 내부 클래스나 메서드를 직접 호출하는 경우 이러한 기호의 이름을 바꾸면 테스트 런타임에 NoSuchMethodError 또는 NoClassDefFoundError가 발생합니다. trace_references_from(또는 trace_references: true가 있는 allow_traced_references)를 구성하고, MyApp.impl를 정적으로 연결하는 자체 계측 APK로 테스트를 구성하거나, @VisibleForTesting로 테스트 후크에 주석을 추가하여(2단계: 테스트 기반 유지 규칙 이전 참고) 테스트에 필요한 내부 기호는 보존하면서 앱의 나머지 부분은 난독화합니다.
  • 문자열 기반 리플렉션 및 JNI: 모듈이 유지 규칙이나 주석 없이 리터럴 문자열 이름 (Class.forName, getDeclaredMethod 또는 JNI FindClass 및 GetMethodID)으로 클래스, 메서드 또는 필드를 조회하는 경우 난독화는 이러한 타겟의 이름을 바꾸고 런타임 조회를 중단합니다. (주석이 없는 리플렉션도 shrink: true 및 optimize: true에서 실패합니다.) 주석 유지 가이드 (@UsesReflection, @UsedByReflection 또는 @UsedByNative)를 사용하여 이러한 진입점에 주석을 달면 R8이 이름을 보존하고 모듈의 나머지 부분을 난독화합니다.

커스텀 플래그 제로 원칙 준수

이상적인 Android 플랫폼 모듈에는 맞춤 proguard.flags 또는 keep.xml 파일이 없습니다. Android 플랫폼 빌드에서 대부분의 맞춤 유지 규칙은 중복됩니다.

  • 플랫폼 기준 및 AAPT2: 대부분의 진입점은 전역 플랫폼 기준 (예: @Keep, JNI native 메서드, @VisibleForTesting)에 의해 이미 자동으로 유지되거나 AndroidManifest.xml 및 레이아웃 리소스에서 AAPT2에 의해 생성됩니다 (기본 플랫폼 유지 규칙 이해 참고).
  • 타겟팅된 대안: 진입점을 보존해야 하는 경우 분리된 .flags 파일보다 선언 사이트 주석 (keepanno, @VisibleForTesting), 내보낸 라이브러리 규칙 (export_proguard_flags_files: true) 또는 정적 테스트 연결을 선호합니다 (기존 유지 규칙 감사 및 이전 참고).

커스텀 proguard.flags 또는 keep.xml 파일을 추가하거나 유지하기 전에 규칙이 이미 기본 기준에 의해 처리되었는지 또는 코드 주석으로 이전할 수 있는지 확인하세요.

기본 플랫폼 보관 규칙 이해하기

Soong은 build/soong/java/dex.go에 구성된 DEX 컴파일 타겟 (android_app, android_test, 설치 가능한 java_library 모듈)의 모든 R8 호출에 다음 전역 기준 규칙을 자동으로 전달합니다.

  • build/make/core/proguard.flags: 모든 패키지에서 @com.android.internal.annotations.VisibleForTesting로 주석이 지정된 클래스와 멤버를 보존하고 android.**, com.android.**, com.google.android.** 패키지 내에서 @VisibleForTesting(androidx.annotation.VisibleForTesting 또는 com.google.common.annotations.VisibleForTesting)로 주석이 지정된 클래스와 멤버를 보존합니다. 또한 @TestApi, @Keep (androidx.annotation, android.support.annotation, com.android.internal.annotations), @KeepForWeakReference, @WeaklyReferencedCallback, @dalvik.annotation.optimization.**도 보존합니다.
  • build/make/core/proguard_basic_keeps.flags: 스택 트레이스, 런타임 공개 상태 주석(RuntimeVisible*Annotations), Exceptions 및 AnnotationDefault 속성, native 메서드, Serializable 멤버, @JavascriptInterface 메서드, Throwable(String) 생성자, Parcelable$CREATOR 필드, protobuf MessageLite 필드의 SourceFile 속성을 보존합니다.
  • build/make/core/proguard/kotlin.flags: 특정 Kotlin 메타 주석 (kotlin.Metadata 및 kotlin.annotation.{AnnotationRetention,AnnotationTarget,Retention,Target})에 관한 무해한 경고를 무음으로 처리하고 출시 빌드에서 Kotlin DebugMetadata 주석을 제거합니다.
  • build/make/core/proguard/checknotnull.flags: 일반적인 null 검사 도우미 호출 (com.google.common.base.Preconditions.checkNotNull 및 dagger.internal.Preconditions.checkNotNull*)을 간결한 바이트 코드 null 검사로 대체합니다. 이 파일은 프레임워크 API 경계에서 명시적 예외 메시지를 보존하기 위해 의도적으로 Objects.requireNonNull를 생략합니다.
  • build/make/core/proguard/enumvalues.flags: 빌드 구성에서 사용 중지하지 않는 한 enum 유형의 values 및 valueOf 메서드를 유지합니다.
  • AAPT2 자동 생성 규칙: AAPT2는 병합된 AndroidManifest.xml, 레이아웃 XML, 환경설정 XML 파일을 검사하여 등록된 모든 Activity, Service, BroadcastReceiver, ContentProvider, BackupAgent, Application, 맞춤 View 서브클래스, Preference 서브클래스, XML android:onClick 메서드의 정확한 유지 규칙을 생성합니다.

기존 보관 규칙 감사 및 이전하기

플랫폼 저장소에서 기존 proguard.flags 또는 keep.xml 파일을 감사할 때는 다음 4단계 계층 구조에 따라 각 규칙을 순서대로 평가합니다.

1단계: 중복되거나 오래된 규칙 삭제하기

전역 기준, AAPT2 매니페스트 및 레이아웃 규칙 또는 R8 기본값에 이미 포함된 규칙과 더 이상 존재하지 않는 클래스 또는 패키지를 참조하는 규칙을 삭제합니다.

.flags 파일의 모든 규칙이 중복되면 파일을 삭제하고 Android.bp에서 proguard_flags_files를 삭제합니다. 남은 optimize 블록이 기본 설정만 다시 명시하는 경우 중복 블록을 삭제하고 bpfmt -w Android.bp를 사용하여 빌드 파일의 형식을 지정합니다. 패키지를 정리할 때 하위 디렉터리의 컴패니언 모듈 (예: Kotlin 타겟 변형)이 동일한 플래그 파일을 참조하는지 확인하고 함께 업데이트합니다.

2단계: 테스트 기반 보관 규칙 이전하기

다음 패턴 중 하나를 사용하여 테스트 기반 유지 규칙을 커스텀 프로덕션 .flags 파일 외부로 이동합니다.

  • 테스트 또는 호환 타겟 (trace_references_from 또는 allow_traced_references)의 참조 추적: 타겟과 직접 종속 모듈이 모두 자체 DEX (android_app, android_test 또는 dexed java_library)를 컴파일하고 instrumentation_for 또는 libs를 통해 연결된 경우 Soong은 종속 모듈에서 R8 tracereferences를 실행하고 종속 항목 주기를 만들거나 수동 .impl 라이브러리 분할을 요구하지 않고 생성된 유지 규칙을 타겟의 R8 컴파일에 제공할 수 있습니다.

    • 동일 저장소 테스트(trace_references_from): 타겟 모듈의 직접 instrumentation_for 또는 libs 종속 항목을 나열합니다. 이는 override_android_app 및 override_android_test 변형 전반에 자동으로 전파되며 .impl 분할과 페어링된 경우 종속되지 않는 컴패니언 Java 라이브러리를 계속 지원합니다.

      android_app {
          name: "MyApp",
          srcs: ["src/**/*.java"],
          optimize: {
              obfuscate: true,
              shrink_resources: true,
              trace_references_from: ["MyAppTests"],
          },
      }
      
      android_test {
          name: "MyAppTests",
          instrumentation_for: "MyApp",
          srcs: ["tests/**/*.java"],
      }
      
    • 교차 저장소 또는 공급업체 테스트 (allow_traced_references 및 trace_references): 타겟 모듈이 trace_references_from에 종속 항목을 직접 나열할 수 없는 경우 (예: 다른 저장소 또는 공급업체 트리의 테스트) 타겟 모듈에 allow_traced_references: true를 설정하고 각 종속 항목에서 trace_references: true를 사용하여 선택합니다.

      android_app {
          name: "MyApp",
          srcs: ["src/**/*.java"],
          optimize: {
              allow_traced_references: true,
          },
      }
      
      android_test {
          name: "MyAppVendorTests",
          instrumentation_for: "MyApp",
          srcs: ["tests/**/*.java"],
          optimize: {
              trace_references: true,
          },
      }
      
    • 테스트의 문자열 기반 리플렉션: tracereferences는 바이트 코드 상수 풀 참조를 검사하며 테스트의 문자열 기반 리플렉션 (Class.forName, getDeclaredField, getDeclaredMethod 또는 리플렉션 필드 삽입 도우미)을 감지할 수 없습니다. 반사적으로 액세스되는 프로덕션 필드 및 메서드에 @VisibleForTesting (또는 @UsedByReflection) 주석을 답니다.

  • 구현 라이브러리를 테스트에 연결 (static_libs): 프로덕션 APK에 테스트 전용 진입점을 유지하지 않고 테스트 변경사항이 프로덕션 타겟에서 R8을 다시 트리거하지 않도록 하려면 앱 소스 파일을 android_library (MyApp.impl)에 배치하고 MyApp.impl을 MyApp 및 MyAppTests에 정적으로 연결합니다.

    android_library {
        name: "MyApp.impl",
        srcs: ["src/**/*.java"],
        manifest: "AndroidManifest.xml",
    }
    
    android_app {
        name: "MyApp",
        static_libs: ["MyApp.impl"],
        optimize: {
            obfuscate: true,
            shrink_resources: true,
        },
    }
    
    android_test {
        name: "MyAppTests",
        srcs: ["tests/**/*.java"],
        static_libs: ["MyApp.impl"],
    }
    

    이 3개 모듈 패턴을 사용하면 MyAppTests가 내부 클래스에 대한 전체 액세스 권한을 가진 자체 계측 테스트로 실행될 수 있으며, MyApp는 프로덕션 진입점에 대해서만 축소 및 난독화할 수 있습니다.

  • @VisibleForTesting로 테스트 후크에 주석 달기: 외부 테스트 APK가 android_app 타겟에서 몇 가지 내부 메서드 또는 생성자만 호출하는 경우 @VisibleForTesting로 이러한 선언에 주석을 달 수 있습니다. 전역 build/make/core/proguard.flags 기준은 커스텀 .flags 파일 없이 android.**, com.android.**, com.google.android.** 패키지의 @VisibleForTesting 항목을 자동으로 유지합니다. 이러한 네임스페이스 외부의 공급업체 모듈의 경우 trace_references_from, @UsedByReflection 또는 내보낸 라이브러리 규칙을 사용합니다. trace_references_from 또는 trace_references를 사용하는 대신 @VisibleForTesting로 직접 주석을 달 때는 Mockito mock(...) 또는 spy(...)에 전달된 클래스에도 주석을 답니다 (스텁 메서드만 주석을 다는 것이 아님). 기기 내 mockito-target (dexmaker-mockito)가 런타임 서브클래스를 생성하고 R8이 서브클래스가 아닌 클래스를 final로 표시할 수 있기 때문입니다 (MockitoException: Can't mock/spy class ... - final class).

  • 중복되는 정적 라이브러리 (jarjar_rules) 방지 또는 재패키징: android_app와 instrumentation_for 테스트 APK가 모두 동일한 라이브러리를 정적으로 연결하는 경우 (guava와 같이 직접 또는 전이적으로) tracereferences는 테스트에 정의된 클래스를 확인하고 앱의 유지 규칙을 내보내지 않으며 런타임 시 앱의 상위 클래스 로더가 R8 축소 클래스로 테스트의 사본을 가려 IllegalAccessError 또는 NoSuchMethodError가 발생합니다. 테스트에 자체 번들 사본이 필요하지 않은 경우 테스트의 static_libs에서 중복을 삭제하거나 libs를 통해 연결하여 충돌을 방지합니다. 그렇지 않으면 jarjar_rules를 사용하여 모듈별 접두사 아래에 앱의 중복 정적 라이브러리 패키지를 다시 패키징합니다.

3단계: 소유 라이브러리에서 규칙 내보내기

공유 java_library 또는 android_library에 자체 내부 리플렉션 또는 JNI 콜백을 위한 유지 규칙이 필요한 경우 라이브러리 타겟에 규칙을 정의하고 export_proguard_flags_files: true를 설정합니다.

java_library {
    name: "my-shared-library",
    srcs: ["src/**/*.java"],
    optimize: {
        proguard_flags_files: ["proguard.flags"],
        export_proguard_flags_files: true,
    },
}

my-shared-library를 정적으로 연결하는 모든 다운스트림 android_app 및 라이브러리 타겟은 이러한 규칙을 자동으로 상속하므로 다운스트림 앱에서 규칙을 복제할 필요가 없습니다. 여러 디렉터리에 있는 여러 모듈이 단일 코드 라이브러리에 연결되지 않은 규칙 집합을 공유해야 하는 경우 Android.bp의 명시적 filegroup를 통해 .flags 파일을 게시하여 모듈이 패키지 경계 전반에서 :my-shared-flags를 깔끔하게 참조할 수 있도록 합니다. 다운스트림 앱 최적화를 제한하지 않고 라이브러리 소비자 유지 규칙을 작성하는 방법에 관한 일반적인 안내는 라이브러리 작성자를 위한 최적화를 참고하세요.

4단계: 소스 코드의 선언에 주석 달기

클래스, 메서드, 생성자 또는 필드가 리플렉션 또는 JNI를 통해 액세스되고 AAPT2 또는 전역 기준에서 다루지 않는 경우 .flags 파일의 분리된 -keep 규칙을 주석 유지 가이드의 소스 주석으로 바꿉니다 (keepanno Javadoc 참조 참고).

  • @UsesReflection: 리플렉션을 실행하는 코드를 제어할 때 이 주석을 사용하는 것이 좋습니다. 리플렉션 호출 사이트에 배치하여 동적으로 액세스되는 타겟 클래스, 메서드 또는 필드를 선언합니다. @UsesReflection는 주석이 달린 호출 사이트 자체에 연결할 수 있다는 전제 조건을 자동으로 인코딩하므로, 호출 사이트가 사용되지 않는 경우 R8은 호출자와 리플렉션 타겟을 모두 프루닝합니다.
  • @UsedByReflection: 외부 코드 또는 라이브러리에 의해 리플렉션 방식으로 인스턴스화되거나 호출되는 클래스, 메서드, 필드 또는 생성자에 배치합니다(예: Bundle 또는 Settings 키에서 Class.forName로 로드된 클래스 또는 동적 클래스 로더 전반에 로드된 플러그인 클래스). kind (예: KeepItemKind.CLASS_AND_METHODS), preconditions (@KeepCondition), 매개변수 제약 조건을 지정하여 R8이 정확한 리플렉션 계약만 유지하고 클래스의 미사용 멤버를 최적화하거나 프루닝할 수 있도록 합니다. AAPT2는 이미 JobService 또는 BroadcastReceiver와 같은 매니페스트 등록 구성요소를 유지하므로 이러한 구성요소에 @UsedByReflection를 추가하지 마세요.
  • @UsedByNative: GetMethodID, GetStaticMethodID 또는 GetFieldID를 사용하여 C 또는 C++ JNI 코드에서 액세스하는 메서드나 필드에 배치합니다.
  • @KeepForApi: 배포 전에 라이브러리 자체가 축소될 때 그대로 유지되어야 하는 라이브러리 API 클래스 또는 멤버에 배치합니다.
  • @Keep (androidx.annotation.Keep): keepanno를 적용할 수 없는 경우 대체로 사용합니다. 클래스에 @Keep를 적용하면 클래스와 모든 멤버가 무조건 유지됩니다. 메서드나 필드에 @Keep를 적용하면 클래스가 인스턴스화되지 않더라도 멤버와 멤버가 포함된 클래스를 유지하는 무조건적인 진입점으로 작동하는 반면, keepanno는 조건부 도달 가능성을 표현합니다.

Soong 모듈에서 R8 keepanno 주석을 사용하려면 다음 단계를 따르세요.

  1. Android.bp의 libs에 "keepanno-annotations"를 추가합니다.

    libs: [
        "keepanno-annotations",
    ],
    
  2. com.android.tools.r8.keepanno.annotations.*를 가져오고 Java 또는 Kotlin 소스 코드에서 선언 또는 호출 사이트에 주석을 추가합니다.

    import com.android.tools.r8.keepanno.annotations.KeepItemKind;
    import com.android.tools.r8.keepanno.annotations.UsedByReflection;
    
    public final class CustomPluginController {
        @UsedByReflection(
            description = "Instantiated via Class.forName from plugin config",
            kind = KeepItemKind.CLASS_AND_METHODS)
        public CustomPluginController(Context context) {
            // ...
        }
    }
    

    R8은 keepanno 주석을 내부 보관 규칙 모델로 직접 변환하고 최종 DEX 출력에서 주석을 제거하여 런타임 바이트 코드 오버헤드를 추가하지 않습니다.

일반적인 문제 방지

다음 섹션에서는 플랫폼 코드에서 자주 발생하는 proguard.flags 및 keep.xml 문제와 해결 방법을 설명합니다.

광범위한 구성요소 유지 규칙

# Don't do this:
-keep class * extends android.app.Activity
-keep class * extends android.app.Service
-keep class * extends android.content.BroadcastReceiver
  • 성능에 부정적인 영향을 미치는 이유: 이 규칙은 AAPT2와 완전히 중복됩니다. 구성요소가 최종 병합된 AndroidManifest.xml에 등록되어 있는지 확인하지 않고 와일드 카드를 사용하므로 R8은 사용되지 않는 라이브러리 구성요소와 사용 중지된 디버그 활동을 포함하여 클래스 경로의 어디에서나 발견되는 모든 Activity, Service 또는 BroadcastReceiver 서브클래스를 유지합니다.
  • 권장 해결 방법: 규칙을 삭제하세요. AAPT2는 병합된 매니페스트를 검사하고 등록된 구성요소에 대한 정확한 -keep 규칙을 생성합니다.

광범위한 XML 클릭 핸들러 유지 규칙

# Don't do this:
-keepclassmembers class * {
    public void *(android.view.View);
}
  • 성능에 미치는 영향: 모듈의 모든 클래스에서 모든 public void *(View) 메서드를 유지하면 R8이 단일 View 매개변수를 허용하는 메서드를 제거하거나 인라인하지 못합니다.
  • 권장 해결 방법: 규칙을 삭제하세요. AAPT2는 레이아웃 XML 파일을 스캔하고 android:onClick 속성에서 참조하는 메서드의 타겟팅된 유지 규칙을 생성합니다.

광범위한 View getter 및 setter 유지 규칙

# Don't do this:
-keep public class * extends android.view.View {
    public <init>(android.content.Context);
    public <init>(android.content.Context, android.util.AttributeSet);
    public <init>(android.content.Context, android.util.AttributeSet, int);
    public void set*(...);
    public *** get*();
}
  • 성능에 미치는 영향: 이 규칙은 R8이 앱의 모든 View 서브클래스와 연결된 모든 라이브러리(AndroidX 및 Material 라이브러리 포함)에서 모든 getter 및 setter를 유지하도록 강제하여 UI 코드 전반에서 사용되지 않는 메서드 삭제 및 인라인 처리를 차단합니다.
  • 권장 해결 방법: 규칙을 삭제하세요. AAPT2는 레이아웃 XML 파일에서 확장된 맞춤 View 클래스의 생성자를 이미 보존하고 있습니다. 코드가 ObjectAnimator.ofFloat(view, "translationZ", ...)과 같은 반사 문자열 이름을 사용하여 뷰 속성을 애니메이션화하는 경우 문자열 이름을 유형이 지정된 속성 참조 (View.TRANSLATION_Z 또는 맞춤 FloatProperty 또는 IntProperty 구현)로 대체하여 반사를 완전히 방지합니다. 반사 속성 액세스를 피할 수 없는 경우 특정 getter 또는 setter에 @UsedByReflection 주석을 추가합니다.

일괄 최적화 또는 난독화 사용 중지

# Don't do this in proguard.flags:
-dontoptimize
-dontshrink
-dontobfuscate
  • 성능에 미치는 영향: .flags 파일 내에 -dontoptimize 또는 -dontshrink를 배치하면 모듈의 Android.bp 설정이 자동으로 재정의되고 전체 타겟에서 최적화 패스가 사용 중지됩니다. 더 나쁜 것은 라이브러리가 -dontoptimize를 포함하는 .flags 파일을 내보내는 경우 라이브러리를 연결하는 모든 다운스트림 android_app의 R8 최적화를 사용 중지한다는 것입니다.
  • 권장 해결 방법: .flags 파일에서 -dontoptimize, -dontshrink, -dontobfuscate를 삭제하세요. 리프 타겟에서 optimize 블록 (shrink, optimize, obfuscate)을 사용하여 Android.bp에서 최적화 동작을 명시적으로 제어합니다.

커밋된 규칙의 진단 플래그

# Don't do this in proguard.flags:
-verbose
-printmapping proguard.map
-printusage usage.txt
-printconfiguration config.txt
  • 빌드에 미치는 영향: 진단 플래그가 빌드 로그를 가득 채우거나 샌드박스 처리된 Soong 빌드 중에 출력 파일을 로컬 경로에 쓰려고 시도합니다.
  • 권장 해결 방법: 커밋된 .flags 파일에서 이러한 플래그를 삭제합니다. Soong은 proguard_dictionary 및 proguard_usage.zip과 같은 R8 매핑 및 사용량 출력을 out/soong/.intermediates/ 아래의 모듈 중간 디렉터리에 자동으로 작성합니다.

테스트 코드의 프로덕션 유지 규칙

  • 성능에 악영향을 미치는 이유: 테스트 액세스만을 위해 맞춤 -keep 규칙을 추가하면 해당 코드가 난독화되지 않은 상태로 유지되고 프로덕션 빌드에 유지됩니다. trace_references_from (또는 trace_references: true가 있는 allow_traced_references)을 구성하거나 @VisibleForTesting로 메서드에 주석을 달면 수동 -keep 규칙을 유지하지 않고 테스트에서 참조하는 항목만 유지하지만 이러한 기호는 여전히 프로덕션 바이너리에 포함됩니다.
  • 권장 수정사항: trace_references_from(또는 allow_traced_references 및 trace_references: true)를 사용 설정하여 맞춤 테스트 -keep 규칙을 제거하거나, static_libs를 사용하여 자체 계측 테스트 APK에 연결된 .impl 라이브러리로 앱을 구성하여 테스트 전용 진입점을 프로덕션 APK에서 완전히 제외합니다(2단계: 테스트 기반 유지 규칙 이전 참고).

keep.xml의 포괄적 리소스 와일드 카드

<!-- Don't do this in res/raw/keep.xml: -->
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@raw/*,@drawable/*,@string/*" />
  • 성능에 미치는 영향: 포괄적인 와일드 카드는 모듈 및 종속 항목 전체에서 일치하는 유형의 모든 리소스를 유지하므로 리소스 축소 (shrink_resources: true)를 무력화하고 APK 및 매핑된 resources.arsc 테이블을 확장합니다.
  • 권장 해결 방법:
    • keep.xml보다 정적 리소스 참조 선호: Resources.getIdentifier 메서드를 사용하여 번호가 지정된 실험 문자열이나 테마가 지정된 드로어블과 같은 제한된 리소스 집합을 동적으로 조회하지 마세요. 대신 컴파일 시간 switch 문을 사용하거나 정적 R.id, R.string 또는 R.drawable 상수를 매핑합니다. 정적 참조를 사용하면 R8 및 AAPT2가 정확한 라이브 리소스를 추적하고 런타임 문자열 조회 오버헤드를 제거하며 keep.xml의 필요성을 완전히 없앨 수 있습니다.
    • 특정 리소스 이름 나열: 서드 파티 라이선스 리더와 같이 동적 조회가 필요한 경우 tools:keep에 정확한 리소스 식별자를 나열합니다 (예: tools:keep="@raw/third_party_licenses").
    • 중복 keep.xml 파일 삭제: 나열된 리소스가 이미 코드 (R.raw.foo) 또는 XML (@raw/foo)에서 정적으로 참조되는 경우 축소기는 리소스를 자동으로 유지하므로 res/raw/keep.xml를 삭제할 수 있습니다.

규칙 변경사항 확인 및 감사

proguard.flags 또는 keep.xml에서 유지 규칙을 삭제하거나 좁힐 때마다 모듈이 깔끔하게 빌드되고 단위 및 계측 테스트를 통과하며 필요한 모든 진입점을 유지하는지 확인합니다.

빌드 및 테스트 모듈

  1. 모듈을 완전히 빌드하여 R8 및 AAPT2가 누락된 참조 경고 없이 완료되는지 확인합니다.

    m <MODULE_NAME>
    
  2. atest를 사용하여 모듈의 단위 테스트 및 계측 테스트를 실행합니다.

    atest <TEST_MODULE_NAME>
    
  3. 시스템 앱, 권한이 있는 서비스 또는 하드웨어 종속 모듈의 경우 타겟 실제 기기 또는 기기 테스트 실험실에서 계측 및 UI 테스트를 실행하여 런타임 리플렉션 경로, IPC 바인딩, 리소스 확장을 실행합니다.

DEX 및 리소스 차이 검사

규칙 변경 전후에 컴파일된 APK 또는 JAR을 비교하여 R8이 예상 진입점을 제거하지 않고 데드 코드와 리소스를 삭제하는지 확인합니다.

  • apkanalyzer 또는 dexdump를 사용하여 출력 APK 또는 DEX 파일에서 유지된 클래스, 메서드, 필드를 검사합니다.

    apkanalyzer dex packages $OUT/system/priv-app/<APP_NAME>/<APP_NAME>.apk
    
  • keep.xml를 수정하거나 shrink_resources를 사용 설정할 때 aapt2 dump resources를 사용하여 빌드가 사용되지 않는 리소스를 삭제하고 필요한 리소스를 유지하는지 확인합니다.

    aapt2 dump resources $OUT/system/priv-app/<APP_NAME>/<APP_NAME>.apk
    
  • 전체 플랫폼 빌드 없이 오프라인에서 플래그 변경사항을 확인하기 위해 모듈의 R8 이전 classes.jar에서 prebuilts/r8/r8.jar를 직접 실행하는 경우 모듈의 --lib 클래스 경로 JAR (android.jar, framework.jar, 공유 libs)과 AndroidManifest.xml 및 레이아웃 XML에서 AAPT2가 생성한 proguard.options 규칙을 모두 전달합니다. 이러한 입력이 없으면 R8은 프레임워크 상위 유형 (Activity, Service, Phone, Handler)을 확인할 수 없고 계층 구조 메서드를 과소 유지합니다.

R8을 사용한 유지 범위 및 규칙 포함 분석

오픈소스 R8 컴파일러에는 컴파일 중에 모든 유지 규칙의 정확한 영향을 측정하는 유지 범위 분석기(Android 스튜디오 및 Gradle에서 R8 구성 분석기로도 표시됨)가 포함되어 있습니다. Soong은 이 분석기를 Android 플랫폼 빌드 (build/soong/java/dex.go에서 구성됨)에 직접 통합합니다.

플랫폼 모듈 또는 전체 빌드에서 유지 규칙을 분석하려면 다음 단계를 따르세요.

  1. R8_DUMP_KEEP_RADIUS=true 환경 변수를 사용하여 빌드를 실행합니다.

    R8_DUMP_KEEP_RADIUS=true m <MODULE_NAME>
    

    R8_DUMP_KEEP_RADIUS=true가 설정되면 Soong은 R8에 out/soong/.intermediates/에서 컴파일된 각 모듈의 중간 r8keepradius.pb 파일에 유지 규칙 측정항목을 기록하도록 지시합니다. 각 보관 규칙 및 keepanno 주석에 대해 R8은 다음을 기록합니다.

    • 즉시 유지 범위: 규칙에 의해 유지되는 정확한 클래스, 필드, 메서드와 축소, 최적화 또는 난독화에 대해 적용되는 특정 제약 조건입니다.
    • 규칙 포함: 이미 동일한 항목을 보관하는 다른 보관 규칙 또는 주석입니다. 커스텀 규칙이 AAPT2 규칙 또는 전역 기준 규칙에 완전히 포함된 경우 안전하게 삭제할 수 있습니다.
    • 패키지 전체 및 전역 규칙: 광범위한 패키지 전체 와일드 카드를 사용하거나 전역 구성 지시어를 적용하는 규칙입니다.
  2. KeepRadiusHtmlReportGenerator (prebuilts/r8/r8.jar에 번들로 포함됨)를 사용하여 r8keepradius.pb 출력을 대화형 HTML 보고서로 변환합니다.

    # Generate an HTML report for a single module
    INTERMEDIATES=out/soong/.intermediates/packages/apps/<APP_NAME>/<APP_NAME>
    java -cp prebuilts/r8/r8.jar \
        com.android.tools.r8.keepradius.KeepRadiusHtmlReportGenerator \
        $INTERMEDIATES/android_common/r8keepradius.pb \
        keep_radius_report.html
    
    # Scan all built modules under out/soong/.intermediates and generate
    # per-module HTML reports plus an aggregate keepradius.html summary
    java -cp prebuilts/r8/r8.jar \
        com.android.tools.r8.keepradius.KeepRadiusHtmlReportGenerator \
        out/soong/.intermediates \
        out/keep_radius_reports
    

    디렉터리가 제공되면 KeepRadiusHtmlReportGenerator는 모든 *keepradius*.pb 파일을 탐색하고 각 모듈의 HTML 보고서를 생성하며 빌드 전반에서 라이브 항목 수, 유지된 항목 수, 가장 높은 반경의 유지 규칙을 요약하는 out/keep_radius_reports/keepradius.html를 만듭니다. 생성된 보고서에서 축소, 최적화, 난독화 점수를 해석하는 방법에 관한 자세한 내용은 R8 구성 분석기 사용을 참고하세요.

Soong은 build/soong/java/dex.go에서 추가 R8 진단 및 내부 루프 환경 변수도 지원합니다.

  • R8_DUMP_INPUT=true: 독립형 R8 재생성을 위해 모든 입력 JAR, 라이브러리 JAR, 병합된 ProGuard 구성이 포함된 모듈의 중간 디렉터리에 r8inputs.zip를 작성합니다.
  • R8_DUMP_PERFETTO_TRACE=true: Perfetto에서 R8 컴파일 패스를 검사하기 위해 모듈의 중간 디렉터리에 r8trace.ptrace를 작성합니다.
  • SOONG_SKIP_TRACE_REFERENCES=true: 테스트 코드를 반복할 때 로컬 내부 루프 빌드 속도를 높이기 위해 trace_references_from 또는 trace_references에서 구성한 tracereferences 규칙 실행을 건너뜁니다.

라이브러리 소비자 규칙 검증

java_library 또는 android_library가 유지 규칙을 다운스트림 소비자 (export_proguard_flags_files: true)로 내보낼 때 이러한 규칙에는 소비 앱의 최적화를 변경하거나 사용 중지하는 전역 플래그가 포함되어서는 안 됩니다.

R8은 Android 플랫폼 빌드에서 process-keep-rules 호스트 도구(prebuilts/r8/Android.bp에 정의됨)로 노출되는 오픈소스 유지 규칙 분석기 (ProcessKeepRules)를 제공합니다.

# Build the R8 keep rules validator host binary
m process-keep-rules

# Validate one or more ProGuard configuration files
out/host/linux-x86/bin/process-keep-rules <PROGUARD_FLAGS_PATH>

process-keep-rules 도구는 각 구성 파일을 파싱하고 라이브러리 소비자 규칙에서 허용되지 않는 지시문을 발견하면 파일 및 줄 진단과 함께 실패합니다. 허용되지 않는 지시문에는 전역 최적화, 축소 또는 난독화 사용 중지 (예: -dontoptimize 또는 -dontshrink), 패키지 재패키징 및 액세스 수정 플래그, 진단 또는 매핑 플래그, 앱 수준 -keepattributes가 포함됩니다.

pgaudit.py로 소스 트리 감사

R8의 빌드 시간 분석기와 함께 빌드되지 않은 소스 디렉터리를 감사하기 위해 Android 플랫폼 트리에는 build/make/core/proguard/tools/ 아래에 pgaudit.py 스크립트가 포함되어 있습니다. 이 정적 분석 도구는 저장소 전체에서 Android.bp, proguard.flags, keep.xml 파일을 스캔하여 AAPT2 또는 전역 플랫폼 기준선, 광범위한 와일드 카드, -dontoptimize 재정의, 테스트 전용 유지 규칙에서 이미 다루는 규칙에 플래그를 지정합니다.

# Audit a single package directory
./build/make/core/proguard/tools/pgaudit.py packages/apps/Provision/

# Scan a subsystem tree and write a structured JSON report
./build/make/core/proguard/tools/pgaudit.py packages/ --json=audit_report.json

플랫폼 및 OEM팀은 pgaudit.py를 결합하여 빠른 소스 트리 분류를 수행하고, process-keep-rules를 결합하여 내보낸 라이브러리 규칙을 검증하고, R8_DUMP_KEEP_RADIUS=true를 KeepRadiusHtmlReportGenerator와 결합하여 빌드의 모든 규칙에 대한 정확한 클래스, 필드, 메서드 보존 반경을 측정할 수 있습니다.