Android Lint实现简介
Android SDK
Android SDK中涉及Lint的主要有下面几个包,均包含在Android Gradle插件com.android.tools.build:gradle
中。
-
com.android.tools.lint:lint-api
,这个包提供了Lint的API,包括Context、Project、Detector、Issue、IssueRegistry等。 -
com.android.tools.lint:lint-checks
,这个包实现了Android原生Lint规则。在25.2.3版本中,BuiltinIssueRegistry
中共包含263条Lint规则。 -
com.android.tools.lint:lint
,这个包用于运行Lint检查,提供:-
com.android.tools.lint.XxxReporter
:检查结果报告,包括纯文本、XML、HTML格式等 -
com.android.tools.lint.LintCliClient
:用于在命令行中执行Lint -
com.android.tools.lint.Main
:这个类是命令行版本Lint的Java入口(Command line driver),主要是解析参数、输出结果
-
-
com.android.tools.build:gradle-core
,这个包提供Gradle插件核心功能,其中与Lint相关的主要有:-
com.android.build.gradle.internal.LintGradleProject
:继承自lint-api
中的Project类。Gradle执行Lint检查时使用的Project对象,可获取Manifest、依赖等信息。其中又包含了AppGradleProject
和LibraryProject
两个内部类。 -
com.android.build.gradle.internal.LintGradleClient
:用于在Gradle中执行Lint,继承自LintCliClient -
com.android.build.gradle.tasks.Lint
,Gradle中Lint任务的实现
-
Lint命令行实现
Lint可执行文件位于<android-home>/tools/lint
,是一个Shell脚本,配置相关参数并执行Java调用com.android.tools.lint.Main
进行检查。
Android Studio、IDEA中的实现
在Android Studio或装有Android插件的IDEA环境下,Inspections中的Lint检查是通过Android插件实现的,代码实现主要在org.jetbrains.android.inspections.lint
包中。
IDEA Android插件中Lint部分的实现
https://github.com/JetBrains/android/blob/master/android/src/org/jetbrains/android/inspections/lint
自定义Lint开发基础
主要API
自定义Lint开发需要调用Lint提供的API,最主要的几个API如下。
-
Issue:表示一个Lint规则。例如调用
Toast.makeText()
方法后,没有调用Toast.show()
方法将其显示。 -
IssueRegistry:用于注册要检查的Issue列表。自定义Lint需要生成一个jar文件,其Manifest指向IssueRegistry类。
-
Detector:用于检测并报告代码中的Issue。每个Issue包含一个Detector。
-
Scope:声明Detector要扫描的代码范围,例如Java源文件、XML资源文件、Gradle文件等。每个Issue可包含多个Scope。
-
Scanner:用于扫描并发现代码中的Issue。每个Detector可以实现一到多个Scanner。自定义Lint开发过程中最主要的工作就是实现Scanner。
简易示例如下。
Manifest文件(META-INF/MANIFEST.MF
)
1 | Manifest-Version: 1.0 |
Java代码
1 | public class MyIssueRegistry extends IssueRegistry { |
Scanner
Lint中包括多种类型的Scanner如下,其中最常用的是扫描Java源文件和XML文件的Scanner。
- JavaScanner / JavaPsiScanner / UastScanner:扫描Java源文件
- XmlScanner:扫描XML文件
- ClassScanner:扫描class文件
- BinaryResourceScanner:扫描二进制资源文件
- ResourceFolderScanner:扫描资源文件夹
- GradleScanner:扫描Gradle脚本
- OtherFileScanner:扫描其他类型文件
值得注意的是,扫描Java源文件的Scanner先后经历了三个版本。
-
最开始使用的是JavaScanner,Lint通过Lombok库将Java源码解析成AST(抽象语法树),然后由JavaScanner扫描。
-
在Android Studio 2.2和lint-api 25.2.0版本中,Lint工具将Lombok AST替换为PSI,同时弃用JavaScanner,推荐使用JavaPsiScanner。
PSI是JetBrains在IDEA中解析Java源码生成语法树后提供的API。相比之前的Lombok AST,可以支持Java 1.8、类型解析等。使用JavaPsiScanner实现的自定义Lint规则,可以被加载到Android Studio 2.2+版本中,在编写Android代码时实时执行。
-
在Android Studio 3.0和lint-api 25.4.0版本中,Lint工具将PSI替换为UAST,同时推荐使用新的UastScanner。
UAST是JetBrains在IDEA新版本中用于替换PSI的API。UAST更加语言无关,除了支持Java,还可以支持Kotlin。
本文目前仍然基于PsiJavaScanner做介绍。根据UastScanner源码中的注释,可以很容易的从PsiJavaScanner迁移到UastScanner。
PSI介绍
PSI(Program Structure Interface)是IDEA中用于解析代码的一套API,可将文件的内容表示为特定编程语言中的元素的层级结构。
A PSI (Program Structure Interface) file is the root of a structure representing the contents of a file as a hierarchy of elements in a particular programming language.
每种Psi元素对应一个类,均继承自com.intellij.psi.PsiElement
。例如PsiMethodCallExpression表示方法调用语句,PsiNewExpression表示对象实例化语句等。
官方文档
IntelliJ Platform SDK DevGuide
http://www.jetbrains.org/intellij/sdk/docs/basics/architectural_overview/psi_files.html
PSI Viewer
可以在IDEA / Android Studio中安装PSI Viewer插件,查看代码解析后的PSI元素及其属性值,例如下图中的new Thread(...)
语句,就是一个PsiNewExpression元素。
JavaPsiScanner介绍
JavaPsiScanner中包含6组、12个回调方法,如下。
-
当
getApplicablePsiTypes
返回了需要检查的Psi元素类型列表时,类型匹配的Psi元素(PsiElement
)就会被createPsiVisitor
返回的JavaElementVisitor
检查。 -
当
getApplicableMethodNames
返回方法名的列表时,名称匹配的方法调用(PsiMethodCallExpression
)就会被visitMethod
检查。 -
当
getApplicableConstructorTypes
返回类名的列表时,类名匹配的构造语句(PsiNewExpression
)就会被visitConstructor
检查。 -
当
getApplicableReferenceNames
返回引用名的列表时,名称匹配的引用语句(PsiJavaCodeReferenceElement
)就会被visitReference
检查。 -
当
appliesToResourceRefs
返回true时,Java代码中的资源引用(例如R.layout.main
)就会被visitResourceReference
检查。 -
当
applicableSuperClasses
返回父类名的列表时,父类名匹配的类声明(PsiClass
)就会被checkClass
检查。
1 | public interface JavaPsiScanner { |
自定义Lint开发过程
示例工程可在此下载
https://github.com/jzj1993/AndroidLint
创建Lint.jar
创建基于Gradle的Java工程/模块,编写代码,使用gradle assemble
指令打包成jar。具体可参考示例工程。
其中build.gradle文件如下。
1 | apply plugin: 'java' |
Java源码如下。在这个例子里,创建了两条Lint规则:
- LogDetector:检查是否使用了Android系统的Log工具类,并要求使用统一封装的工具类。
- NewThreadDetector:检查是否直接创建了新线程,并要求使用AsyncTask或统一工具类。
1 | package com.paincker.lint.core; |
1 | package com.paincker.lint.core; |
1 | package com.paincker.lint.core; |
验证Lint.jar文件可用
复制上一步生成的lint.jar
文件到~/.android/lint/
目录下,在Android工程中写一些不符合自定义Lint规则的代码如下。在工程根目录下调用./gradlew lint
执行Lint检查,即可看到Lint输出结果。
验证完成后删掉jar文件,防止和后续步骤冲突。
1 | public class MainActivity extends Activity { |
创建Lint.aar
前面的使用方式,自定义Lint必须保存在电脑中的特定文件夹。实际应用时,往往希望自定义Lint和工程关联,而不是和电脑关联,因此需要创建lint.aar
包,并在需要执行自定义Lint检查的工程中依赖这个AAR。
依赖关系:Java模块 --> 包含lint.jar
的lint.aar
模块 --> 实际Android项目
具体步骤如下(完整的工程见示例代码)。
-
在Android Studio中创建一个空的Java模块
lintjar
,和一个空的Android Library模块lintaar
。 -
lintjar
模块中的配置和前面相同,用于编写实际的Lint规则。 -
lintaar
模块依赖lintjar
模块,build.gradle
如下,主要是把jar文件改成了lint.jar
并打包到AAR里。lintaar
模块编译生成的AAR即为需要的lint.aar
。
1 | apply plugin: 'com.android.library' |
运行自定义Lint
在Android工程中依赖lint.aar
,或者直接依赖前面的lintaar
工程,在执行Lint任务时,就会同时执行自定义的Lint规则(完整工程见示例代码)。
自定义Lint调试
开发过程中,可能需要对自定义Lint进行调试。在电脑上编译Android工程时,自定义Lint是以jar文件的形式被加载并运行的。实际试验发现,其调试过程和Gradle插件开发的调试过程相似。
-
在Android项目中,以源码形式依赖自定义Lint代码(和示例代码一致)。
-
提前在自定义Lint代码中打好断点。
-
在Android Application模块的build.gradle中关闭Lint的abortOnError选项,以免还没到断点时build就中止了。
1
2
3lintOptions {
abortOnError false
} -
在Android Studio的运行参数(Run Configurations)中添加一个Remote类型,都取默认值即可。
-
打开一个命令行窗口,执行下面命令设置临时环境变量,从而开启Gradle调试。端口号为默认的5005,和前面在Android Studio中新增的Run Configuration端口号一致。
1
export GRADLE_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005"
-
还是在这个命令行窗口,执行Gradle任务,并设置参数关闭Gradle Deamon。执行后Gradle会等待Android Studio调试器连接。
1
./gradlew clean lintDebug -Dorg.gradle.daemon=false
-
Android Studio使用刚配置的Remote运行参数,点击调试箭头按钮,连接到Gradle就会开始执行,执行到Lint任务时就会在断点处中断,可以正常调试Java源码。
-
命令行执行下面代码,可关闭Gradle调试
1
unset GRADLE_OPTS
参考资料与扩展阅读
-
使用 Lint 改进您的代码
https://developer.android.com/studio/write/lint.html -
AndAndroid Plugin DSL Reference:LintOptions
http://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.LintOptions.html -
How Do We Configure Android Studio to Run Its Lint on Every Build?
http://stackoverflow.com/questions/32631131/how-do-we-configure-android-studio-to-run-its-lint-on-every-build -
Writing custom lint rules and integrating them with Android Studio inspections
https://android.jlelse.eu/writing-custom-lint-rules-and-integrating-them-with-android-studio-inspections-or-carefulnow-c54d72f00d30 -
你可能不知道的Android Studio/IDEA使用技巧
http://www.paincker.com/android-studio-skill -
Android自定义Lint实践
http://tech.meituan.com/android_custom_lint.html -
Android Gradle配置快速入门
http://www.paincker.com/android-gradle-basics -
Gradle开发快速入门——DSL语法原理与常用API介绍
http://www.paincker.com/gradle-develop-basics -
Viewing PSI Structure
https://www.jetbrains.com/help/idea/viewing-psi-structure.html -
Git - Documentation
https://git-scm.com/documentation