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:
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:
- 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. - 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'
):
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:
Build lint.jar with Gradle
First we need to create a pure Java module to generatelint.jar
. - In the Android library project, create a new Java module called
lintrules
in which all lint checks related code will reside. - Add lint-api dependency from Maven Central in the LintChecks's
build.gradle
file.https://gist.github.com/yangcheng/1f1fce94ef4df90a6ca3.js - 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.
- 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.
- Copy the JAR to
~/.Android/lint
https://gist.github.com/yangcheng/616c17a2ab3bd1aa450a.js - Make sure the MyId will show up in
lint
https://gist.github.com/yangcheng/3e7b3f645d3ab8442c7a.js - 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.
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
:
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:
In the future, someone may try to save the ViewHolder
as a reference in the Update
class by refactoring:
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.
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 usinglint.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.