Optimizing Android Build Speed with Fast‑Build Plugin and Monorepo Strategy at Bilibili
Zhang Yang explains how Bilibili’s massive Android monorepo was accelerated by a fast‑build Gradle plugin that replaces unchanged modules with cached AARs versioned by Git SHA, validates them with an R8‑based A8 check, and leverages cloud compilation and independent build units, cutting full builds from ~30 minutes to under 6 minutes locally and about 3 minutes in the cloud.
This article, authored by Zhang Yang, a senior development engineer at Bilibili, describes the challenges and solutions for accelerating the Android CI/CD pipeline of Bilibili’s massive codebase.
Background : Bilibili’s Android project uses a monorepo‑style structure (a single repository containing all modules). While monorepo offers atomic commits, visibility, and stable slicing, permission constraints and shared components (e.g., ijkplayer) still require multiple Git repositories. Gradle plugins are used to combine these sub‑repositories, giving the CI environment monorepo‑like capabilities.
With over 500 modules and 19 composite builds, a full local build can take ~30 minutes, which is unacceptable for a team of hundreds of developers. CI full‑build times are around 16.6 minutes on faster hardware.
Compilation Optimization : By adding the --scan flag, the team identified that most build time is spent in the Gradle task phase (source‑to‑bytecode compilation). Tasks exceeding 2 minutes were highlighted.
To reduce this, the “fast‑build” approach converts unchanged source modules into pre‑built AAR artifacts, skipping their compilation. This sacrifices some code‑level checks but dramatically cuts build time to ~6 minutes for typical changes. The trade‑off is monitored by detecting method‑signature changes that could cause runtime issues.
Workflow : The fast‑build plugin operates during the Gradle configuration phase. It:
Retrieves cached slice information (called “babel commit”) based on Git permissions.
Generates a Project data structure (fields: dir, group, name, version, change, a8Change) for each module.
Computes a version string from the module’s Git SHA combined with a salt value.
Attempts to download the corresponding AAR; if unavailable, the module is compiled from source.
The Project table is defined in the article and the following Groovy helper functions are used:
static def getGitSha(String file) {
def text = "git rev-parse --show-toplevel".execute(null, new File(file)).text.trim()
if (text.length() == 0) { return "" }
def releaPath = file.replace(text + "/", "")
def cmd = 'git log --max-count=1 --pretty=%H '
if (releaPath.length() > 0) { cmd = cmd + " " + releaPath }
def sha = cmd.execute(null, new File(text)).text.trim()
if (sha.startsWith("HEAD") || sha.startsWith("fatal:")) { return "" }
return sha
}To list changed files when a commit is not yet pushed:
static List<String> getAllChangeFile(File file) {
def text = "git status -u -s".execute(null, file).text
String[] txts = text.split("
")
List<String> result = new ArrayList<>()
txts.each { String item ->
if (item.length() > 3) { result.add(item.substring(3)) }
}
return result
}During the configuration phase, a ResolutionStrategy replaces dependencies with the computed AAR versions:
eachDependency { DependencyResolveDetails details ->
Logger.debug("requested " + requested.group + ":" + requested.name + ":" + requested.version)
def targetInfo = details.getTarget().toString().split(":")
String sVersion = compare.select(targetInfo[0], targetInfo[1], targetInfo[2])
if (sVersion != targetInfo[2]) {
def tar = targetInfo[0] + ":" + targetInfo[1] + ":" + sVersion
Logger.debug("git select " + tar)
details.useTarget(tar)
details.because(" git flow ")
}
Logger.debug("target new " + details.getTarget())
}A8 Class Check : To catch method‑signature mismatches after replacing sources with AARs, the team built an A8 check based on Android R8. The core entry point is:
public static void run(A8Command command) throws Throwable {
AndroidApp app = command.getInputApp();
InternalOptions options = command.getInternalOptions();
ExecutorService executor = ThreadUtils.getExecutorService(options);
new A8(options, command.forceReflectionError).run(app, executor, false);
}This check runs in the CI pipeline; any failure blocks the merge.
Remote Upload : When a commit is pushed, a GitLab CI job publishes changed modules as AARs to a custom Maven repository using the maven-publish plugin. The upload is conditioned on the build type (IDE sync vs. compilation) to avoid unnecessary overhead.
Cloud Compilation : The team also introduced a cloud‑build service that sends only the changed files (compared with the previous babel commit) to remote build machines, leveraging larger hardware and cached build artifacts. Average cloud build time drops to ~3 minutes.
Independent Build Units : To further reduce sync time, the monorepo is split into independent build units (framework, common, business modules). A lightweight plugin adds a sub‑settings file when the project is the root, allowing each unit to be compiled separately as either normal, aarOnly, or owner mode.
Future Work : Plans include auto‑generating shell projects for each independent unit, IDE plugins for quick scaffolding, and tighter integration with Bilibili’s automation framework.
Overall, the article demonstrates a comprehensive strategy—combining monorepo management, fast‑build AAR substitution, versioning via Git SHA, dependency resolution, R8‑based validation, and cloud compilation—to achieve dramatically faster Android build cycles for a large‑scale production team.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Bilibili Tech
Provides introductions and tutorials on Bilibili-related technologies.
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.
