Optimize platform code and resources with R8

The Android platform build system (Soong) runs the R8 compiler on targets that compile DEX bytecode, including Android apps (android_app), tests (android_test), and installable Java libraries with compile_dex: true (such as services.jar), to shrink, optimize, and strip unused code and resources. On static libraries (android_library and static java_library), Soong doesn't run R8 directly, but uses the optimize property block to attach and propagate consumer keep rules (export_proguard_flags_files: true) to downstream DEX targets that statically link them.

For platform engineers building system images or vendor packages, eliminating overly broad keep rules offers direct system health benefits:

  • Smaller system partition footprint: R8 removes dead classes, methods, and resources before packaging APKs and JARs onto /system, /system_ext, /product, and /vendor.
  • Smaller compiled artifacts: Fewer DEX methods mean smaller .odex and .vdex files generated by dex2oat during build-time or on-device compilation.
  • Lower runtime memory pressure: Because Android maps .odex, .vdex, and APK resource tables into process memory using demand-paged mmap calls, smaller binaries reduce major page faults during app startup and lower resident code and resource memory across processes. For more information about how compiled app code affects device memory, see App code is memory.
  • More effective whole-program optimization: Narrow keep constraints let R8 inline methods, devirtualize interface and virtual calls, prune unused fields, and propagate constants across classes.

Configure R8 optimization in Soong

In Android.bp files, configure R8 optimization using the optimize property block on android_app and installable java_library targets (or on android_library and static java_library modules to export consumer keep rules):

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

The following table summarizes the most common optimize properties in Soong (defined in build/soong/java/dex.go):

Property Description
enabled Controls whether R8 runs on the target. Defaults to true for all android_app targets.
shrink Controls tree shaking to remove unreachable classes, fields, and methods. Defaults to true for android_app targets (false for standalone java_library and test modules, which pass -dontshrink unless explicitly set).
optimize Controls bytecode optimizations such as method inlining, class merging, constant propagation, and dead branch removal. Defaults to true for android_app targets (controlled by the RELEASE_R8_OPTIMIZE_BY_DEFAULT release build flag).
obfuscate Controls identifier minification and renaming. Defaults to false for historical compatibility, but set it to true wherever possible to reduce DEX size and unlock deeper optimizations (see Enable obfuscation wherever possible).
shrink_resources Removes unused resources (res/ entries) from the packaged APK after code shrinking. Defaults to false.
optimized_shrink_resources Runs the integrated R8 code and resource shrinker pipeline so R8 traces code and resource references jointly in a single pass. When shrink_resources: true is set, defaults to RELEASE_USE_OPTIMIZED_RESOURCE_SHRINKING_BY_DEFAULT (true in standard platform builds).
proguard_flags_files Lists module-specific .flags or .pro files containing custom keep rules. Avoid adding custom files when standard annotations or defaults suffice.
export_proguard_flags_files Propagates this library's proguard_flags_files to downstream modules that depend on it statically.
trace_references_from Lists direct DEX-compiling dependent targets (connected through instrumentation_for or libs) whose bytecode references into this target R8 automatically traces and retains (also propagated across override_android_app and override_android_test variants, and supported for non-dependent companion Java libraries when paired with an .impl split).
allow_traced_references Allows direct DEX-compiling dependent targets (connected through instrumentation_for or libs, such as cross-repository or vendor tests that cannot be listed in trace_references_from) to opt in to tracing their bytecode references into this target by setting trace_references: true.
trace_references When set to true on a direct DEX-compiling dependent of a target with allow_traced_references: true, runs R8 tracereferences on this module and passes the generated keep rules to the target module.
protect_api_surface Passes --protect-api-surface to R8. When set on a java_sdk_library, also automatically generates and attaches Metalava API keep rules across all enabled API scopes (public, system, module-lib, system-server, and test) to the implementation library.

Static libraries (android_library, java_library, and AAR imports) can also set required_downstream_optimization: { enabled: true, optimize: true } in Android.bp to require any downstream DEX-compiling target that statically links them (directly or transitively) to enable R8 shrinking and bytecode optimization. Soong enables this automatically on androidx.compose.runtime_runtime so targets linking Jetpack Compose are optimized with R8.

Enable obfuscation wherever possible

