Wednesday, November 25, 2015

Gradle Performance Tips from Android Dev Summit

Yesterday was the last day of the Android Dev Summit conference by Google. The conference was packed with deep-dive tech talks on some of the most relevant topics to Android devs today, including Gradle build optimizations, performance testing tools, and data binding tips. I left the conference with a long list of things I can change to improve the performance of my build and tests, and be ready for new tooling features from Google like Instant Run.

The full list of improvements will take a while to implement, but there were three simple fixes I was able to make in a day that had a dramatic impact on the performance of my builds.

------------------------------------------------------------------------

#1 Don't add timestamps, etc to BuildConfig fields in debug builds

It's very convenient to add extra fields to your BuildConfig file. My app adds the git commit sha and the build time so we can display this information inside the app on the developer settings screen. Unfortunately, both of these things change very frequently; timestamp obviously changes every time you build. This means the BuildConfig file needs to be regenerated on every build, and any files that depend on it (probably many files in the app) need to be recompiled as well, essentially killing the benefits of incremental builds.

The fix for this one is to only generate these fields in release builds. In debug fields, we just set them to the constant string "DEBUG".

For example:
buildTypes {
  debug {
    buildConfigField 'String', 'GIT_SHA', "\"DEBUG\""
  }
  release {
    buildConfigField 'String', 'GIT_SHA', "\"${gitSha()}\""
  }
}

------------------------------------------------------------------------

#2 Use the official api for generating code

Version 0.7.0 of the Android Gradle plugin added two api methods: registerJavaGeneratingTask() and registerResGeneratingTask(). These let you register a custom gradle task that will generate code, and will automatically set up the code to be generated at the proper time and indexed & recognized as source by the IDE.

My app used a custom task to generate model files from API data schemas, but we were manually inserting it into the build pipeline without using the official api. The downside of this is that the task was never recognized as "up to date", which forced all models (and all classes that depended on those models) to be recompiled on every build, removing the benefits of incremental builds. Before switching to the official API, running "gradle assembleGoogleDebug" consistently took ~2 minutes, even if there were no code changes. After switching, running the task with no code changes takes ~16 seconds.

------------------------------------------------------------------------

#3 Fix dependency conflicts between different configurations

This one isn't a performance optimization, but rather a way to increase the maintainability of your build script. I ran into this issue a long time ago on my app; Android testing works by loading your app apk and your test apk in the same classpath. If the two configurations depend on different versions of the same library, you'll run into crashes at runtime due to the conflict. The most frequent offender is the support annotations jar, since almost every modern Android library depends on it. Gradle already forces your build to use the same version of shared transitive dependencies, but this only works within the same configuration.

I fixed this previously using Gradle's resolutionStrategy option to force all configurations to resolve to the same version of a dependency, but this isn't a great solution because it requires you to hardcode the version of your dependency in a second location and there's a strong chance you'll forget to update it. The better option is to just add the dependency normally, but add it to both your prod and test configurations.

For example:
compile "com.android.support:support-annotations:${supportLibVersion}"
androidTestCompile "com.android.support:support-annotations:${supportLibVersion}"

------------------------------------------------------------------------

This is just the low hanging fruit that I was able to optimize in a day. Going forward, I'll be following up on several other lessons learned from the conference and hopefully improving build times and incremental compilation times even further. Be sure to check out the session videos from the Android Dev Summit conference if you want to learn more!