Mobile Development 30 min read

Optimizing Static Code Scanning for Android Projects: Full and Incremental Scan Strategies

The article outlines how integrating CheckStyle, Lint, and FindBugs with a custom Gradle plugin, applying full‑scan optimizations to collect only necessary source and class files and implementing incremental scans that target only files changed in a PR, cuts Android CI static‑analysis time by over 50 %, dropping full scans from nine to five minutes and enabling sub‑minute incremental checks.

Meituan Technology Team
Meituan Technology Team
Meituan Technology Team
Optimizing Static Code Scanning for Android Projects: Full and Incremental Scan Strategies

Background and Problem In our DevOps practice, the CI pipeline for Android projects includes code submission, static analysis, unit testing, and packaging. Static analysis tools (CheckStyle, Lint, FindBugs) help catch coding style issues, bugs, performance problems, and security risks early. After integrating an internal static‑code‑scan plugin, a typical PR build took 1–2 minutes, but as code volume grew and flavors were added, the scan time rose to 8–9 minutes, with static analysis accounting for about 50 % of the total build time.

Thinking and Strategy We asked whether all three tools are necessary, how to improve scan efficiency, and whether incremental scanning is feasible.

Tool Comparison

Focus of each scanner (style, bugs, Android‑specific checks)

Built‑in rule coverage

Supported file types

Scanning principles

Pros and cons (speed, extensibility, customizability, comprehensiveness)

Conclusion: integrate all three tools to leverage their complementary strengths.

Full‑Scan Optimization

Collecting All Module Target Files We need to identify every module (Gradle Project) involved in a build. The following Groovy method gathers dependent projects for a given variant:

static Set<Project> collectDepProject(Project project, BaseVariant variant, Set<Project> result = null) {
  if (result == null) {
    result = new HashSet<>()
  }
  Set taskSet = variant.javaCompiler.taskDependencies.getDependencies(variant.javaCompiler)
  taskSet.each { Task task ->
    if (task.project != project && hasAndroidPlugin(task.project)) {
      result.add(task.project)
      BaseVariant childVariant = getVariant(task.project)
      if (childVariant.name == variant.name || "${variant.flavorName}${childVariant.buildType.name}".toLowerCase() == variant.name.toLowerCase()) {
        collectDepProject(task.project, childVariant, result)
      }
    }
  }
  return result
}

We then separate source files and compiled class files:

projectSet.each { targetProject ->
  if (targetProject.plugins.hasPlugin(CodeDetectorPlugin) && GradleUtils.hasAndroidPlugin(targetProject)) {
    GradleUtils.getAndroidExtension(targetProject).sourceSets.all { AndroidSourceSet sourceSet ->
      if (!sourceSet.name.startsWith("test") && !sourceSet.name.startsWith(SdkConstants.FD_TEST)) {
        source sourceSet.java.srcDirs
      }
    }
  }
}

For class files we build a FileTree collection, excluding generated or template classes:

static final Collection<String> defaultExcludes = (androidDataBindingExcludes + androidExcludes + butterKnifeExcludes + dagger2Excludes).asImmutable()

List<ConfigurableFileTree> allClassesFileTree = new ArrayList<>()
ConfigurableFileTree currentProjectClassesDir = project.fileTree(dir: variant.javaCompile.destinationDir, excludes: defaultExcludes)
allClassesFileTree.add(currentProjectClassesDir)
GradleUtils.collectDepProject(project, variant).each { targetProject ->
  if (targetProject.plugins.hasPlugin(CodeDetectorPlugin) && GradleUtils.hasAndroidPlugin(targetProject)) {
    GradleUtils.getAndroidVariants(targetProject).each { BaseVariant targetProjectVariant ->
      if (targetProjectVariant.name == variant.name || "${targetProjectVariant.name}".toLowerCase() == variant.buildType.name.toLowerCase()) {
        allClassesFileTree.add(targetProject.fileTree(dir: targetProjectVariant.javaCompile.destinationDir, excludes: defaultExcludes))
      }
    }
  }
}

After applying these optimizations, the overall scan time dropped from ~9 minutes to ~5 minutes.

Incremental Scan Optimization

Only files changed in a PR need to be scanned. We obtain the changed file list via git diff:

git remote add upstream ${upstreamGitUrl}
git fetch upstream ${targetBranch}
git diff --name-only --diff-filter=dr $sourceCommitHash upstream/$targetBranch

For Lint, we construct a custom LintDriver execution that limits the Scope to the subset of changed files. The key method is runLint in LintGradleExecution:

private Pair<List<Warning>, LintBaseline> runLint(@Nullable Variant variant, @NonNull VariantInputs variantInputs, boolean report, boolean isAndroid) {
    IssueRegistry registry = createIssueRegistry(isAndroid);
    LintCliFlags flags = new LintCliFlags();
    LintGradleClient client = new LintGradleClient(
            descriptor.getGradlePluginVersion(),
            registry,
            flags,
            descriptor.getProject(),
            descriptor.getSdkHome(),
            variant,
            variantInputs,
            descriptor.getBuildTools(),
            isAndroid);
    // ... configure options ...
    Pair<List<Warning>, LintBaseline> warnings = client.run(registry);
    return warnings;
}

By setting client.run with a project.subset that contains only the changed Java, XML, or class files, Lint processes a much smaller input set.

FindBugs incremental scanning is achieved by limiting the Classes property to the compiled class files of the changed sources. The task configuration looks like:

findBugs {
    classes = fileTree(dir: "${project.buildDir}/intermediates/classes", includes: classIncludes)
    classpath = variant.javaCompile.classpath + additionalDeps
    // other properties (effort, reportLevel, etc.)
}

CheckStyle incremental scanning simply sets the source property to the list of changed Java files:

void configureIncrementScanSource() {
    boolean isCheckPR = project.hasProperty(CodeDetectorExtension.CHECK_PR) && project.getProperties().get(CodeDetectorExtension.CHECK_PR)
    DiffFileFinder diffFileFinder = isCheckPR ? new DiffFileFinderHelper.PRDiffFileFinder() : new DiffFileFinderHelper.LocalDiffFileFinder()
    source diffFileFinder.findDiffFiles(project)
    if (getSource().isEmpty()) {
        println 'No changed Java files found, skipping CheckStyle.'
    }
}

To avoid false positives in incremental FindBugs scans, we resolve additional dependent classes using ASM and include them in the scan set.

Results

Combining full‑scan and incremental‑scan optimizations yields a >50 % reduction in CI scan time. Full‑scan time decreased to ~5 minutes, and incremental scans on PRs finish within a minute, while daily builds still run full scans to guarantee completeness.

Practical Adoption

We expose plugin configuration flags (e.g., checkStyleIncrement, lintIncrement, findBugsIncrement) so teams can choose full or incremental scans per scenario. The plugin also aggregates results from all three tools without aborting the build on the first failure, and can automatically open the generated HTML report for local developers.

Overall, the article demonstrates a systematic approach to improving static analysis efficiency in large Android codebases, covering tool comparison, full‑scan reduction, incremental‑scan implementation, and real‑world performance gains.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

performanceAndroidGradlestatic analysisCIlintcheckstylefindbugs
Meituan Technology Team
Written by

Meituan Technology Team

Over 10,000 engineers powering China’s leading lifestyle services e‑commerce platform. Supporting hundreds of millions of consumers, millions of merchants across 2,000+ industries. This is the public channel for the tech teams behind Meituan, Dianping, Meituan Waimai, Meituan Select, and related services.

0 followers
Reader feedback

How this landed with the community

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.