In Soong, obfuscate defaults to false for historical compatibility, but you should explicitly set obfuscate: true on android_app and standalone DEX targets wherever possible:

  • Smaller DEX and .vdex footprint: Renaming packages, classes, fields, and methods to short identifiers (a, b) shrinks the DEX string pool (string_ids and string_data_item) and type descriptors. Because Android maps .vdex and .odex files into process memory, smaller symbol tables directly reduce both system partition size and runtime memory footprint.
  • Deeper whole-program optimizations: Letting R8 rename identifiers unlocks class merging, package flattening, and synthetic class deduplication across packages that R8 must otherwise skip to avoid name collisions.
  • Full stack trace symbolication: Enabling obfuscation in Soong doesn't reduce debuggability. For every R8-compiled target, Soong emits a proguard_dictionary mapping file in the module's intermediate directory, bundles all module dictionaries into the build's proguard-dict.zip artifact, embeds a mapping hash (--map-id-template) into the DEX header, and rewrites class SourceFile attributes (--source-file-template) so retrace can symbolicate stack traces automatically.

Keep obfuscate: false (or explicitly preserve public API names) only for:

  • Shared libraries on the bootclasspath or system_server classpath: Modules (java_library or java_sdk_library) that expose an external API surface linked dynamically by other modules at run time (such as framework.jar, services.jar, or <uses-library> targets) must either keep obfuscate: false or explicitly preserve their API surface using protect_api_surface: true, @KeepForApi, or -keep rules so callers compiled against their stubs can resolve class and member names at run time. On java_sdk_library modules, setting protect_api_surface: true automatically generates and attaches Metalava ProGuard keep rules across all enabled API scopes (public, system, module-lib, system-server, and test) to the implementation library. When generalized R8 optimization is enabled on non-updatable bootclasspath targets (framework-minus-apex, ext, and framework-location), they also include framework-hiddenapi-proguard-rules (generated by hiddenapi_to_proguard) to retain members listed in active boot/hiddenapi/*.txt files (omitting hiddenapi-max-target-o.txt).

Before enabling obfuscate: true on an application, verify the following migration prerequisites:

  • External test APK dependencies: If an external android_test APK sets instrumentation_for: "MyApp" and directly invokes internal classes or methods of MyApp, renaming those symbols causes NoSuchMethodError or NoClassDefFoundError at test run time. Configure trace_references_from (or allow_traced_references with trace_references: true), structure the test as a self-instrumenting APK linking MyApp.impl statically, or annotate test hooks with @VisibleForTesting (see Step 2: Migrate test-driven keep rules) so internal symbols needed by tests are preserved while obfuscating the rest of the app.
  • String-based reflection and JNI: If a module looks up classes, methods, or fields by literal string names (Class.forName, getDeclaredMethod, or JNI FindClass and GetMethodID) without keep rules or annotations, obfuscation renames those targets and breaks runtime lookups. (Note that unannotated reflection also fails under shrink: true and optimize: true.) Annotate those entry points with Guide to Keep Annotations (@UsesReflection, @UsedByReflection, or @UsedByNative) so R8 preserves their names and obfuscates the rest of the module.

Follow the zero custom flags principle

The ideal Android platform module has no custom proguard.flags or keep.xml files. In the Android platform build, the vast majority of custom keep rules are redundant:

  • Platform baselines and AAPT2: Most entry points are already retained automatically by global platform baselines (such as @Keep, JNI native methods, and @VisibleForTesting) or generated by AAPT2 from AndroidManifest.xml and layout resources (see Understand default platform keep rules).
  • Targeted alternatives: Where entry points must be preserved, prefer declaration-site annotations (keepanno, @VisibleForTesting), exported library rules (export_proguard_flags_files: true), or static test linking over detached .flags files (see Audit and migrate existing keep rules).

Before adding or retaining a custom proguard.flags or keep.xml file, verify whether the rule is already handled by default baselines or can be migrated to code annotations.

Understand default platform keep rules

Soong automatically passes the following global baseline rules to every R8 invocation on DEX-compiling targets (android_app, android_test, and installable java_library modules), configured in build/soong/java/dex.go:

  • build/make/core/proguard.flags: Preserves classes and members annotated with @com.android.internal.annotations.VisibleForTesting across all packages, and with @VisibleForTesting (androidx.annotation.VisibleForTesting or com.google.common.annotations.VisibleForTesting) within android.**, com.android.**, and com.google.android.** packages. Also preserves @TestApi, @Keep (androidx.annotation, android.support.annotation, com.android.internal.annotations), @KeepForWeakReference, @WeaklyReferencedCallback, and @dalvik.annotation.optimization.**.
  • build/make/core/proguard_basic_keeps.flags: Preserves SourceFile attributes for stack traces, runtime visibility annotations (RuntimeVisible*Annotations), Exceptions and AnnotationDefault attributes, native methods, Serializable members, @JavascriptInterface methods, Throwable(String) constructors, Parcelable$CREATOR fields, and protobuf MessageLite fields.
  • build/make/core/proguard/kotlin.flags: Silences harmless warnings for specific Kotlin meta-annotations (kotlin.Metadata and kotlin.annotation.{AnnotationRetention,AnnotationTarget,Retention,Target}) and strips Kotlin DebugMetadata annotations in release builds.
  • build/make/core/proguard/checknotnull.flags: Replaces common null check helper calls (com.google.common.base.Preconditions.checkNotNull and dagger.internal.Preconditions.checkNotNull*) with concise bytecode null checks. This file intentionally omits Objects.requireNonNull to preserve explicit exception messages across framework API boundaries.
  • build/make/core/proguard/enumvalues.flags: Retains the values and valueOf methods on enum types unless disabled by the build configuration.
  • AAPT2 auto-generated rules: AAPT2 inspects merged AndroidManifest.xml, layout XML, and preference XML files to generate exact keep rules for every registered Activity, Service, BroadcastReceiver, ContentProvider, BackupAgent, Application, custom View subclass, Preference subclass, and XML android:onClick method.

Audit and migrate existing keep rules

When auditing existing proguard.flags or keep.xml files in a platform repository, evaluate each rule in order against the following four-step hierarchy:

Step 1: Delete redundant or obsolete rules

Delete rules that are already covered by global baselines, AAPT2 manifest and layout rules, or R8 defaults, as well as rules referencing classes or packages that no longer exist.

If all rules in a .flags file are redundant, delete the file and remove proguard_flags_files from Android.bp. If the remaining optimize block only restates default settings, remove the redundant block and format the build file with bpfmt -w Android.bp. When cleaning up a package, check whether companion modules in subdirectories (such as Kotlin target variants) reference the same flags file and update them together.

Step 2: Migrate test-driven keep rules

Move test-driven keep rules out of custom production .flags files using one of the following patterns:

  • Trace references from test or companion targets (trace_references_from or allow_traced_references): When both the target and a direct dependent module compile their own DEX (android_app, android_test, or a dexed java_library) and are connected through instrumentation_for or libs, Soong can run R8 tracereferences on the dependent module and feed the generated keep rules into the target's R8 compilation without creating a dependency cycle or requiring a manual .impl library split:

    • Same-repository tests (trace_references_from): List the direct instrumentation_for or libs dependents on the target module (this also propagates automatically across override_android_app and override_android_test variants, and continues to support non-dependent companion Java libraries when paired with an .impl split):

      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"],
      }
      
    • Cross-repository or vendor tests (allow_traced_references and trace_references): When the target module can't list the dependent directly in trace_references_from (such as tests in another repository or vendor tree), set allow_traced_references: true on the target module and opt in from each dependent with 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,
          },
      }
      
    • String-based reflection in tests: tracereferences inspects bytecode constant-pool references and can't detect string-based reflection in tests (Class.forName, getDeclaredField, getDeclaredMethod, or reflective field-injection helpers). Annotate reflectively accessed production fields and methods with @VisibleForTesting (or @UsedByReflection).

  • Link the implementation library into tests (static_libs): If you want zero test-only entry points retained in the production APK and don't want test changes to retrigger R8 on the production target, place the app source files in an android_library (MyApp.impl) and statically link MyApp.impl into both MyApp and 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"],
    }
    

    This 3-module pattern lets MyAppTests run as a self-instrumenting test with full access to internal classes while allowing MyApp to shrink and obfuscate purely against production entry points.

  • Annotate test hooks with @VisibleForTesting: If an external test APK calls only a few internal methods or constructors on an android_app target, you can annotate those declarations with @VisibleForTesting. The global build/make/core/proguard.flags baseline retains @VisibleForTesting items in android.**, com.android.**, and com.google.android.** packages automatically without custom .flags files. (For vendor modules outside these namespaces, use trace_references_from, @UsedByReflection, or exported library rules.) When hand-annotating with @VisibleForTesting instead of using trace_references_from or trace_references, also annotate classes passed to Mockito mock(...) or spy(...) (not just the stubbed methods), because on-device mockito-target (dexmaker-mockito) generates runtime subclasses and R8 can mark un-subclassed classes final (MockitoException: Can't mock/spy class ... - final class).

  • Avoid or repackage overlapping static libraries (jarjar_rules): When both an android_app and its instrumentation_for test APK statically link the same library (directly, such as guava, or transitively), tracereferences sees those classes defined in the test and emits no keep rules for the app, and at run time the app's parent classloader shadows the test's copy with its R8-shrunk classes, causing IllegalAccessError or NoSuchMethodError. If the test doesn't need its own bundled copy, remove the duplicate from the test's static_libs (or link it through libs) to avoid the collision; otherwise, repackage the app's overlapping static library packages under a module-specific prefix using jarjar_rules.

Step 3: Export rules from the owning library

If a shared java_library or android_library requires keep rules for its own internal reflection or JNI callbacks, define the rules on the library target and set 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,
    },
}

All downstream android_app and library targets that statically link my-shared-library inherit those rules automatically, so downstream apps don't need to duplicate them. When multiple modules across different directories must share a rule set that isn't tied to a single code library, publish the .flags file through an explicit filegroup in Android.bp so modules can reference :my-shared-flags cleanly across package boundaries. For general guidance on authoring library consumer keep rules without restricting downstream app optimization, see Optimization for library authors.

Step 4: Annotate declarations in source code

When a class, method, constructor, or field is accessed through reflection or JNI and isn't covered by AAPT2 or global baselines, replace detached -keep rules in .flags files with source annotations from the Guide to Keep Annotations (see the keepanno Javadoc reference):

  • @UsesReflection: Prefer this annotation when you control the code performing the reflection. Place it at the reflection call site to declare which target classes, methods, or fields are accessed dynamically. Because @UsesReflection automatically encodes a precondition that the annotated call site itself is reachable, R8 prunes both the caller and the reflective target if the call site is unused.
  • @UsedByReflection: Place on classes, methods, fields, or constructors that are instantiated or invoked reflectively by external code or libraries, such as classes loaded with Class.forName from Bundle or Settings keys, or plugin classes loaded across dynamic classloaders. Specify kind (such as KeepItemKind.CLASS_AND_METHODS), preconditions (@KeepCondition), and parameter constraints so R8 keeps only the exact reflective contract and can still optimize or prune unused members of the class. Don't add @UsedByReflection to manifest-registered components like JobService or BroadcastReceiver, because AAPT2 already keeps them.
  • @UsedByNative: Place on methods or fields accessed from C or C++ JNI code using GetMethodID, GetStaticMethodID, or GetFieldID.
  • @KeepForApi: Place on library API classes or members that must remain intact when a library itself is shrunk before distribution.
  • @Keep (androidx.annotation.Keep): Use as a fallback when keepanno isn't applicable. Applying @Keep to a class retains the class and all its members unconditionally. Applying @Keep to a method or field acts as an unconditional entry point that retains the member and its containing class even if the class is never instantiated, whereas keepanno expresses conditional reachability.

To use R8 keepanno annotations in a Soong module:

  1. Add "keepanno-annotations" to libs in Android.bp:

    libs: [
        "keepanno-annotations",
    ],
    
  2. Import com.android.tools.r8.keepanno.annotations.* and annotate the declaration or call site in Java or Kotlin source code:

    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 translates keepanno annotations directly into its internal keep rule model and strips the annotations from the final DEX output, adding zero runtime bytecode overhead.

Avoid common pitfalls

The following sections describe frequent proguard.flags and keep.xml pitfalls in platform code and how to fix them.

Broad component keep rules

# Don't do this:
-keep class * extends android.app.Activity
-keep class * extends android.app.Service
-keep class * extends android.content.BroadcastReceiver
  • Why it hurts performance: This rule is completely redundant with AAPT2. Because it uses a wildcard without checking whether the component is registered in the final merged AndroidManifest.xml, it forces R8 to retain every Activity, Service, or BroadcastReceiver subclass found anywhere on the classpath, including unused library components and disabled debug activities.
  • Recommended fix: Delete the rule. AAPT2 inspects the merged manifest and generates exact -keep rules for registered components.

Broad XML click handler keep rules

# Don't do this:
-keepclassmembers class * {
    public void *(android.view.View);
}
  • Why it hurts performance: Retaining every public void *(View) method across every class in the module prevents R8 from stripping or inlining any method that happens to accept a single View parameter.
  • Recommended fix: Delete the rule. AAPT2 scans layout XML files and generates targeted keep rules for methods referenced by android:onClick attributes.

Broad View getter and setter keep rules

# 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*();
}
  • Why it hurts performance: This rule forces R8 to retain every getter and setter across every View subclass in the app and all linked libraries (including AndroidX and Material libraries), blocking dead method removal and inlining across UI code.
  • Recommended fix: Delete the rule. AAPT2 already preserves constructors for custom View classes inflated from layout XML files. If code animates a view property using reflective string names such as ObjectAnimator.ofFloat(view, "translationZ", ...), replace the string name with a typed property reference (View.TRANSLATION_Z or a custom FloatProperty or IntProperty implementation) to avoid reflection entirely. If reflective property access can't be avoided, annotate the specific getter or setter with @UsedByReflection.

Blanket optimization or obfuscation disables

# Don't do this in proguard.flags:
-dontoptimize
-dontshrink
-dontobfuscate
  • Why it hurts performance: Placing -dontoptimize or -dontshrink inside a .flags file silently overrides the module's Android.bp settings and disables optimization passes across the entire target. Worse, if a library exports a .flags file containing -dontoptimize, it disables R8 optimization for every downstream android_app that links the library.
  • Recommended fix: Remove -dontoptimize, -dontshrink, and -dontobfuscate from .flags files. Control optimization behavior explicitly in Android.bp using the optimize block (shrink, optimize, obfuscate) on the leaf target.

Diagnostic flags in committed rules

# Don't do this in proguard.flags:
-verbose
-printmapping proguard.map
-printusage usage.txt
-printconfiguration config.txt
  • Why it hurts the build: Diagnostic flags flood build logs or attempt to write output files to local paths during sandboxed Soong builds.
  • Recommended fix: Delete these flags from committed .flags files. Soong automatically writes R8 mapping and usage outputs, such as proguard_dictionary and proguard_usage.zip, to the module's intermediate directory under out/soong/.intermediates/.

Production keep rules for test code

  • Why it hurts performance: Adding custom -keep rules solely for test access forces that code to remain un-obfuscated and retained in production builds. While configuring trace_references_from (or allow_traced_references with trace_references: true) or annotating methods with @VisibleForTesting avoids maintaining manual -keep rules and retains only what tests reference, those symbols still ship in the production binary.
  • Recommended fix: Eliminate custom test -keep rules by enabling trace_references_from (or allow_traced_references and trace_references: true), or structure the app into an .impl library linked into a self-instrumenting test APK using static_libs to keep test-only entry points completely out of the production APK, as described in Step 2: Migrate test-driven keep rules.

Blanket resource wildcards in keep.xml

<!-- Don't do this in res/raw/keep.xml: -->
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@raw/*,@drawable/*,@string/*" />
  • Why it hurts performance: Blanket wildcards retain every resource in the matching type across the module and its dependencies, defeating resource shrinking (shrink_resources: true) and inflating the APK and mapped resources.arsc table.
  • Recommended fix:
    • Prefer static resource references over keep.xml: Avoid using the Resources.getIdentifier method to look up a bounded set of resources dynamically, such as numbered experiment strings or themed drawables. Instead, use a compile-time switch statement or map over static R.id, R.string, or R.drawable constants. Static references let R8 and AAPT2 trace the exact live resources, eliminate runtime string lookup overhead, and remove the need for keep.xml altogether.
    • List specific resource names: If dynamic lookup is required, such as by a third-party license reader, list the exact resource identifiers in tools:keep (for example, tools:keep="@raw/third_party_licenses").
    • Delete redundant keep.xml files: If the listed resources are already referenced statically in code (R.raw.foo) or XML (@raw/foo), the shrinker retains them automatically, so you can delete res/raw/keep.xml.

Verify and audit rule changes

Whenever you remove or narrow keep rules in proguard.flags or keep.xml, verify that the module builds cleanly, passes unit and instrumentation tests, and retains all required entry points.

Build and test modules

  1. Build the module cleanly to verify that R8 and AAPT2 complete without missing reference warnings:

    m <MODULE_NAME>
    
  2. Run unit and instrumentation tests for the module using atest:

    atest <TEST_MODULE_NAME>
    
  3. For system apps, privileged services, or hardware-dependent modules, run instrumentation and UI tests on target physical devices or in your device test lab to exercise runtime reflection paths, IPC bindings, and resource inflation.

Inspect DEX and resource diffs

Compare the compiled APK or JAR before and after rule changes to confirm that R8 removes dead code and resources without stripping expected entry points:

  • Use apkanalyzer or dexdump to inspect retained classes, methods, and fields in the output APK or DEX files:

    apkanalyzer dex packages $OUT/system/priv-app/<APP_NAME>/<APP_NAME>.apk
    
  • When modifying keep.xml or enabling shrink_resources, use aapt2 dump resources to verify that the build strips unused resources and retains required ones:

    aapt2 dump resources $OUT/system/priv-app/<APP_NAME>/<APP_NAME>.apk
    
  • When running prebuilts/r8/r8.jar directly on a module's pre-R8 classes.jar to verify flag changes offline without a full platform build, pass both the module's --lib classpath JARs (android.jar, framework.jar, and shared libs) and AAPT2's generated proguard.options rules from AndroidManifest.xml and layout XMLs. Without those inputs, R8 can't resolve framework supertypes (Activity, Service, Phone, Handler) and under-retains hierarchy methods.

Analyze keep radius and rule subsumption with R8

The open-source R8 compiler includes a Keep Radius analyzer (also exposed in Android Studio and Gradle as the R8 Configuration Analyzer) that measures the exact impact of every keep rule during compilation. Soong integrates this analyzer directly into the Android platform build (configured in build/soong/java/dex.go).

To analyze keep rules for a platform module or across an entire build:

  1. Run the build with the R8_DUMP_KEEP_RADIUS=true environment variable:

    R8_DUMP_KEEP_RADIUS=true m <MODULE_NAME>
    

    When R8_DUMP_KEEP_RADIUS=true is set, Soong instructs R8 to record keep rule metrics in an intermediate r8keepradius.pb file for each compiled module under out/soong/.intermediates/. For each keep rule and keepanno annotation, R8 records:

    • Immediate keep radius: The exact classes, fields, and methods retained by the rule, along with the specific constraints it enforces against shrinking, optimization, or obfuscation.
    • Rule subsumption: Which other keep rules or annotations already retain the same items. If a custom rule is completely subsumed by an AAPT2 rule or a global baseline rule, you can safely delete it.
    • Package-wide and global rules: Rules that use broad package-wide wildcards or apply global configuration directives.
  2. Convert the r8keepradius.pb output into an interactive HTML report using KeepRadiusHtmlReportGenerator (bundled in prebuilts/r8/r8.jar):

    # 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
    

    When given a directory, KeepRadiusHtmlReportGenerator walks all *keepradius*.pb files, generates an HTML report for each module, and creates out/keep_radius_reports/keepradius.html summarizing live item counts, kept item counts, and the highest-radius keep rules across the build. For more information about interpreting shrinking, optimization, and obfuscation scores in the generated report, see Use R8 Configuration Analyzer.

Soong also supports additional R8 diagnostic and inner-loop environment variables in build/soong/java/dex.go:

  • R8_DUMP_INPUT=true: Writes r8inputs.zip to the module's intermediate directory containing all input JARs, library JARs, and merged ProGuard configurations for standalone R8 reproduction.
  • R8_DUMP_PERFETTO_TRACE=true: Writes r8trace.ptrace to the module's intermediate directory for inspecting R8 compilation passes in Perfetto.
  • SOONG_SKIP_TRACE_REFERENCES=true: Skips running tracereferences rules configured by trace_references_from or trace_references to speed up local inner-loop builds when iterating on test code.

Validate library consumer rules

When a java_library or android_library exports keep rules to downstream consumers (export_proguard_flags_files: true), those rules mustn't include global flags that alter or disable optimization for the consuming app.

R8 provides an open-source keep rule analyzer (ProcessKeepRules), exposed in the Android platform build as the process-keep-rules host tool (defined in 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>

The process-keep-rules tool parses each configuration file and fails with file and line diagnostics if it encounters directives that are disallowed in library consumer rules. Disallowed directives include global optimization, shrinking, or obfuscation disables (such as -dontoptimize or -dontshrink), package repackaging and access modification flags, diagnostic or mapping flags, and app-level -keepattributes.

Audit source trees with pgaudit.py

To audit unbuilt source directories alongside R8's build-time analyzers, the Android platform tree includes the pgaudit.py script under build/make/core/proguard/tools/. This static analysis tool scans Android.bp, proguard.flags, and keep.xml files across a repository to flag rules already covered by AAPT2 or global platform baselines, broad wildcards, -dontoptimize overrides, and test-only keep rules:

# 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

Platform and OEM teams can combine pgaudit.py for fast source-tree triage, process-keep-rules to validate exported library rules, and R8_DUMP_KEEP_RADIUS=true with KeepRadiusHtmlReportGenerator to measure the exact class, field, and method retention radius of every rule in a build.