天天看点

自定义Lint检查规则

文章目录

      • 一、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

,会弹出如下对话框:

自定义Lint检查规则

在这个对话框中有两个可配置项:

  • Inspection scope

    :选择检查的范围,可以选择整个项目检查或是只检查某些模块,也可以自定义检查范围。
  • Inspection profile

    :用来配置检查的问题项,这里点进去可以看到Lint内置的所有检查项。比如下图中用红色框标出来的,表示它支持检查Handler引用可能导致内存泄漏的问题。如果想取消某项检查,只需取消勾选就行。
    自定义Lint检查规则

点击Ok按钮,即启动Lint检查。

检查完成之后,Android Studio底部将弹出一个检查结果对话框,列出所有检查到的问题及所在文件位置,点击即可直接打开文件进行修改。如下图所示:

自定义Lint检查规则

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));
           
自定义Lint检查规则

不妨把前面的图片在这里再放一次,可以对照图片来理解各参数的作用:

  • 第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节点类型,我们这里返回了

    UCallExpression.class

    ,表示LogDetector只扫描方法调用,就是当扫描代码的时候,只关心方法调用的语句。
  • createUastHandler,需要返回一个自定义的

    UElementHandler

    ,用来处理UAST。这里我们要重写UElementHandler的

    visitCallExpression

    方法,为什么要重写这个方法呢,因为我们在

    getApplicableUastTypes

    中指定了该Detector只扫描方法调用。这两个方法是相互对应的,在

    getApplicableUastTypes

    方法中指定了什么类型,就需要重写

    UElementHandler

    里面指定的visit方法。

简单举几个例子:

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规则库的结构如下图所示:

自定义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规则,如下:

自定义Lint检查规则

现在,如果我们在代码里调用了

Log

类的方法,Android Studio就会自动报错,如下:

自定义Lint检查规则

由于在封装的

LogUtil

类里面也调用了Log类方法,我们需要在LogUtil类顶部添加注解

@SuppressLint("Log_Issue")

,该注解表示在该类中取消对

Log_Issue

的Lint检查。

至此,自定义Lint检查算是完成了,如果有代码违反了我们自定义的规则,Android Studio 就能即时提醒。

但是这样的提醒并不是强制的。即使出现了代码出错的提醒,项目仍然可以顺利构建并打包。而我们希望的是在项目构建之前必须通过Lint检查。下一节我们就来讲解如何让项目在构建之前自动运行Lint检查,如果发现错误就中断构建。

三、Lint检查的配置

让项目在构建之前自动进行Lint检查有两种方式:

第一种方式是通过

Edit Configurations

手动添加。

自定义Lint检查规则

但是这里就不详细介绍了,在本文末尾参考文章列表的第一篇《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规则,看了这篇文章,在操作的过程中遇到无法解决的问题,欢迎在下面留言询问。如果发现文章中有不对的地方,也请不吝指出。总之,欢迎大家多交流讨论,希望能一起进步。

参考文章
  1. Run lint when building android studio projectsAsk Question
  2. 美团外卖Android Lint代码检查实践
  3. Enforcing Team Rules with Lint: Detectors 🕵️
  4. 自定义Lint规则 Google Sample

继续阅读