文章目录
-
-
- 一、Lint基本使用介绍
-
- 1. 界面操作
- 2 命令行运行
- 二、自定义Lint规则
-
- 第一步:创建一个Lint规则库
-
- 1.1 定义Issue
- 1.2 定义Detector
- 1.3 定义并配置Registry
- 第二步:创建一个Android库
- 第三步:在项目中添加自定义Lint规则
- 三、Lint检查的配置
- 四、总结
-
Lint
是
Android Studio
里面提供的一个代码检查工具,相信大多数Android开发者都用过或了解过Lint,它可以用来对项目做一些基本的但却非常有必要的代码检查,以帮助开发者提升程序的可靠性和性能,以及提高程序的可维护性。比如:XML中存在无用的命名空间、代码中调用了已被弃用的API、多余的装箱操作等,Lint通通可以一次检查出来并给予提示和修改建议。
然而这样的检查仅限于检查Lint支持的问题,并且需要开发者主动操作进行检查。那我们可不可以自定义Lint检查规则呢?可不可以让Lint在我们输入代码的时候就给出错误提示呢?或者在编译项目的时侯自动检查呢?答案是:可以的。
这篇文章我们将着重讲解自定义Lint检查规则及Lint自动检查配置。但在开始之前,不妨先简单介绍一下Lint的基本使用方法。
一、Lint基本使用介绍
有两种方式可以运行Lint检查。
1. 界面操作
在 Android Studio中选择
Analyze
->
Inspect Code
,会弹出如下对话框:
在这个对话框中有两个可配置项:
-
:选择检查的范围,可以选择整个项目检查或是只检查某些模块,也可以自定义检查范围。Inspection scope
-
:用来配置检查的问题项,这里点进去可以看到Lint内置的所有检查项。比如下图中用红色框标出来的,表示它支持检查Handler引用可能导致内存泄漏的问题。如果想取消某项检查,只需取消勾选就行。Inspection profile
点击Ok按钮,即启动Lint检查。
检查完成之后,Android Studio底部将弹出一个检查结果对话框,列出所有检查到的问题及所在文件位置,点击即可直接打开文件进行修改。如下图所示:
2 命令行运行
在Terminal中输入命令行
gradlew lint
即可启动Lint检查。检查完成后会生成一份名叫
lint-results.html
的文档,列出所有检查到的问题。
以上两种运行Lint的方式,我个人偏向于使用第一种,因为比较直观,而且检查完成后可以直接定位修改问题。
更详细的关于Lint的使用方法,请参考官方文档 Improve your code with lint checks
二、自定义Lint规则
前面我们介绍了如何运行Android Studio自带的Lint检查,以及介绍了如何查看或者勾选/取消其自带的检查规则。
自定义Lint检查规则,其实就是把自己写的规则加入到Lint里面去。这个过程有些麻烦,下面我们分步来讲解。
请注意:在不同版本的Android Studio里面,Lint检查的版本有所不同。这篇文章的内容只针对 Android Studio 4
以上的版本。
第一步:创建一个Lint规则库
首先,我们要单独建一个
Java库
来写自定义的Lint规则。请注意,是
Java Library
,不是
Android Library
。
要自定义Lint检查规则,首先需要导入两个Lint官方库,导入方式为
compileOnly
,因为Lint检查只在编译时有效。
dependencies {
compileOnly 'com.android.tools.lint:lint-api:27.1.1'
compileOnly 'com.android.tools.lint:lint-checks:27.1.1'
}
注意:这两个库的版本号必须与Android Studio里面Lint的版本号对应起来,对应关系是
Gradle plugin
的版本号加上23要等于这两个库的版本号。例如这两个库的版本号是
27.1.1
,那么
com.android.tools.build:gradle
的版本号就应该是
4.1.1
,如下:
dependencies {
classpath "com.android.tools.build:gradle:4.1.1"
}
版本号如果不匹配,自定义的Lint规则将无法生效。
导入合适的库之后就可以开始自己写检查规则了,自定义规则有三个重要的类:
- Detector :用来寻找和定位代码中的问题。我们需要创建自己的探测器,来检查代码;
- Issue:用来定义和描述问题;
- IssueRegistry:问题注册器,通过该注册器,我们可以把自定义的问题注册到Android Studio里面。
我们以一个例子来详细介绍上面这三个类以及自定义Lint规则的整个过程。
假设我们的项目中封装了一个打印日志的类,名叫
LogUtil
,我们希望项目中所有要打印日志的地方都使用该类提供的方法。如果有人直接调用了
android.util.Log
来打印日志,我们就让Android Studio报错,并提示他/她使用已经封装好的
LogUtil
类。
1.1 定义Issue
Issue所定义的就是前面的
Inspection profile
列表里面的问题项。可以用如下方法创建一个Issue:
public final static Issue LOG_ISSUE = Issue.create("Log_Issue",
"Do not use Log",
"Please Use `LogUtil` instead of `android.util.Log`",
Category.CORRECTNESS,
6,
Severity.ERROR,
new Implementation(LogDetector.class, Scope.JAVA_FILE_SCOPE));
不妨把前面的图片在这里再放一次,可以对照图片来理解各参数的作用:
- 第1个参数为Issue的id;
- 第2个参数为Issue的简单描述,可以理解为Issue的标题;
- 第3个参数是Issue的详细描述,通常用来提示如何修改该问题;
- 第4个参数是Issue的类别,如图中所示,可以分为各种类别;
- 第5个参数是Issue的优先级,范围是1到10
- 第6个参数表示Issue的严重程度,比如ERROR表示出错,必须修改;WARNING表示提醒,可能存在问题;
- 第7个参数用来对应Issue和Detector,每个Issue都要有与之对应的Detector,表示用该Detector来查找该Issue。其中的Scope表示扫描的范围,Scope.JAVA_FILE_SCOPE表示只扫描Java源文件。
1.2 定义Detector
public final class LogDetector extends Detector implements Detector.UastScanner{
@Override
public List<Class<? extends UElement>> getApplicableUastTypes() {
return Collections.singletonList(UCallExpression.class);
}
@Override
public UElementHandler createUastHandler(@NotNull JavaContext context) {
return new UElementHandler(){
@Override
public void visitCallExpression(@NotNull UCallExpression node) {
if(context.getEvaluator().isMemberInClass(node.resolve(), "android.util.Log")){
context.report(LOG_ISSUE,
context.getLocation(node),
LOG_ISSUE.getExplanation(TextFormat.TEXT));
}
}
};
}
}
这个比较复杂,不如先上代码。
这里我们定义了一个
LogDetector
,它继承自Detector,并且实现了其中的UastScanner接口。
UAST
的全称是
Unified Abstract Syntax Tree
,可翻译为
统一抽象语法树
,即以一种特定的树状结构来描述代码。要在这篇文章里详细介绍UAST是不可能的,我们可以把它类比为一个XML文件的结构,通过解析XML,我们可以获得XML文件所描述的全部信息。同样,通过解析UAST,我们可以获得代码文件的全部信息,比如里面定义的类的信息、类里面的方法的信息、方法里面的代码语句等等。
我们主要关注两个方法:
- getApplicableUastTypes,该方法返回Detector扫描的UAST节点类型,我们这里返回了
,表示LogDetector只扫描方法调用,就是当扫描代码的时候,只关心方法调用的语句。UCallExpression.class
- createUastHandler,需要返回一个自定义的
,用来处理UAST。这里我们要重写UElementHandler的UElementHandler
方法,为什么要重写这个方法呢,因为我们在visitCallExpression
中指定了该Detector只扫描方法调用。这两个方法是相互对应的,在getApplicableUastTypes
方法中指定了什么类型,就需要重写getApplicableUastTypes
里面指定的visit方法。UElementHandler
简单举几个例子:
getApplicableUastTypes中返回的类型 | 需要重写的UElementHandler方法 |
---|---|
UCallExpression.class | visitCallExpression |
UField.class | visitField |
UAnnotation.class | visitAnnotation |
在LogDetector的
visitCallExpression
方法中,我们检测如果被调用的方法来自于
android.util.Log
类,就上报前面定义好的
LOG_ISSUE
。
补充说明:Detector类里面不只有UastScanner接口,也还有其它各种Scanner接口,其作用各不相同,这里列举其中几个作简单介绍;
- UastScanner: 用来扫描java/kotlin源文件
- XmlScanner: 用来扫描XML文件
- GradleScanner: 用来扫描Gradle文件
可见,我们不仅能自定义Java/Kotlin代码的Lint检查规则,也可以对资源文件甚至Gradle文件制定检查规则。
对于Detector里面的检测方法如何写,其实挺不好讲的,因为我自己也没有完全弄明白。官方也说了该API目前只是一个Beta版,甚至连正式的API文档都没有。但是呢还是有办法可以摸索的,比如可以调用
System.out.println()
方法把节点信息打印出来,看哪些信息是自己需要的;也可以多看别人是如何写的,从中借鉴学习;甚至,因为Android Studio内置了许多Lint检查规则,所以可以通过读源码学习官方的写法。在这里提供一个官方Detector源码入口。
1.3 定义并配置Registry
Registry就是用来把我们自定义的规则注册到Android Studio的Lint检查规则里面去。
不如先放代码:
public final class CustomIssueRegistry extends IssueRegistry {
@NotNull
@Override
public List<Issue> getIssues() {
return Collections.singletonList(CustomIssues.LOG_ISSUE);
}
@Override
public int getApi() {
return ApiKt.CURRENT_API;
}
}
很简单,最重要的只有一个方法,即
getIssues()
,通过这个方法返回自定义的全部Issue。geApi方法用来返回当前Lint的API版本号,这是一个固定写法,通常只需要返回
CURRENT_API
即可。
写完Registry之后,要在与
Java文件夹
同级的文件夹下创建一个
resources
资源文件夹,然后在resources文件夹下创建
META-INF
文件夹,然后再在META-INF文件夹下创建
services
文件夹,然后在services文件夹中创建一个名为
com.android.tools.lint.client.api.IssueRegistry
的文件,然后把自定义的Registry的全路径名写在里面。
自定义的Lint规则库的结构如下图所示:
其中
com.android.tools.lint.client.api.IssueRegistry
文件中的内容为:
com.crx.lintrules.CustomIssueRegistry
第二步:创建一个Android库
Android库新建完成后,无需在里面添加任何代码,它只需要依赖于我们前面定义好的Lint规则库。依赖方式如下:
dependencies {
lintPublish project(':lint_rules')
}
配置好依赖,编译完成之后Lint规则库会产生一个
lint.jar
包,Android库会接收该jar包并把它包含在自己编译生成的aar文件中。之后,任何依赖于此Android库的其他项目都将自动执行自定义的Lint检查。
第三步:在项目中添加自定义Lint规则
在项目里引用在第二步中创建好的Android库,该项目就会自动执行在Lint规则库里定义的检查。假如Android库的名字为
lintlibrary
,那么依赖方式如下:
implementation project(':lintlibrary')
编译好项目之后,打开
Analyze
->
Inspect Code
->
Inspection profile
,可以在里面找到我们自定义的Lint规则,如下:
现在,如果我们在代码里调用了
Log
类的方法,Android Studio就会自动报错,如下:
由于在封装的
LogUtil
类里面也调用了Log类方法,我们需要在LogUtil类顶部添加注解
@SuppressLint("Log_Issue")
,该注解表示在该类中取消对
Log_Issue
的Lint检查。
至此,自定义Lint检查算是完成了,如果有代码违反了我们自定义的规则,Android Studio 就能即时提醒。
但是这样的提醒并不是强制的。即使出现了代码出错的提醒,项目仍然可以顺利构建并打包。而我们希望的是在项目构建之前必须通过Lint检查。下一节我们就来讲解如何让项目在构建之前自动运行Lint检查,如果发现错误就中断构建。
三、Lint检查的配置
让项目在构建之前自动进行Lint检查有两种方式:
第一种方式是通过
Edit Configurations
手动添加。
但是这里就不详细介绍了,在本文末尾参考文章列表的第一篇《Run lint when building android studio projectsAsk Question》里有详细的添加步骤。该方法的缺点是这种添加方式只对个人有效,没办法做到一次配置对所有参与该项目的人都产生效果。
那我们直接讲第二种配置方式,在App Module的
build.gradle
文件里配置。
如下:
android {
··· ···
lintOptions{
abortOnError true
}
applicationVariants.all { variant ->
def lintTask = tasks["lint${variant.name.capitalize()}"]
variant.getAssembleProvider().configure(){
it.dependsOn lintTask
}
}
}
lintOptions
选项表示如果lint检查遇到Error,就停止构建。
第二个配置项是让assemble任务依赖于lint检查,即每次执行assemble任务时都会启动lint检查。
配置好之后,以后项目的每一次构建,都会自动执行Lint检查,如果遇到Error,就会终止构建。当然,运行Lint检查是需要消耗时间的,不可避免地会造成项目构建速度变慢。
完整的项目代码已上传到github,欢迎取用LintCustomRuleSample
四、总结
前面已经说过,自定义Lint规则的API仍然是Beta版,也没有正式的文档,因此还存在不少坑。受限于个人水平,这篇文章只作一个初步介绍。如果想进一步了解,需要自己多花时间去摸索,也要多读其他人写的相关文章。我非常建议想作进一步了解的人读一下参考文章的第二篇《美团外卖Android Lint代码检查实践》和第三篇《Enforcing Team Rules with Lint: Detectors 🕵️》。
如果是初次接触自定义Lint规则,看了这篇文章,在操作的过程中遇到无法解决的问题,欢迎在下面留言询问。如果发现文章中有不对的地方,也请不吝指出。总之,欢迎大家多交流讨论,希望能一起进步。
参考文章
- Run lint when building android studio projectsAsk Question
- 美团外卖Android Lint代码检查实践
- Enforcing Team Rules with Lint: Detectors 🕵️
- 自定义Lint规则 Google Sample