Writing Custom Lint Checks with Gradle

December 9, 2014

Backstory

As part of LinkedIn's mobilize effort, we are enabling more developers to contribute to our mobile codebase by providing them with a number of internal libraries. Since it's easy to misuse APIs in the Android SDK, we realized that we needed an effective way to communicate to the entire company about the correct usage of internal libraries as well as application coding conventions. For example, a subclass may have to call super() when overriding some methods, or there may be times when passing an anonymous class as an argument can cause a memory leak. Those errors are not caught by Java compiler. Android's solution for these kind of issues is to use a tool called lint. For example, MissingSuperCall and HandlerLeak are lint checks that will address the above issues. You can get more information by running the following commands:

https://gist.github.com/yangcheng/15d1a3b300eafeef47d6.js

Therefore we decided to explore custom lint checks for our libraries. It turns out that custom lint checks can be useful for both library and application development. In this post I would like to share some of our learnings.

Challenges

Writing Custom Lint Rules is not a new thing. But there are 2 issues in the approach described in the Android documentation that we had to fix before rolling out custom lint checks at LinkedIn:

  1. The customrule.jar that is generated after following the above instructions needs to be put in a local directory (~/.Android/lint). Using a local directory does not scale well with a large number of developers since it's hard to update everyone's machine at the same time. Local directory does not work well with a continuous integration (CI) system like Hudson. Therefore we needed a scalable way to distribute the rules.
  2. Writing Custom Lint Rules are based on Eclipse. We have moved to the new Gradle based build system, and we wanted to use one build system for both lint rules and libraries.

The solution we found is outlined below. It's based on Gradle and does not require any local environment setup other than installing the Android SDK.

AAR to rescue

AAR is the new binary distribution format of Android Libraries. It bundles resources within it so that images and layout files can be distributed. The AAR format indicates that an AAR file can contain an optional lint.jar. It's been confirmed by Android's tools team that if an app depends on a library that contains a lint.jar, then the rules defined in the lint.jar will be executed in the app's lint task. So if we can package the lint.jar into an Android library in AAR format, then the lint rules will be shipped through Gradle to all the team members, and will also be automatically integrated with CI systems like Hudson. In this way both challenges mentioned above are solved.

At LinkedIn, we use an internal private ivy repository to publish all libraries. But an online repository is not a requirement. Local AAR also works. Android Studio (A more current version is available here) has made the process easier. You can now create a new module by importing JAR or AAR:

Then let the app module depend on the new module by adding the following contents to the app's build.gradle (assuming the newly created module's name is 'aar-lib' ):

https://gist.github.com/yangcheng/c9937fbf433731a5f25f.js

You can find a working sample using this configuration here.

Another thing worth mentioning here is that most CI systems are running without window permission, so to get lint running we need to enable headless mode. One approach is to create a gradle.properties file in the project's root folder with the following contents:

https://gist.github.com/yangcheng/8b93a9744337125adbba.js

Build lint.jar with Gradle

First we need to create a pure Java module to generate lint.jar.
  1. In the Android library project, create a new Java module called lintrules in which all lint checks related code will reside.
  2. Add lint-api dependency from Maven Central in the LintChecks's build.gradle file.
    https://gist.github.com/yangcheng/1f1fce94ef4df90a6ca3.js
  3. Write the lint check as described in Writing Custom Lint Rules. Note that the API referenced in the documentation is a bit outdated and the associated code will have to be slightly modified to build successfully.
  4. Register the detectors in manifest by modifying the build.gradle file as shown below:
    https://gist.github.com/yangcheng/45f027035f95953d2db5.js

At this point we should have a working JAR file. You can build the Java project using ./gradlew lintrules:assemble. The resulting JAR is located at lintrules/build/libs/lintrules.jar. It's a good idea to verify the JAR works before continuing any further.

  1. Copy the JAR to ~/.Android/lint
    https://gist.github.com/yangcheng/616c17a2ab3bd1aa450a.js
  2. Make sure the MyId will show up in lint
    https://gist.github.com/yangcheng/3e7b3f645d3ab8442c7a.js
  3. Now delete local JARs to avoid any conflict.
    https://gist.github.com/yangcheng/2b3110c0ebb8aa0b2510.js

Put the JAR into AAR

Unfortunately adding lint.jar into AAR is not supported by Android's Gradle plugin. You can work around this issue by adding some cross project configuration in Gradle.

LintRules module should provide its output so that the library module can reference it.

https://gist.github.com/yangcheng/11c9b6c847aa2c972464.js

The library module will get the output JAR and put it in build/intermediates/lint folder before the AAR is generated. The JAR file needs to be renamed to lint.jar:

https://gist.github.com/yangcheng/21f3af3b8e3ed0767999.js

A working project based on Android tools documentation is available here and an app that depends on the output AAR file is available here.

Real world usage

The above demo is extremely simple but this project setup enables us to develop more powerful lint checks. I'd like to share a simplified version of a custom lint rule we used. In LinkedIn's Android app, the news feed content is modeled as an Update class. An article shared or a profile changed are represented as different subclasses. The code looks like:

https://gist.github.com/yangcheng/3816b2e269ef74638274.js

In the future, someone may try to save the ViewHolder as a reference in the Update class by refactoring:

https://gist.github.com/yangcheng/7b56edf03377848aa481.js

Even though the code appears perfectly fine, the hidden problem here is that we try to cache the Update objects in memory. Let us assume an Activity displays a View that is rendered by LeakUpdate and the user presses back to quit its Activity. At this point, we will keep the model object in memory for later usage. The model object references the ViewHolder that holds on to the View which in turn references the Activity. This results in causing one of the most common memory leaks in Android that is caused by keeping a long-lived reference to a Context.

These errors are not hard to fix but definitely hard to find. The symptoms are general slowness due to constant GC or out of memory error at random places. Once we figured out the pattern, we decided to write a lint check for this issue. The logic for this lint check is simple. It scans all Update subclasses and find fields whose type are View, Context, Fragment or ViewHolder. Lint API package contains some useful utilities like isSubclassOf that make this particular check easy to write. It's also helpful to read Writing a Lint Check and asm documents.

https://gist.github.com/yangcheng/7a292176d9b90ed13472.js

The result was shocking since we found several different leaks and were able to fix them quickly.

Gotchas

  • Lint APIs are not final and they do change.
  • Do check the source code of lint checks in SDK for reference. The source code can be viewed online.
  • Depending on a library project source code via compile project(':library') does not work.
  • Writing unit tests as described in write a lint check is not straightforward for custom lints. The lint-api is on Maven Central but the test dependencies are not. This requires us to copy the source files to the tools and build the Android SDK tools project by following the instructions here in order to unit test custom lint checks.
  • Lint ID should be unique. It's a good idea to prefix the id with Java package name of the id. E.g. 'com.linkedin.updateLeak'. You can reference the ID later to change the severity in the same manner as SDK's lint IDs . For example, we treat an update leak as an error using lint.xml.
    https://gist.github.com/yangcheng/1a3e3885e2c76359f067.js

Moving forward

This is just a taste of custom lint checks. We are excited about the potential and looking forward to what is in store in the future.

Acknowledgment

Thanks to Sameer Sayed and Dustin Shean for providing feedback and Jan Soliman and Szczepan Faber for Gradle tips.

Topics