How Incremental Proguard Cuts Android Build Time from 90 Minutes to 8 Minutes
This article explains how a custom incremental Proguard solution for Android Gradle builds reduces the Proguard stage from dozens of minutes to around eight minutes by hashing class nodes, calculating diffs, correcting bytecode, and updating mappings, resulting in stable, fast release builds.
Background: As the business grew, the Android WeChat release build time increased from 50 minutes at the end of 2020 to about 1 hour 30 minutes, with the Proguard stage consuming nearly 40 % of the total time.
Problem: The modular Gradle build runs many plugins in parallel, but the Proguard phase (full Proguard with three optimization passes) takes about 40 minutes, or 20 minutes with an apply‑mapping file.
Why Proguard: Proguard still provides comparable optimization to R8 for WeChat, and the team has custom Dex optimizations, so they continue to use Proguard.
Incremental Obfuscation
The goal is to perform incremental Proguard only on the code that changed between builds, reducing the processing time to minutes.
Implementation Steps
Use ASM to hash the previous output JARs and the new input JARs into a HashClassNodes tree.
Build a Class Nodes reference graph from the new inputs.
Diff the two HashClassNodes trees to obtain changed class nodes.
Spread the diff through the reference graph to capture transitive changes.
Correct bytecode of the diff nodes using the previous usage.txt and mapping.txt.
Re‑map the corrected bytecode.
Update usage.txt, mapping.txt and output JARs.
Diff Calculation
Each JAR is hashed with hash = hash * iConstant + value (iConstant = 37, initial hash = 17). The diff identifies added, changed, removed, line‑number‑changed, and spread changes.
Class Node Structure
Class nodes store direct references and inline relationships; the tree is built with an extended ASM‑Tree library (MM‑ClassNode‑Tree).
Corrector
Based on the diff type, the corrector adds new methods, copies changed code, removes obsolete members, and adjusts line numbers. Example code snippets illustrate handling of ADD, CHANGE, and REPLACE cases.
usageMarker.isAdded(programMethod) || usageMarker.isSpreadAdded(programMethod) -> {
val exceptionsArray = if (programMethod.exceptions == null) null else programMethod.exceptions.toTypedArray<String>()
val correctMethodVisitor = outputClass.visitMethod(programMethod.access, programMethod.name, programMethod.desc, programMethod.signature, exceptionsArray) as MMethodNode
processingFlagMarker.markCorrectAdd(correctMethodVisitor)
programMethod.accept(InnerProcessingMethodVisitor(programClass = inputProgramClass, inputMethodNode = programMethod, methodVisitor = correctMethodVisitor))
}Similarly, CHANGE and REPLACE cases are handled with analogous visitor logic.
Remapping and Output Update
After correction, the bytecode is re‑obfuscated using the previous mapping, updating class, method and field descriptors, then writing the new usage.txt, mapping.txt and output JARs.
Result: The incremental Proguard process finishes in about 8 minutes, with stable build times and minimal code intrusion.
Conclusion: Incremental Proguard provides a controllable, low‑impact solution to Android release build time problems, though further compile‑time optimizations remain under investigation.
Reference: https://www.guardsquare.com/blog/proguard-and-r8
WeChat Client Technology Team
Official account of the WeChat mobile client development team, sharing development experience, cutting‑edge tech, and little‑known stories across Android, iOS, macOS, Windows Phone, and Windows.
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.
