Mobile Development 23 min read

Design and Implementation of an Incremental Compilation Component for Android Projects

The team built a low‑intrusion Gradle plugin that detects changed source and resource files, performs precise dependency analysis, leverages aapt2 for incremental resource compilation, and injects updated Dex and assets into the running app, cutting full Android builds from 418 seconds to 13 seconds and boosting daily developer productivity on large projects such as QQ Music.

Tencent Music Tech Team
Tencent Music Tech Team
Tencent Music Tech Team
Design and Implementation of an Incremental Compilation Component for Android Projects

1. Introduction

In large‑scale Android projects, compilation time becomes a serious bottleneck. For example, QQ Music’s codebase exceeds 1.2 million lines, and a single code change can require more than four minutes before the change is visible on a device.

To address this, the team built a custom incremental compilation component. After a year of optimization, the component is now used daily and significantly improves local development compilation speed.

2. Problem Analysis

During local development the workflow repeats modify code → compile → install APK → run verification . The slowdown can be examined from two dimensions: compilation and installation.

Compilation phase : All resources are first compiled into a resource package and an index class. The index class, together with all source files, is compiled into bytecode, then transformed into Dex files, which are finally packaged, signed and aligned to produce an APK. The time spent is roughly proportional to the number of files being compiled. In practice only a few files change, but native tools re‑compile many more because of dependency analysis.

Android Gradle Plugin 3.0 introduced implementation to reduce compile scope, yet for monolithic projects the benefit is limited.

Installation phase : The APK is transferred via ADB, verified, and its contents (Dex, native libraries, etc.) are unpacked. On Android 5.0/6.0 devices the AOT compiler further pre‑compiles Dex bytecode, adding extra installation time. Package size and OS version both affect this phase.

3. Optimization Ideas

The analysis leads to three categories of solutions:

Optimize the build toolchain (upgrade Gradle/AGP, adjust parameters). In practice the gains are modest.

Replace Gradle with a faster system such as Facebook’s Buck, which supports parallel multi‑module compilation. Migration cost is high for large teams.

Reduce the amount of code that participates in compilation by eliminating redundant code, splitting modules, or applying component‑based architecture. This is difficult and time‑consuming.

The desired solution is a tool that, during local development, only recompiles the changed files and skips the APK installation step, pushing only the incremental artifacts to the device.

4. Birth of Incremental Compilation

The team started developing the component in early 2019. The first version was integrated into the QQ Music project in June 2019.

Measurements show a full build takes about 418 seconds, while an incremental build takes only 13 seconds – a 74 % reduction in daily compilation time per developer.

The component is implemented as a standard Gradle plugin with low intrusion. It supports Java, Kotlin, and all resource types, and later added DataBinding incremental support. An accompanying Android Studio plugin provides a visual interface.

5. Core Principles

5.1 Code Compilation

(1) Detecting Changed Files

After each successful compilation the plugin records a snapshot of file modification timestamps. On the next build it compares the new snapshot with the previous one to obtain the set of changed files.

(2) Dependency Analysis

Only compiling the changed files is insufficient because other classes may depend on them. The plugin uses ASM to parse class files into a custom ResolvedClass structure, recording class name, modifiers, fields, methods, and a list of classes that reference it ( resolvedBy ).

During a full build, all classes are analyzed and stored. When an incremental build is triggered, the changed classes are compiled, their new ResolvedClass data is generated, and compared with the stored version to determine the change type (e.g., method signature change). If the change can affect dependent classes, the plugin gathers the dependent set and performs a second compilation round for those classes.

5.2 Resource Compilation

(1) Incremental Resources

The native resource compiler aapt does not support single‑resource compilation, so the team migrated to aapt2 , which separates compile and link steps and allows incremental resource builds.

During the first full compile, all generated resource binaries are cached. When a resource changes, aapt2 compiles the modified resource to a binary file, which replaces the old file in the cache. The cache directory is then linked to produce the final incremental resource package.

(2) Stable Resource IDs

Resource IDs are generated in R.java . Changes in resource order can shift IDs, causing stale references. To keep IDs stable, the plugin invokes aapt2 with the --emit-ids flag to output a name‑to‑ID map, and on subsequent builds supplies this map via --stable-ids . This guarantees that the same resource receives the same ID across builds.

5.3 Dynamic Loading

(1) Code Injection

Incremental builds produce one or more Dex files that are pushed to a dedicated directory on the device. At application start the component inserts these Dex files at the beginning of the class loader’s Dex array (using reflection), ensuring that the runtime loads the newer classes first. Dex files are inserted in reverse chronological order (e.g., inc_3.dex , inc_2.dex , inc_1.dex ) so the latest implementation wins.

On Android 7.0+ the ART runtime uses a hybrid mode (Interpreter + JIT + AOT). If a class is already present in the ART ClassTable (generated by AOT), the incremental Dex will be ignored, leading to “code change not taking effect”. The component mitigates this by either using a custom class loader (as Tinker does) or, for debug builds, setting android:vmSafeMode="true" in the manifest to disable AOT.

(2) Resource Injection

Resource loading follows the Instant Run approach: the component reflects into AssetManager and calls addAssets to load the incremental resource package, then replaces the global Resources instances held by ActivityThread and other components.

6. Conclusion

The incremental compilation component demonstrates a comprehensive use of Android build, bytecode manipulation, Gradle, and hot‑fix techniques to dramatically improve local development efficiency for large projects. Recommendations include keeping the toolchain up‑to‑date, using Gradle’s profiling tools for targeted optimizations, and gradually modularizing the codebase to reap further benefits.

Future work will add support for incremental Android components (Activities, Services, etc.) and module‑level incremental builds, with an open‑source release planned.

performanceAndroiddynamic loadingBuild OptimizationGradle PluginIncremental Compilationresource compilation
Tencent Music Tech Team
Written by

Tencent Music Tech Team

Public account of Tencent Music's development team, focusing on technology sharing and communication.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.