Build-time obfuscation for Android code & resources: inject junk code, rewrite resources and DEX during normal assemble / bundle, and emit a merged mapping file. No extra Gradle tasks required.
Use cases: raise the cost of reverse engineering / repackaging, protect resources from automated extraction, and isolate multi-channel or white-label builds.
中文文档:README.zh-CN.md
| Plugin ID | io.github.amsonix.molt |
| Extension | molt { } |
| Version | 1.1.0 |
| Requirements | AGP 8.0.0 – 9.3.x (probed) · probe report |
| Feature | Description |
|---|---|
| Junk Code | Generates utility classes to add DEX complexity (light / medium / heavy); optional Activities require activityCountPerPackage + mergeJunkManifest to register in the Manifest |
| Resource overlay | Rewrites image metadata and optional XML comments at compile time (pre-packaging source set) |
| Resource table obfuscation | Rewrites resources.arsc and res paths inside APK/AAB after packaging |
| Component rename | Maps four component types to random short FQCNs (e.g. SplashActivity → e3.gj1); patches DEX / Manifest |
| View rename | Replaces custom View FQCNs in layouts |
| Mapping merge | Merges R8, resource, and rename mappings for Crashlytics upload |
| Baseline Profile | Recompiles baseline.prof / baseline.profm using the merged mapping |
| Keep verification | Optional check that kept resources were not obfuscated |
Pipeline: compile-time junk / resource overlay → R8 → post-R8 rename + resource table obfuscation → merged mapping.
| Term | Meaning |
|---|---|
| AGP | Android Gradle Plugin |
| R8 | Android code shrinker / obfuscator; Release must set isMinifyEnabled = true |
| Post-R8 | Artifact transform stage after R8 finishes and before the final APK/AAB is produced |
| DEX | Android bytecode packaged into APK/AAB |
| Utility class | Junk Code helper class with random methods/fields; not a manifest component; Manifest unchanged by default |
| FQCN | Fully qualified class name, e.g. com.example.SplashActivity |
| Overlay | Compile-time rewrite layered onto source/resources before compilation |
| Mapping | Obfuscation name mapping for deobfuscation and Crashlytics |
| Manifest | AndroidManifest.xml |
| Variant | Build variant, e.g. googleRelease (flavor + buildType, lowercase) |
1. Add the plugin
settings.gradle.kts:
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("io.github.amsonix.molt") version "1.1.0" apply false
}For local plugin development, use a composite build:
pluginManagement { includeBuild("path/to/molt") }. App-sidemolt { }config stays the same.
2. Apply to the app module
app/build.gradle.kts:
plugins {
id("com.android.application")
id("io.github.amsonix.molt")
}
android {
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(/* ... */)
}
}
}
molt {
junkCode { profile.set("light") }
}See Configuration template or the full reference.
3. Build as usual
./gradlew :app:assembleRelease
# or
./gradlew :app:bundleReleaseDaily use usually needs only the blocks below. All options (top-level, resourceObfuscate details, verify* / failOn*, etc.) are in docs/CONFIG.md.
| Option | Default | Description |
|---|---|---|
enabled |
true |
Master switch |
enabledBuildTypes |
release |
Apply only to listed build types; add custom types (e.g. alpha, debug) as needed |
seed |
applicationId.hashCode() |
Obfuscation seed; stable per applicationId |
autoDiscoverKeepXml |
true |
Scan res/raw/keep.xml automatically |
Generates junk code at compile time and packages it into DEX.
| Option | Default | Description |
|---|---|---|
profile |
light |
Utility class scale: light / medium / heavy / custom |
activityCountPerPackage |
0 |
Activities per sub-package; 0 = no components |
mergeJunkManifest |
false |
Merge junk Activities into Manifest (requires previous option > 0) |
profile presets (utility classes only): light 30 classes · medium 100 · heavy 1500. See CONFIG.md.
// Default: utility classes only
molt { junkCode { profile.set("light") } }
// Utility classes + Activities in Manifest
molt {
junkCode {
profile.set("medium")
activityCountPerPackage.set(2)
mergeJunkManifest.set(true)
}
}Post-R8 class renames (see Glossary). Example:
com.example.app.SplashActivity → e3.gj1
Simple names change too — not package-only renames. Both blocks default to enabled = true; disable per variant if needed.
Override global settings per variant (e.g. googleRelease):
variantConfig {
create("googleRelease") {
junkCode { profile.set("heavy") }
componentRename { enabled.set(false) }
}
}Override summary: CONFIG.md → variantConfig.
molt {
junkCode {
profile.set("medium")
// No Activities / Manifest by default; enable when needed:
// activityCountPerPackage.set(2)
// mergeJunkManifest.set(true)
}
resourceObfuscate {
imageAntiDetect.set(true)
}
bundleResourceObfuscate {
enabled.set(true)
obfuscateApk.set(true)
}
componentRename { enabled.set(true) }
viewRename { enabled.set(true) }
autoDiscoverKeepXml.set(true)
variantConfig {
create("googleRelease") {
junkCode { profile.set("heavy") }
verify {
verifyApkKeep.set(true)
verifyBundleKeep.set(true)
}
}
}
}Declare resources that must stay readable in res/raw/keep.xml:
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@string/app_name,@layout/activity_main" />- Exact entries (
@layout/foo): keep original name when present in the artifact - Wildcard prefixes (
@drawable/ad_*): whitelist only; not required to exist
The plugin merges keep files from the app and library dependencies.
Default output root: <rootProject>/build/shell-obfuscate/<variant>/ (not under app/build).
| Path | Description |
|---|---|
build/shell-obfuscate/<variant>/component-mapping.json |
Component rename mapping (JSON) |
build/shell-obfuscate/<variant>/component-mapping.txt |
Component rename report |
build/shell-obfuscate/<variant>/view-mapping.json |
View rename mapping |
build/shell-obfuscate/<variant>/apk-resource/resources-mapping.txt |
APK resource table obfuscation mapping |
build/shell-obfuscate/<variant>/bundle-resource/resources-mapping.txt |
AAB resource table obfuscation mapping |
app/build/outputs/mapping/<variant>/shell-obfuscate-mapping.txt |
Merged mapping (R8 + rename); Crashlytics upload target |
app/build/shell-obfuscate/molt-junk-keep.pro |
Auto-generated ProGuard keep for junk classes |
Run ./gradlew :app:moltPrintVariantPlan to print flags and paths for each variant.
- Release must enable R8 (
isMinifyEnabled = true). Component / View rename patches DEX post-R8; skipped when minify is off. - Default build types: default is
enabledBuildTypes = ["release"]only. Adddebug,alpha, or other custom build types explicitly when needed. - Junk
profile≠ Manifest:light/medium/heavyonly scales utility classes; Manifest changes needactivityCountPerPackage+mergeJunkManifest. - Keep before obfuscate: put ad / Firebase / SDK-critical resources in
keep.xml; enableverifyApkKeep/verifyBundleKeepwhen appropriate. - AGP version: 8.13.x recommended; mismatch warns by default, or set
failOnAgpToolchainMismatch = trueto fail the build. - Crashlytics hook: set
failOnCrashlyticsHookFailure = truein production if upload wiring must not silently skip. - Configuration Cache: compatible with Gradle Configuration Cache; enable
org.gradle.configuration-cache=trueingradle.properties(see COMPATIBILITY.md). - Crashlytics + reproducibility: the Crashlytics Gradle plugin regenerates a new
mapping_file_idresource on every build, which re-triggers R8 (resource shrinking) and therefore invalidates incrementality and makes artifacts non-byte-reproducible. This is expected; each build ships its own matching upload. If you need byte-reproducible builds, pin/cache the mapping file id (e.g. commitmappingFileId.txt) or drop the Crashlytics plugin for those builds. - Mapping is input-sensitive: the rename mapping is regenerated from scanned sources (app + library modules). Any source file change re-rolls all renamed names — do not rely on stable class names across builds; always upload the merged mapping produced by the same build.
When bumping AGP / Gradle in your project:
- Run
./tools/agp-compat.sh(or CI matrix) on the molt repo against the target AGP. - Run
FEATURE_PROBE_TIER=gate ./tools/feature-probe.shfor feature regressions. - Temporarily set
molt { failOnAgpToolchainMismatch.set(true) }to catch aapt2-proto / bundletool pin drift. - Rebuild release APK/AAB and verify
shell-obfuscate-mapping.txt+ Crashlytics upload wiring (moltPrintVariantPlan).
See docs/COMPATIBILITY.md for the verified matrix and probe details.
The sample/ directory contains a minimal app + library + flavor setup.
Plugin resolution (pick one):
- Composite build (recommended for molt repo development): in
sample/settings.gradle.kts, usepluginManagement { includeBuild("..") }and dropmavenLocal(). - Maven Local: publish first with
./gradlew :plugin:publishToMavenLocal, then build sample (current default in repo).
./gradlew -p sample :app:assembleGoogleReleaseSee sample/README.md.
| Doc | Audience |
|---|---|
| docs/CONFIG.md | Full configuration reference |
| README.zh-CN.md | Chinese documentation |
| sample/README.md | Integration sample |
| plugin/CHANGELOG.md | Release notes |
| plugin/README.md | Plugin development & publishing |