App protection

ProGuard vs mobile app shielding

What ProGuard and R8 do for an Android build, what name shortening leaves exposed, and when an app needs hardening and runtime checks on top.

A shrinker such as ProGuard or R8 makes an Android app smaller and faster. It removes unused code, optimises what remains and renames classes, methods and fields to short names. That renaming makes decompiled output less readable, but it is a side effect of size reduction, not a security design. Strings, native libraries, resources and framework bundles stay as they were, and nothing checks the app while it runs. For apps that handle money, identity or valuable logic, hardening and runtime protection are the extra work. This article covers Android, with a note on iOS.

What a shrinker is for

From ProGuard to R8
  1. 2002

    ProGuard first released

    An open-source shrinker, optimiser and renamer for Java bytecode.

  2. 2010

    ProGuard ships with the Android SDK tools

    Release builds gain a standard way to shrink and rename Java code.

  3. 2019

    R8 becomes the default

    The Android Gradle plugin switches from ProGuard to R8, which reads the same keep rules.

ProGuard

ProGuard is an open-source tool that shrinks, optimises and renames Java bytecode. For years it was the standard way to reduce Android release builds, and its configuration format, the keep rule file, is still how Android developers tell the build what must not be removed or renamed.

R8

R8 is the compiler in the Android build toolchain that replaced ProGuard as the default. It performs shrinking, optimisation and renaming while it converts bytecode to DEX, and it reads ProGuard-compatible keep rules. When a release build type has minification enabled, R8 runs and writes a mapping file that translates renamed stack traces back to the original names.

A typical release configuration in a Kotlin build script looks like this:

android {
    buildTypes {
        release {
            isMinifyEnabled = true       // run R8: shrink, optimise, rename
            isShrinkResources = true     // drop unused resources
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
}

What that renaming leaves in place

  • Kept names. Classes and members that are looked up by name, through reflection, serialisation or some SDKs, must keep their names through a keep rule.
  • String literals. API endpoints, error messages and lookup keys are readable in the decompiled app.
  • Native code. Libraries written in C or C++ are outside what R8 processes.
  • Resources and assets. Resource shrinking can remove unused resources, but configuration files, bundled scripts and images that remain are packaged in readable form.
  • Framework code. Dart code in Flutter apps and JavaScript or Hermes bytecode in React Native apps are separate artefacts.
  • The running app. A shrinker has no runtime presence, so it cannot notice a debugger, a hook or a re-signed copy.

Keep rules deserve a closer look, because they decide how much of the renaming survives. A broad rule copied from a template can keep an entire package readable:

# Broad: keeps every class and member in the package readable
-keep class com.example.payments.** { *; }

# Narrow: keeps only what serialisation needs
-keepclassmembers class com.example.payments.model.** {
    <fields>;
}

Renaming, hardening and runtime checks compared

ControlQuestion it answersProvided by a shrinkerWhat app shielding adds
Name obfuscationCan an analyst read class and method names?Yes, for Java and Kotlin code it processesRenaming coordinated with your keep rules, plus protection of native and framework layers
String protectionCan an analyst read endpoints and keys?NoStrings stored in protected form and decoded only when needed
Control-flow obfuscationCan an analyst follow the logic?No; optimisation may reshape code incidentallyRestructured branching in code you choose
Native and framework protectionAre C, C++, Dart and JavaScript layers readable?NoTransformation of each layer on its own terms
Tamper detectionHas the app been modified or re-signed?NoIntegrity checks that fire at runtime and report to the backend
Runtime checksIs the app under attack now?NoDetection of hooking, debugging, emulators, root and jailbreak
Controls in the table above, by who provides them
Provided by a shrinker1Needs app shielding5

Counts rows of the comparison table on this page. A shrinker provides one of the six controls, and only for the Java and Kotlin code it processes.

What a shrinker gives you

  • Smaller, faster release builds
  • Unused code removed
  • Short names for Java and Kotlin code it processes
  • A mapping file for crash reports

What app shielding adds

  • Protected strings, resources and assets
  • Restructured control flow where you choose
  • Native and framework code protection
  • Tamper detection and runtime checks that report to your backend
The two are complementary: shielding works on top of a shrunk build, not instead of it.

When existing tooling is enough, and when it is not

R8 on its own can be a reasonable baseline when the app holds no secrets, shows only public content and relies on the server to enforce every rule. It stops being enough when any of these apply:

  • the app moves money, changes account details or verifies identity;
  • the logic or algorithms are intellectual property;
  • repackaged, modded or cheat versions would hurt revenue or users;
  • the app carries keys, tokens or endpoints you would rather not publish;
  • a regulator or customer expects evidence of protection beyond the default build.

On iOS the question is different again. The toolchain compiles to native code and strips symbols on request but offers no protective renaming, and Objective-C selectors remain visible. iOS protection has to be scoped on its own terms, not inferred from the Android toolchain.

How Otukan extends the baseline

Otukan protects the parts a shrinker does not reach. It renames identifiers, protects strings, restructures control flow in the code you choose, protects configuration files, bundled scripts and assets, transforms native libraries and protects framework code, including Dart in Flutter apps and JavaScript or Hermes bytecode in React Native apps. Anti-tampering detects altered code and re-signed copies, and runtime checks detect hooking frameworks, debuggers, emulators, rooted Android devices and jailbroken iOS devices.

The control is covered on mobile code obfuscation. For the full product, see mobile app security.

Common questions

  • Should we turn off R8 if we add app shielding?

    No. Keep shrinking and optimisation on for release builds; they reduce size and remove unused code. App shielding adds protection on top of the output. Your keep rules and crash reporting keep working, with a private mapping file for each protected build.

  • Is there a ProGuard equivalent for iOS?

    Not in the same form. Xcode compiles Swift and Objective-C to native code and can strip symbols, but it does not rename classes and methods for protection. Objective-C selectors remain visible in the binary. iOS protection is scoped on its own terms, not inferred from the Android toolchain.

  • Do keep rules weaken obfuscation?

    They leave the listed names readable, which is necessary for reflection, serialisation and some SDKs. Broad keep rules copied from templates often keep far more than needed. Review them, narrow them where you can, and protect the code around the names that must stay, rather than giving up on renaming.

Keep reading

Go beyond shrinking in your next release

Tell us your platforms, whether your app uses Kotlin, native libraries, Flutter or React Native, and what you most want to protect. We never ask for source code, binaries or signing keys.

Or request a technical demo