Migrating JD Android App to R8: Obfuscation Rules, v1 Signing Issues, and Build Process Pitfalls
This article details JD's Android app migration to the R8 compiler, explains differences between ProGuard and R8, analyzes the impact of the -useuniqueclassmembernames rule, addresses v1 signing loss after AGP 3.6.4 upgrade, and provides practical solutions for build‑process ordering and packaging pitfalls.
Background: JD's Android app previously reduced APK size through image compression, resource shrinking, pluginization, and other techniques. To achieve further reduction, the team investigated Google’s new R8 compiler, which promises both faster builds and smaller packages, and began upgrading the Android Gradle Plugin (AGP) to enable R8.
Obfuscation Tool Overview
Android apps often use code obfuscation to improve security and shrink APK size. ProGuard, the default optimizer before AGP 3.4.0, performs four steps: shrink, optimize, obfuscate, and preverify. The steps can be toggled via -dontshrink , -dontoptimize , -dontobfuscate , and -dontpreverify . After ProGuard processing, D8 converts the resulting .class files to .dex files.
R8 Introduction
Starting with AGP 3.3.0, Google introduced R8 as a replacement for ProGuard, supporting the same -keep rules while integrating shrinking, desugaring, obfuscation, optimization, and D8 processing into a single step. R8 generally yields faster builds and smaller APKs.
Key R8 optimizations include tree shaking, resource shrinking, name shortening, and bytecode optimization.
Enabling R8 in Gradle
release {
// Enable code shrinking
minifyEnabled true
// Enable resource shrinking
shrinkResources true
// ProGuard rules file
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}Pitfall 01 – R8 Obfuscation Rule
When upgrading to AGP 3.3.3, the generated mapping.txt showed that the OtherBean class kept its name (due to a -keep rule) while WatermelonBean was fully obfuscated. After moving to AGP 3.6.4 with R8, even the fields name , color , and shape of WatermelonBean were renamed, breaking JSON deserialization.
com.jd.obfuscate.bean.OtherBean -> com.jd.obfuscate.bean.OtherBean:
java.lang.String name -> name
java.lang.String color -> color
...
com.jd.obfuscate.bean.WatermelonBean -> com.jd.obfuscate.bean.a:
java.lang.String name -> a
java.lang.String color -> b
java.lang.String shape -> c
...The root cause is the -useuniqueclassmembernames rule, which forces globally unique member names. R8 ignores this rule, leading to inconsistent field naming.
AGPBI: {"kind":"warning","text":"Ignoring option: -useuniqueclassmembernames"}Removing the rule from the ProGuard configuration restores expected behavior.
ProGuard Obfuscation Process (Key Code Snippets)
public void execute() throws IOException {
// ... configuration checks ...
if (configuration.shrink || configuration.optimize || configuration.obfuscate || configuration.preverify) {
clearPreverification();
}
// ... class pool initialization ...
if (configuration.obfuscate) {
obfuscate();
}
// ...
}The obfuscate() method ultimately creates a Obfuscator that renames classes and members, applying NameMarker for -keep entries and generating new short names via SimpleNameFactory (a‑z, then aa, ab, …).
R8 Unsupported ProGuard Rules
R8 does not support several ProGuard options, including -useuniqueclassmembernames , -forceprocessing , -dontusemixedcaseclassnames , and many others. Developers must audit their configuration before migration.
-forceprocessing
-dontusemixedcaseclassnames
-dontpreverify
-... (list continues)Pitfall 02 – v1 Signing Loss
After upgrading to AGP 3.6.4, debug builds lost the v1 signature, causing crashes on devices that still require it. The signing logic checks targetApi (the connected device’s API level) and disables v1 signing for API ≥ 24.
private static boolean enableV1Signing(boolean v1Enabled, boolean v2Enabled, int minSdk, @Nullable Integer targetApi) {
if (!v1Enabled) return false;
if (!v2Enabled) return true;
return (targetApi == null || targetApi < NO_V1_SDK) && minSdk < NO_V1_SDK;
}Solution: force targetApi to null via reflection in a Gradle script.
project.afterEvaluate {
project.android.applicationVariants.all { variant ->
def task = project.tasks.findByName("package${variant.name.capitalize()}")
try {
if (task.targetApi != null) {
def field = task.getClass().superclass.superclass.getDeclaredField("targetApi")
field.setAccessible(true)
field.set(task, null)
}
} catch (Exception e) { e.printStackTrace() }
}
}Pitfall 03 – Asset Merging Order
Annotation processing generates a JSON file placed in assets . After AGP upgrade, the mergeAssets task ran before the Java compilation task, causing the JSON to be omitted from the final APK.
// Before upgrade
:compileJavaWithJavac
:mergeAssets
// After upgrade
:mergeAssets
:compileJavaWithJavacFix: make mergeAssets depend on the compilation task.
project.afterEvaluate {
project.android.applicationVariants.all { variant ->
def compileTask = project.tasks.findByName("compile${variant.name.capitalize()}JavaWithJavac")
def mergeTask = project.tasks.findByName("merge${variant.name.capitalize()}Assets")
mergeTask.dependsOn(compileTask)
}
}AGP Upgrade Recommendations
Compare APKs before and after upgrade to detect missing resources.
Automate diff of mapping.txt files to spot missing -keep rules.
Assess risks of ProGuard rules unsupported by R8 (e.g., -useuniqueclassmembernames ).
Prepare a rollback plan before upgrading AGP.
Conclusion
The migration to AGP 3.6.4 and R8 reduced the JD Android app size by roughly 1.5 MB, but introduced several build‑time pitfalls related to obfuscation rules, signing configurations, and task ordering. Addressing these issues ensures a smooth upgrade and stable releases.
JD Retail Technology
Official platform of JD Retail Technology, delivering insightful R&D news and a deep look into the lives and work of technologists.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.