Skip to content

Android DCE R8

kirich1409 edited this page May 18, 2026 · 1 revision

Android — R8 Dead-Code Elimination

How it works

The Gradle plugin generates per-function ProGuard / R8 -assumevalues rules for the generated extension functions of every local boolean flag with default = false. These rules instruct R8 to treat the flag accessor as returning a constant false at shrink time.

With the rule in place, R8:

  1. Constant-folds the accessor call to false.
  2. Determines that any branch gated on isXxxEnabled() is unreachable.
  3. Removes the unreachable classes and methods from the output APK or JAR.

Remote flags are excluded — their values are dynamic (fetched at runtime) and cannot be treated as constants.

Task and output

The rule generation task runs automatically when you build a release variant. To run it manually:

./gradlew :app:generateFeaturedProguardRules

Output path: app/build/featured/proguard-featured.pro

The plugin wires the generated file into R8's input automatically — no proguardFiles entry in build.gradle.kts is needed.

Checking the generated rules

For a flag new_checkout with default = false, the generated rule looks like:

-assumevalues class dev.example.app.GeneratedLocalFlagsKt {
    boolean isNewCheckoutEnabled(dev.androidbroadcast.featured.ConfigValues) return false;
}

R8 reads this and treats every call to configValues.isNewCheckoutEnabled() as returning false, making any if (configValues.isNewCheckoutEnabled()) { … } branch dead.

No extra setup needed

The Gradle plugin handles wiring. You do not need to:

  • Add proguardFiles entries manually.
  • Configure any R8 keep rules for the generated code.
  • Run a separate task before the release build.

Verifying DCE

The featured-shrinker-tests module provides automated, deterministic verification that the exact rule format produced by the plugin is sufficient for R8 to perform DCE.

Why it matters: a rule that is syntactically correct but semantically wrong would silently fail to eliminate dead code. The tests catch that class of regression before it reaches consumers.

How the tests work:

  1. Bytecode generation (ASM)SyntheticBytecodeFactory builds .class files in memory that mirror the structure the plugin generates: a ConfigValues holder, extension functions that read from it, branch-target classes (IfBranchCode, ElseBranchCode, PositiveCountCode), and a caller entry point.

  2. Rules generationProguardRulesWriter writes .pro files in the exact format ProguardRulesGenerator produces, optionally including the -assumevalues block.

  3. R8 invocationR8TestHarness.runR8() calls R8 programmatically via R8Command.builder(), producing an output JAR with DCE applied.

After each run, JarAssertions inspects the output JAR and asserts which classes are present or absent.

Test scenarios:

Module Test Rule Expected result
Boolean guarded class is eliminated when flag is assumed false -assumevalues … return false IfBranchCode absent; ElseBranchCode present
Boolean guarded class survives when no assumevalues rule No rule Both classes present
Int guarded class eliminated when int flag assumed zero -assumevalues … return 0 PositiveCountCode absent; IntCaller present
Int guarded class survives when no assumevalues rule No rule Both classes present

Run the tests:

./gradlew :featured-shrinker-tests:test

AGP 9.x note

On AGP 9.x, variant.proguardFiles as a ListProperty<RegularFile> does not propagate its provider dependency to the underlying R8 task on the releases verified during the 1.0.0-Beta cycle. The plugin retains a tasks.configureEach { … } fallback inside AndroidProguardWiring.kt that explicitly establishes the task dependency. The fallback is CC-safe and will be removed when the upstream AGP gap is fixed. See Configuration Cache for full details.

Clone this wiki locally