R8 is on by default for release builds in Android Studio, which means most teams already ship shrunk and renamed code without deciding to. What they usually have not decided is whether that renaming is doing any security work, and the honest answer is that it does a little, in a narrow way, and far less than the word obfuscation suggests.
Short answer
R8 replaced ProGuard as the default compiler for Android release builds, and Android's app optimization documentation describes what it does: shrinking removes unused classes and members, optimization rewrites code, and resource shrinking removes unused resources. Renaming identifiers is part of that work, and its primary purpose is size, with reverse-engineering friction as a side effect. Treat R8 as a size tool that raises the cost of casual inspection, never as a place to hide a secret.
What R8 actually does
Four things, and it helps to keep them separate because teams enable one and assume all four.
Shrinking walks the code from your entry points and removes what nothing reaches. Anything reflected into, or referenced only from a manifest or a layout, has to be kept explicitly or it disappears.
Optimization rewrites the remaining code: inlining, removing dead branches, merging classes. This is where behaviour changes are possible if the code depended on something the optimizer considered unobservable.
Renaming replaces class, method and field names with short ones. This is the part people mean when they say obfuscation, and it is a rename rather than an encryption.
Resource shrinking removes drawables, layouts and strings nothing references.
Renaming is the only one of the four with any security character, and even then it is friction rather than protection.
Why renamed code is still readable
A renamed APK decompiles. The names are gone; the structure, the strings and the API calls are not.
An analyst opening a shrunk release build still sees the network calls, the endpoint paths, the string constants, the third-party libraries and the general shape of every function. Renaming means they have to read a.b(c) instead of AuthManager.refreshToken(session), which costs them time on a first pass and almost nothing on a second.
Strings are the specific gap. R8 does not encrypt string literals by default, so an API key, a base URL, a debug flag or a hardcoded credential survives renaming intact and is findable with a text search over the decompiled output.
This is why the OWASP MASVS resilience category treats anti-reverse-engineering measures as defence in depth against a determined attacker rather than as a control you can rely on. The category exists precisely because these measures raise cost rather than close doors.
What R8 does not protect
Anything present in the binary is present to whoever holds the binary. That includes every secret compiled into it.
A key that ships inside the app is a published key. Renaming the class it lives in changes nothing about that. The correct handling is to move the secret to a server you control and give the app a token scoped to what that user is allowed to do, which is a design change rather than a build setting.
Network behaviour is equally visible. An analyst does not need to read your code to see your API; a proxy on a rooted or jailbroken device shows the traffic directly, which is why transport controls and server-side authorization matter far more than what the class names look like.
Business logic that must not be forged belongs on the server. Client-side checks in a renamed binary are still client-side checks, and they can be patched out by anyone willing to spend an afternoon.
Configuring it without breaking the build
Most R8 problems are keep-rule problems, and they show up as crashes in release builds that never appear in debug.
Reflection is the usual cause. Code that looks up a class or method by name at runtime has no static reference for the shrinker to follow, so the target gets removed or renamed and the lookup fails. Serialization libraries, dependency injection and anything reading annotations at runtime are the common cases.
The rule to write is the narrowest one that works. A broad keep rule that preserves whole packages defeats the shrinking you enabled R8 for, and teams that respond to one crash with a package-wide keep tend to end up with release builds barely smaller than debug ones.
Keep your mapping file. Renaming makes crash reports unreadable unless you retain the mapping produced by the build, and losing it means losing the ability to interpret every stack trace from that release.
Test the release build, not the debug build. This is the single most common source of shipped R8 breakage, because the optimizer only runs on release and the failure is therefore invisible until a user finds it.
Where stronger measures make sense
Some apps genuinely need more than renaming, and the honest test is whether the app itself is the asset.
If the app enforces licensing, protects paid media, or is the target of automated fraud, then string encryption, tamper detection and integrity checks earn their place. The MASVS code quality category and the resilience category together describe what those layers look like when they are done properly.
If the app is a client for a service whose rules are enforced server-side, additional hardening buys very little. The attacker's easier path is the API, not the binary, and effort spent on the binary is effort not spent on authorization.
The judgment call is worth making explicitly rather than by default, because commercial hardening tools are expensive and their marketing tends to promise more certainty than the technique supports.
Checking what actually shipped
The build settings you configured and the binary you uploaded are two different artifacts, and only one of them is what users get.
The check that matters is opening the release artifact and looking. Decompiling your own APK is legitimate, quick, and routinely surprising: teams find debug endpoints, test credentials, verbose logging and dependencies nobody remembers adding. Android's security best practices guidance is a reasonable checklist for what should not be in there.
The OWASP MASTG documents the specific techniques for that inspection if you want to do it by hand. For teams without the time to do it manually before every release, an automated scan of the built artifact such as PTKD.com covers the recurring classes of problem: secrets in the bundle, insecure network configuration, permissions that do not match the described functionality.
Either way, inspect the artifact rather than the configuration. A minifyEnabled line in a Gradle file is a statement of intent, and the APK is the evidence.
What renaming costs an analyst, concretely
It helps to put a number on the friction rather than arguing about it in the abstract.
Reading a renamed build means an analyst starts without a map. They open the decompiled output, search for the strings they care about, and work outward from there to the methods that reference those strings. Names come back as they go, not from the binary but from context: a method that builds an Authorization header is the auth method regardless of what it is called.
That process takes hours on a first pass for a moderately sized app, and the second pass on your next release takes considerably less, because the structure rarely changes between versions and their notes carry over.
So the honest description of the benefit is that renaming imposes a one-time cost on a motivated analyst and a permanent cost on a casual one. Both are real. Neither is protection for a secret.
Native code, which is a common misunderstanding
Teams sometimes move a secret into a native library on the assumption that compiled C is meaningfully harder to read than renamed Java or Kotlin.
It is harder, and the difference is smaller than expected. A string constant in a native library is still a string constant, findable with the same tools people use on any binary. Extracting it requires familiarity with disassembly rather than a decompiler click, which filters out some people and not the ones you are worried about.
Where native code genuinely helps is when the value is computed rather than stored, and when the computation depends on inputs the attacker does not have. That is a design property rather than a language property, and you can achieve it in Kotlin just as well by not having the secret on the device.
The same reasoning applies to splitting a key into fragments, encoding it, or assembling it at runtime. Every one of those techniques ends with the plaintext value in memory at the moment it is used, and a debugger attached at that moment reads it. They raise cost; they do not change the category.
Where R8 interacts with the rest of the release
Three practical interactions are worth planning for rather than discovering.
Crash reporting. Renamed stack traces are unreadable until deobfuscated with the mapping file for that exact build. Upload the mapping to your crash reporter as part of the release process, not manually afterwards, because the manual version is the one that gets skipped during a hotfix.
Library keep rules. Dependencies ship their own consumer rules, which is why some libraries work under R8 without configuration. When one does not, check whether the library documents rules before writing your own, since a hand-written rule tends to be broader than the one the maintainer would have given you.
Build reproducibility. Optimization decisions can differ between toolchain versions, which means a release built on a developer machine and one built in CI are not guaranteed identical. Build releases in one place, and treat the CI artifact as the only candidate for upload.
Reading a build report before you argue about it
Before deciding whether R8 is worth tuning, look at what it already achieved on your project, because the answer varies enormously between codebases.
The build produces a mapping file listing every renamed identifier and a set of reports describing what was removed. A project with broad keep rules produces a mapping file that is mostly identity mappings, which is a direct measurement that the renaming is not happening.
Compare the release artifact size with and without shrinking on the same commit. Teams frequently find the difference is smaller than assumed, usually because resources and native libraries dominate the package and code was never the bulk of it.
Then look at what survived. Classes kept by a rule you no longer remember writing, test helpers reachable from production code, and debug-only utilities that were never conditioned on build type all show up here, and each one is both size and exposure.
The point of this exercise is to replace an assumption with a number. Teams argue about whether obfuscation is worth it while having never checked whether their build is obfuscating anything.
One more measurement is worth taking at the same time. Search the decompiled release output for the strings you would least like to find: your production hostnames, any key format your provider uses, the word secret, and the names of internal services. That search takes two minutes and answers the only question that matters about a shrunk build, which is whether renaming changed what an attacker can find.
What to take away
R8 is a size and performance tool that happens to make casual reading harder. That is a real benefit and a small one.
Renaming does not hide strings, endpoints, library usage or program structure, so no secret becomes safe by being compiled into a shrunk build.
Keep rules should be narrow, mapping files should be retained, and release builds should be tested as release builds.
Additional hardening is worth buying when the app itself is the asset being attacked, and worth skipping when the real target is your API. Decide which of those you are, then spend accordingly.
The check that settles the argument is the one described above: decompile your own release build and search it for the strings you would not want found. Whatever survives that search is what shipped, regardless of what the build configuration says.



