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

Android 플랫폼 빌드 시스템 (Soong)은 Android 앱 (android_app), 테스트(android_test), compile_dex: true (예: services.jar)이 있는 설치 가능한 Java 라이브러리 등 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입니다 (명시적으로 설정하지 않는 한 -dontshrink를 전달하는 독립형 java_library 및 테스트 모듈의 경우 false).
optimize 메서드 인라이닝, 클래스 병합, 상수 전파, 데드 브랜치 삭제와 같은 바이트 코드 최적화를 제어합니다. android_app 타겟의 경우 기본값은 true입니다 (RELEASE_R8_OPTIMIZE_BY_DEFAULT 출시 빌드 플래그로 제어됨).
obfuscate 식별자 축소 및 이름 바꾸기를 제어합니다. 기본적으로 이전 버전과의 호환성을 위해 false로 설정되지만 가능한 경우 DEX 크기를 줄이고 더 심층적인 최적화를 지원하도록 true로 설정합니다 (가능한 경우 난독화 사용 설정 참고).
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 바이트 코드가 이 타겟을 참조하는 Java 라이브러리 동반 타겟을 나열합니다. 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 아티팩트로 번들링하고, DEX 헤더에 매핑 해시 (--map-id-template)를 삽입하고, retrace가 스택 트레이스를 자동으로 심볼화할 수 있도록 클래스 SourceFile 속성 (--source-file-template)을 다시 작성합니다.

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

  • bootclasspath 또는 system_server classpath의 공유 라이브러리: 런타임에 다른 모듈 (예: framework.jar, services.jar, <uses-library> 타겟)에 의해 동적으로 연결된 외부 API 노출 모듈 (java_library 또는 java_sdk_library)은 obfuscate: false을 유지하거나 호출자가 스텁에 대해 컴파일될 때 런타임에 클래스 및 멤버 이름을 확인할 수 있도록 @KeepForApi 또는 -keep 규칙 (bootclasspath 타겟의 경우 protect_api_surface: true와 함께)을 사용하여 API 노출 영역을 명시적으로 보존해야 합니다.

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

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

맞춤 플래그 0개 원칙 준수

이상적인 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 파일에서 이동합니다.

  • 테스트에 구현 라이브러리 연결 (static_libs): android_test가 앱의 패키지 비공개 또는 내부 구현 세부정보를 실행할 때 권장되는 플랫폼 아키텍처는 앱 소스 파일을 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가 테스트를 중단하지 않고 자유롭게 obfuscate: true를 사용 설정하며, 프로덕션 APK에서 테스트 전용 진입점을 제공하지 않고, 테스트 코드가 변경될 때 MyApp를 다시 빌드하지 않아도 됩니다.

  • @VisibleForTesting로 테스트 후크 주석 달기: 외부 테스트 APK가 android_app 타겟에서 소수의 내부 메서드나 생성자를 호출하고 .impl 라이브러리로 재구성하는 것이 비실용적인 경우 해당 선언에 @VisibleForTesting로 주석을 답니다. 전역 build/make/core/proguard.flags 기준은 맞춤 .flags 파일 없이 android.**, com.android.**, com.google.android.** 패키지에 @VisibleForTesting 항목을 자동으로 유지합니다. (이러한 네임스페이스 외부의 공급업체 모듈의 경우 @UsedByReflection 또는 내보낸 라이브러리 규칙을 사용하세요.)

  • 플랫폼 라이브러리 경계에서 trace_references_from 사용: 테스트가 타겟 구현을 정적으로 연결할 수 없는 경우 (예: 시스템 서버 서비스 또는 framework-connectivity와 같은 플랫폼 JAR을 실행하는 테스트) 테스트 소스를 포함하는 Java 라이브러리 동반자 모듈을 가리키는 타겟 모듈에서 trace_references_from을 구성합니다. R8은 동반자 바이트 코드에서 참조하는 모든 클래스와 멤버를 추적하고 유지합니다.

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

4단계: 소스 코드에서 선언에 주석 추가

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

  • @UsesReflection: 리플렉션을 실행하는 코드를 제어하는 경우 이 주석을 사용하는 것이 좋습니다. 리플렉션 호출 사이트에 배치하여 동적으로 액세스되는 타겟 클래스, 메서드 또는 필드를 선언합니다. @UsesReflection는 주석이 달린 호출 사이트 자체가 도달 가능하다는 사전 조건을 자동으로 인코딩하므로 호출 사이트가 사용되지 않으면 R8은 호출자와 리플렉션 타겟을 모두 삭제합니다.
  • @UsedByReflection: Bundle 또는 Settings 키에서 Class.forName로 로드된 클래스나 동적 클래스 로더에서 로드된 플러그인 클래스 등 외부 코드나 라이브러리에 의해 리플렉션 방식으로 인스턴스화되거나 호출되는 클래스, 메서드, 필드 또는 생성자에 배치합니다. R8이 정확한 리플렉션 계약만 유지하고 클래스의 사용되지 않는 멤버를 계속 최적화하거나 삭제할 수 있도록 kind (예: KeepItemKind.CLASS_AND_METHODS), preconditions (@KeepCondition), 매개변수 제약 조건을 지정합니다. 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 출력에서 주석을 삭제하여 런타임 바이트 코드 오버헤드를 0으로 추가합니다.

일반적인 문제 방지

다음 섹션에서는 플랫폼 코드에서 자주 발생하는 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 속성에서 참조하는 메서드의 타겟 유지 규칙을 생성합니다.

광범위한 뷰 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 구현)로 바꿔 리플렉션을 완전히 방지하세요. 리플렉티브 속성 액세스를 피할 수 없는 경우 특정 게터 또는 세터에 @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 규칙을 추가하면 해당 코드가 난독화되지 않은 상태로 유지되고 프로덕션 빌드에 보관됩니다. @VisibleForTesting로 메서드에 주석을 달거나 trace_references_from를 구성하면 수동 -keep 규칙을 유지하지 않아도 되지만 이러한 기호는 여전히 프로덕션 바이너리에 포함됩니다.
  • 권장 수정사항: 테스트 전용 진입점을 프로덕션 APK에서 완전히 제외하려면 2단계: 테스트 기반 유지 규칙 이전에 설명된 대로 애플리케이션을 .impl 라이브러리로 구조화하고 static_libs를 사용하여 자체 계측 테스트 APK에 연결하세요.

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로 유지 반경 및 규칙 포함 분석

오픈소스 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를 씁니다.

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

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

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

# 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를 결합하여 빌드의 모든 규칙의 정확한 클래스, 필드, 메서드 보존 반경을 측정할 수 있습니다.