静态代码分析工具常被用来检测代码中的质量问题或者编码规范问题。 lint [1] 作为最早的静态代码分析 [2] 工具,已被用来作为静态代码分析工具的代名词 。因此,Android SDK也把其静态代码分析工具取名为Android Lint。
当初,在Android Lint [3] 被加入到Android SDK的时候,提供给开发者的IDE还是Eclipse/ADT组合。就像其它Android SDK中的工具一样,Android Lint并没有与Eclipse/ADT紧密结合 [4],而是作为一个独立工具存在。其用法如下:
lint [flags] <project directory>
在Android Studio出现后,Android Lint与IDE进行了很好地整合。因此,对于Android Studio/Gradle项目,不再建议直接使用独立的lint工具,而是直接在Android Studio中使用或者通过gradle来调用:
$ ./gradlew lint
Android Lint内置了很多lint规则,用来检测一些常见的代码问题(例如,正确性问题、安全问题、性能问题,等等)。同时,Android Lint也支持自定义lint规则,以便开发者灵活应用,更好地提升项目代码质量 [5] 。利用自定义lint规则,既可以用来在项目中检测代码质量问题,也可以用来保证编码规范的执行。
首先,我们需要创建一个Java项目,用来输出包含自定义lint规则的jar。有了包含lint规则的jar后,我们有两种后续方案:
-
方案一:把此jar拷贝到 ~/.android/lint/ 目录中(文件名任意)。此时,这些lint规则针对所有项目生效。
-
方案二:继续创建一个Android library项目,用来输出包含lint.jar的aar;然后,让目标项目依赖此aar即可使自定义lint规则生效。
由于方案一是全局生效的策略,无法单独针对目标项目,用处不大。在工程实践中,我们主要使用方案二。
为了探索自定义Android Lint规则的使用,我创建了一个独立项目AndroidArch,Github地址:https://github.com/yongce/AndroidArch。
我们在开发Android应用时,需要对一些系统API进行二次封装,以便制定统一的处理策略,或者方便将来演进技术方案。为此,项目中往往需要制定开发规范,让团队中所有开发人员遵守。而在工程实践中,如果没有相应的技术手段来保障,团队中难免会有人触犯所制定的开发规范。
AndroidArch项目的主要目的,就是用来展示如何通过自定义lint规则来保证开发规范的实施。
在AndroidArch项目中,共有4个模块:
-
:archLib:Android library项目,包含开发规范所定义的一些基类和一些wrapper类
-
:archLintRules:Java项目,包含开发规范所对应的自定义lint规则
-
:archLintRulesAAR:Android library项目,仅用来输出包含lint.jar的aar
-
:demo:示例项目,用来测试自定义lint规则
自定义lint规则是以一个jar包形式存在的。因此,我们只需要创建一个标准的Java项目即可,参见AndroidArch项目的模块“:archLintRules”。
该Java项目主要有两个重要组成部分:
-
一个Lint注册类:继承自 com.android.tools.lint.client.api.IssueRegistry 的类,用于提供此jar中所有输出的lint规则
-
若干自定义lint规则类:继承自 com.android.tools.lint.detector.api.Detector 类,在其中定义代码检查规则,并定义相应的 com.android.tools.lint.detector.api.Issue 对象。
在输出的jar包中,我们需要在META-INF/MANIFEST.MF清单文件中,添加一项“Lint-Registry”,用来指定该Lint注册类。例如,在模块“:archLintRules”生成的jar中包含如下信息:
$ cat META-INF/MANIFEST.MF Manifest-Version: 1.0 Lint-Registry: me.ycdev.android.arch.lint.MyIssueRegistry
类 me.ycdev.android.arch.lint.MyIssueRegistry 的定义如下:
public class MyIssueRegistry extends IssueRegistry {
@Override
public List<Issue> getIssues() {
System.out.println("!!!!!!!!!!!!! ArchLib lint rules works");
return Arrays.asList(
MyToastHelperDetector.ISSUE,
MyBroadcastHelperDetector.ISSUE,
MyBaseActivityDetector.ISSUE,
MyIntentHelperDetector.ISSUE
);
}
}
从上面的代码可以看到,IssueRegistry类的主要接口是#getIssues(),返回jar中所有输出的Issue。而每一个Issue对象,关联了一个Detector类,从而间接指定了所有支持的lint规则。
在写自定义lint规则时,我们既可以分析Java代码(Java源码文件或者.class文件),也可以分析XML文件(AndroidManifest.xml和各种XML资源文件)。在模块“:archLintRules”中,仅用到了Java源码文件分析。由于相关官方文档还处于缺失状态,Android Lint内置规则的源码成了主要的参考资料:
git clone https://android.googlesource.com/platform/tools/base.git
lint规则相关代码位于目录 lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks 。
为了在输出的jar中添加Lint注册类信息,我们可以通过build.gradle配置来实现。例如:
jar { manifest { attributes("Lint-Registry": "me.ycdev.android.arch.lint.MyIssueRegistry") } }
为了便于生成aar的模块能够直接编译依赖生成jar的模块,我们需要在build.gradle中做一些特殊处理来协同这两个模块。例如:
/* * rules for providing "MyLintRules.jar" */ configurations { lintJarOutput } dependencies { lintJarOutput files(jar) }
这里,创建了一个叫“lintJarOutput”的Gradle configuration,用于输出我们生成的jar包。在生成aar的模块的build.gradle中会引用此configuration。
由于lint jar无法直接在目标项目中使用(这应该是Android Lint值得改进的地方),但aar文件中可以包含一个“lint.jar” [6]。因此,我们需要创建一个Android library项目,仅用来输出“lint.jar”。
创建的Android library项目,仅需要配置build.gradle文件即可。例如:
/* * rules for including "lint.jar" in aar */ configurations { lintJarImport } dependencies { lintJarImport project(path: ":archLintRules", configuration: "lintJarOutput") } task copyLintJar(type: Copy) { from (configurations.lintJarImport) { rename { String fileName -> 'lint.jar' } } into 'build/intermediates/lint/' } project.afterEvaluate { def compileLintTask = project.tasks.find { it.name == 'compileLint' } compileLintTask.dependsOn(copyLintJar) }
这里,创建了一个叫“lintJarImport”的Gradle configuration,其引用了模块 “:archLintRules”的Gradle configuration “lintJarOutput”。
同时,对内置的Gradle task “compileLint”做了修改,让其依赖于我们定义的一个task “copyLintJar”。在task “copyLintJar”中,把模块 “:archLintRules”输出的jar包拷贝到了build/intermediates/lint/lint.jar。从而,生成了一个包含“lint.jar”的aar文件。
有了带有“lint.jar”的aar,我们可以在任何项目中依赖于它,从而让自定义lint规则生效。例如,在AndroidArch项目的模块“:archLib”的build.gradle中,有如下依赖:
dependencies { compile project(':archLintRulesAAR') }
从而,让自定义lint规则在模块“:archLib”中生效。
而模块“:demo”并没有直接依赖于模块“:archLintRulesAAR”,而是通过模块“:archLib”间接依赖的:
dependencies { compile project(':archLib') }
现在,让我们跑跑lint看看:
$ ./gradlew lint ... :archLib:lint !!!!!!!!!!!!! ArchLib lint rules works Ran lint on variant release: 0 issues found Ran lint on variant debug: 0 issues found No issues found. ... :demo:lint !!!!!!!!!!!!! ArchLib lint rules works Ran lint on variant release: 13 issues found Ran lint on variant debug: 13 issues found
可以看到,自定义lint规则生效了!
在探索自定义lint时,发现了两个Android Gradle的Bug:第一个Bug已经修复(http://code.google.com/p/android/issues/detail?id=174808);第二个Bug还未修复(http://code.google.com/p/android/issues/detail?id=178699)。
如果遇到了aar中的“lint.jar”不能被正常加载,可以尝试通过下面的workaround解决,或者升级Android Gradle插件版本解决('com.android.tools.build:gradle:1.3.0-beta4’版本合入了此Bug的修复):
// workaround for the bug: http://code.google.com/p/android/issues/detail?id=174808 project.afterEvaluate { tasks.matching { it.name.startsWith('lint') }.each { task -> task.doFirst { fileTree(project.buildDir) { include '**/jars/lint.jar' }.each { File file -> println "copy lint jar: " + file.absolutePath file.renameTo(new File(file.parentFile.parentFile, file.getName())) } } } }
关于此Bug的细节、workaround工作原理和Android官方的最终修复方法,请参考bug报告中的记录。
对于第二个Bug,具体表现在Android Lint仅会在编译第一个模块时加载“lint.jar”。因此,当需要编译多个模块时,不同的编译顺利可能会导致“lint.jar”能够加载或者无法加载。workaround也很简单,只要保证第一个编译的模块加载了“lint.jar”即可。
目前,自定义lint规则的单元测试还需要依赖于Android源码,但应该很快就会有独立的库可用了。参见https://bintray.com/android/android-tools/com.android.tools.lint.lint-tests/view,但目前还没有可供下载的文件。
Linkedin团队的Cheng Yang同学的这篇文章 https://engineering.linkedin.com/android/writing-custom-lint-checks-gradle 给了很好的启发和开始。