Establish what the build still has to expose
Obfuscation renames and rewrites code, but the runtime still resolves some things by string. Every APK keeps a set of anchors for that reason.
- Components declared in the manifest, which the system instantiates by class name.
- Classes referenced from XML layouts, menus and other resources, including custom views.
- Native methods, because the JNI link is by name and signature.
- Members that reflection or a serialization library looks up by string.
- Resource identifiers, asset paths, file names and any string the app sends over the network.
These survive most name mangling because removing them breaks the app. They also form the skeleton an analysis can hang everything else on.
Name the protection before reading code
Different protections need different moves, and the moves are not interchangeable. Spend the first pass deciding which ones are present rather than decompiling everything. Read the manifest entry point, list the native libraries per ABI, and check whether classes load through a custom class loader at startup.
- Renamed classes and members with readable structure underneath: name mangling from R8 or ProGuard.
- Constants replaced by numbers and a decode routine: string encryption.
- A method whose body is a loop and a switch over a state variable: control-flow flattening.
- Classes absent from the dex and loaded later: class encryption or runtime dex loading.
- An interpreter with its own opcode table: code virtualization.
Two layers frequently appear together, so treat the list as a set of independent checks rather than a ladder.
Decide what you need from the build
A full reconstruction of a protected app is rarely the goal. Most projects need one action to work outside the app, with the request it sends and the response it reads. Set that boundary before decompiling a large codebase. An analysis that starts without a target produces a lot of notes and no working call.
Once the target is fixed, the reading narrows. You need the endpoint, the headers the client attaches, the body builder, the code that turns the response into a model, and the session state the call depends on. Everything else in the package can stay unread, including screens, background services and code that belongs to third-party libraries.
Read the mapping when it exists
A release build generates a mapping file that records original names against the names in the shipped dex. That file belongs to the build system, not to the distributed APK, so an APK pulled from a device or a store usually does not include it. When you do have it, apply it before you read anything, because every later note becomes clearer with names restored. A mapping from a different build is worse than none, since inlined and merged code can move a symbol to a name that means something else.
Work from the network outward
For API reconstruction, the fastest route into an obfuscated build is the code the app cannot rename into nothing: the HTTP client setup. Look for the library the app uses, the base URL or host names, the interceptors added to the client, and the serializers registered on it. That cluster is recognizable by its API calls even when every application class is a single letter.
From an interceptor or a request builder you reach the class that owns the target endpoint. Follow the data rather than the names. A method that assembles a URL, attaches a token from storage and hands the request to the client belongs to the flow regardless of what it is called.
If the dex holds no signer, check the shipped native libraries next. Exports and strings in the ELF often name the operation, and a native method declaration in Java marks the boundary where the value crosses. Reading one small native routine is usually faster than reconstructing a heavily flattened dex method that does the same work.
Confirm statically before you commit
Static reading produces a hypothesis: this endpoint, these headers, this body shape. Run the app with instrumentation and watch the same call leave the process. Compare the bytes. Values that appear in the trace and not in your notes show where the reconstruction is incomplete, and values in your notes that never appear show where a branch you found does not run on the path you care about.
Keep the trace for the target action only. A full traffic log of an app is a large amount of data, and the useful part is the short window around the call you are rebuilding.
Related work
Reviewed 28 September 2026 · SReverse research desk