前言
我们在开发的过程中,或多或少都遇到过下面这样的提示信息吧:
编写了可能会出错的代码:
使用了过时的api:
xml中直接使用汉语
除此之外,还有很多IDE给我们的提示信息,我们可以随时查看自己的代码是否符合要求以及是否有潜在的风险。这些提示信息都是来自AndroidStudio内置的Lint工具检查我们代码后给出的反馈。当然我们也可以通过配置消除上面的提示。那么Lint是什么呢?我们是否可以自己定义一些规则来对我们写的代码进行一些检查并给出反馈呢?
什么是lint?
定义
Android lint是ADT(Android Developer Tools)16提供的一个代码扫描工具,能够帮我们识别资源、代码结构存在的问题,并检查代码的正确性、安全性、性能、易用性、便利性、国际化方面是否需要优化改进。系统会报告检测到的问题并提供问题描述和严重级别,以便快速确定需要优先进行的修改。
AS中已经内置了大量的Lint检查规则,但是当我们需要定制化规则的时候,就需要自定义Lint了。
禁止检查
我看到这么多提示信息,非常的不爽,但我又不想改,怎么办?
通常情况下,对于一些红色的错误提示,建议是按照提示修改的,但如果实在不想改,我们也可以通过注释来消除。
添加@SuppressLint注解之后,那些讨厌的红色的错误提示没有了。
XML呢?可以消除吗?
1、声明命名空间namespace xmlns:tools="http://schemas.android.com/tools"
2、在布局中使用
Lint的工作过程:
上图是Lint工具的工作流程,下面了解相关概念。
- App Source Files:应用源文件,包含组成Android项目的文件,java、kotlin、xml、图标、Proguard配置文件等
- lint.xml:一个配置文件,此配置文件可用于指定您希望排除的任何 Lint 检查以及自定义问题严重级别。
- lint Tool:一个静态代码扫描工具,可以从命令行或者在AS中对项目运行该工具
- lint Output:lint检查结果,可以在控制台或 Android Studio 的 Inspection Results 窗口中查看 lint 检查结果
Lint 的工作过程由 Lint Tool(检测工具),Source Files(项目源文件) 和 lint.xml(配置文件) 三个部分组成,Lint Tool 读取 Source Files,根据 lint.xml 配置的规则(issue)输出最终的结果。
lint.xml
通过Lint工具的工作流程了解到,可以在lint.xml文件配置一些信息,一般新建项目都是没有lint.xml文件的,在项目的根目录创建lint.xml文件。格式如下:
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<!-- Disable the given check in this project -->
<issue id="IconMissingDensityFolder" severity="ignore" />
<!-- 忽略指定文件中的ObsoleteLayoutParam问题 -->
<issue id="ObsoleteLayoutParam">
<ignore path="res/layout/activation.xml" />
<ignore path="res/layout-xlarge/activation.xml" />
</issue>
<!-- 忽略指定文件中的UselessLeaf问题 -->
<issue id="UselessLeaf">
<ignore path="res/layout/main.xml" />
</issue>
<!-- 将硬编码字符串的严重性更改为“错误” -->
<issue id="HardcodedText" severity="error" />
</lint>
复制代码
运行lint
命令行运行Lint
如果你使用的是 Android Studio,你可以在项目的根目录下输入以下某个命令,使用 Gradle 封装容器对项目调用 lint 任务:
- 在 Windows 上
gradlew lint
- 在 Linux 或 Mac 上
./gradlew lint
> Task :app:lint
Ran lint on variant release: 9 issues found
Ran lint on variant debug: 9 issues found
//生成的报告路径
Wrote HTML report to file:///D:/lintlearn/app/build/reports/lint-results.html
Wrote XML report to file:///D:/lintlearn/app/build/reports/lint-results.xml
复制代码
Android Studio 中使用 Lint
从菜单栏,选择Analyze > Inspect Code
选择检查范围
之后就可以看到结果
左侧是问题分类 右侧是具体的问题
Lint会关注哪些问题
lint关注的问题
- Correctness 正确性:比如硬编码、使用过时 API 等
- Security 安全性:比如在 WebView 中允许使用 JavaScriptInterface 等
- Performance 性能:有影响的编码,比如:静态引用,循环引用等
- Usability 可用性:有更好的替换的 比如排版、图标格式建议.png格式 等
- Accessibility 可访问性:比如ImageView的contentDescription往往建议在属性中定义 等
- Internationalization 国际化:直接使用汉字,没有使用资源引用等
Lint问题的等级
- Fatal:致命的,该类型错误, 该类型的错误会直接中断 ADT 导出 APK
- Error:错误,明确需要解决的问题,包括Crash、明确的Bug、严重性能问题、不符合代码规范等,必须修复。
- Warning:警告, 警告,包括代码编写建议、可能存在的Bug、一些性能优化等,可能是一个潜在的问题
- Informational:可能没有问题,但是检查发现关于代码有一些说法
- ignore:用户不希望看到此问题
Lint相关API,先快速过一遍
导入Lint
为了方便查看,我们首先导入一下Lint的api,在build.gradle中添加一下依赖
就可以在依赖包中看到相关的项目
dependencies {
implementation "com.android.tools.lint:lint-api:27.2.1"
implementation "com.android.tools.lint:lint-checks:27.2.1"
}
复制代码
- com.android.tools.lint:lint-api:这个包提供了Lint的Api,包括Context、Project、Detector、Issue、IssueRegistry等
- com.android.tools.lint:lint-checks:这个包实现了Android原生的Lint规则。
自定义Lint开发需要调用Lint提供的API,最主要的几个API如下。
Issue(问题):
表示一个Lint规则。每一个Issue都有一个ID,并且这个id是唯一的
//使用create方法创建Issue
public static final Issue ISSUE = Issue.create(
"Id", // 唯一的id,简要表面当前提示的问题
"briefDescription", //简单描述当前问题
"explanation",//详细解释当前问题和修复建议
Category.CORRECTNESS, //问题类别,例如上文讲到的Security、Usability等等。
6,//优先级,从1到10,10最重要
Severity.ERROR,//严重程度:FATAL(奔溃), ERROR(错误), WARNING(警告),INFORMATIONAL(信息性),IGNORE(可忽略)
new Implementation(LogDetector.class, Scope.JAVA_FILE_SCOPE)//Issue和哪个Detector绑定,以及声明检查的范围Scope
);
复制代码
- Scope:声明Detector要扫描的代码范围,例如Java源文件、XML资源文件、Gradle文件等。每个Issue可包含多个Scope。
IssueRegistry(注册器):
用于注册要检查的Issue列表。自定义Lint需要生成一个jar文件,其Manifest指向IssueRegistry类。
//用于注册要检查的Issue(规则),只有注册了Issue,该Issue才能被使用
public class MyIssueRegistry extends IssueRegistry {
@NotNull
@Override
public List<Issue> getIssues() {
//返回值为之前定义的Issue
return Arrays.asList(LogDetector.ISSUE);
}
}
复制代码
Detector(探测器):
用于检测并报告代码中的Issue。每个Issue包含一个Detector。自定义Lint的过程其实就是重写Detector的相关方法。
//自定义的LogDetector继承自Detec
public class LogDetector extends Detector implements Detector.UastScanner {
....
}
复制代码
看一下有哪些方法接下来我们会用到,以及查询和检查的对应关系:
- getApplicableUastTypes(): 此方法返回需要检查的AST节点的类型,类型匹配的UElement将会被createUastHandler(createJavaVisitor)创建的UElementHandler(Visitor)检查。
- createUastHandler(): 创建一个uastHandler来检查需要检查的UElement,也就是getApplicableUastTypes返回的UElement
- getApplicableMethodNames(): 返回你所需要检查的方法名称列表,这些方法将通过visitMethod方法被检查
- visitMethod(): 检查getApplicableMethodNames相匹配的方法
- getApplicableConstructorTypes(): 返回需要检查的构造函数列表,类型匹配的方法将通过visitConstructor方法被检查
- visitConstructor(): 检查getApplicableConstructorTypes相匹配的构造方法
- getApplicableReferenceNames(): 返回需要检查的引用路径名,匹配的引用将通过visitReference被检查
- visitReference(): 检查与getApplicableReferenceNames相匹配的引用路径
- appliesToResourceRefs(): 返回需要检查的引用资源,将通过visitResourceReference检查
- visitResourceReference(): 检查appliesToResourceRefs相匹配的资源
- applicableSuperClasses(): 返回需要检查的父类名列表
- visitClass(): 检查applicableSuperClasses返回的类
这些方法很重要,等下还要用到,不同的规则要实现不同的方法进行实现。
Scanner(扫描仪):
通过上面的代码,我们可以看到除了要继承Detecor之外,还要实现xxxScanner,Scanner是干啥用的呢?它是用于扫描并发现代码中的Issue。每个Detector可以实现一到多个Scanner。自定义Lint开发过程中最主要的工作就是实现Scanner。
- UastScanner:扫描Java、Kotlin源文件
- XmlScanner:扫描XML文件
- ResourceFolderScanner:扫描资源文件
- ClassScanner:扫描class文件
- BinaryResourceScanner:扫描二进制文件
- ResourceFolderScanner:扫描资源文件夹
- GradleScanner:扫描Gradle脚本
- OtherFileScanner:扫描其他类型文件
旧版本的JavaScanner、JavaPsiScanner已经被UastScanner替代了。
扫描Java源文件的Scanner先后经历了三个版本。
- 最开始使用的是JavaScanner,lint通过Lombok库将Java源码解析成AST(抽象语法树),然后由JavaScanner扫描
- 在Android Studio 2.2和lint-api 25.2.0版本中,Lint将Lombok AST替换为PSI(Program Structure Interface,是IDEA中用于解析代码的一套API),同时弃用JavaScanner,推荐使用JavaPsiScanner
- 在Android Studio 3.0和lint-api 25.4.0版本中,Lint将PSI替换为UAST(通用抽象语法树),同时推荐使用UastScanner。UAST更加与语法无关,同时支持java和Kotlin,UAST节点本质上是java和kotlin所支持的超集,当你使用UAST编写lint规则的时候,你的lint规则同时适用于java和kotlin文件,无需编写两套规则,还可以查看gradle文件和xml文件。
自定义Lint实践
通过以上的了解,我们大体上知道了自定义一个Lint规则所用到的API,现在我们就来实际操作一把。
我们创建一个Log的lint规则,项目中如果使用Log.d("","");//e,v,w,wtf
这样的日志打印,就给出提示需要使用项目中集成的 LogUtil 工具类中的方法。
创建java library
自定义规则需要在java工程创建,这里我们创建一个java library,并添加依赖
定义自己的Detector
大致可以分为四步
- 我们定义的是检测java代码级别的lint,所以实现
Detector.UastScanner
- 创建一个Issue,定义问题的 级别、提示信息、权重、作用域等内容
- 实现所需方法并重写逻辑(getApplicableUastTypes拿到节点,createUastHandler检查)
- 符合条件的 通过
context.report()
方法进行上报Issue
1、继承Detector并实现Scanner
public class MyDetector extends Detector implements Detector.UastScanner {
//2、定义 ISSUE
public static final Issue ISSUE = Issue.create(
"Log Use Error", //唯一 ID 这个id必须是独一无二的
"please use LogUtil", //简单描述
"please use LogUtil!!!", //详细描述
Category.CORRECTNESS, //验证正确性
6, //权重 优先级,必须在1到10之间。
Severity.WARNING, //这是一个警告
new Implementation( //这是连接Detector与Scope的桥梁,其中Detector的功能是寻找issue,而scope定义了在什么范围内查找issue
LogDetector.class,
Scope.JAVA_FILE_SCOPE));
//此方法返回需要检查的AST节点的类型,类型匹配的UElement将会被createUastHandler(createJavaVisitor)创建的UElementHandler(Visitor)检查。
@Nullable
@Override
public List<Class<? extends UElement>> getApplicableUastTypes() {
return Collections.singletonList(UCallExpression.class);
}
//3、创建一个uastHandler来检查需要检查的UElement
@Nullable
@Override
public UElementHandler createUastHandler(@NotNull JavaContext context) {
//UElementHandler]类似于[UastVisitor],但它仅用于访问单个元素
return new UElementHandler() {
@Override
public void visitCallExpression(@NotNull UCallExpression node) {
if (!UastExpressionUtils.isMethodCall(node)) {
return;
}
if (node.getReceiver() != null && node.getMethodName() != null) {
String methodName = node.getMethodName();
//检查方法名是否匹配
if (methodName.equals("i")
|| methodName.equals("d")
|| methodName.equals("e")
|| methodName.equals("v")
|| methodName.equals("w")
|| methodName.equals("wtf")) {
PsiMethod resolve = node.resolve();
if (context.getEvaluator().isMemberInClass(resolve, "android.util.Log")) {
//4、符合条件,上报 ISSUE
context.report(ISSUE, node, context.getLocation(node), "please use LogUtil");
}
}
}
}
};
}
}
复制代码
注册 Detector
public class MyIssueRegistry extends IssueRegistry {
@NotNull
@Override
public List<Issue> getIssues() {
return Arrays.asList(MyDetector.ISSUE);
}
}
复制代码
这里可以注册多个 Detector,目前最新版本的 Lint 内置了300多种 Detector,都在 BuiltinIssueRegistry 类中,可以作为我们编写自定义 Lint 的最佳参考案例。如图:
集成到项目中
使用自定义Lint规则,有两种方式:jar包和aar文件
jar包的方式:
在AS中的Terminal中输入:gradlew 你自己定义的module名:assemble
BUILD SUCCESSFUL之后,在libs目录下会生成jar包,将jar包拷贝到copy到.android/lint/
目录下,没有lint目录则新建
在cmd下输入lint --list
就可以看到配置的lint规则(需要配置lint的环境变量在path中)
重启之后,最终,我们自定义的lint规则在代码中就体现出来了
使用aar的方式
使用jar的方式的话,因为是全局的,所以定义的lint规则,所有项目都会检测到,这样就会导致本项目的规则在其他项目也适用了,其他项目就会报一些本不该出现的异常提示信息。如果要针对单个项目,就需要使用aar的方式。
1.在同一个工程下新建Android Library,用于导出aar
2.修改自定义lint规则的Java库的build.gradle,注意到要将implementation改为compileOnly。
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
compileOnly "com.android.tools.lint:lint-api:27.2.1"
compileOnly "com.android.tools.lint:lint-checks:27.2.1"
}
复制代码
3.修改Android Library的依赖
dependencies {
...
lintPublish project(':lintlib')
}
复制代码
4.输出aar文件
在Android Library下执行assemble,
BUILD SUCCESSFUL之后,生成新的aar
5.依赖aar文件(本地依赖,也可以上传到远端仓库)
将aar文件拷贝到app的libs目录下,并配置gradle
repositories {
flatDir {
dirs 'libs'
}
}
dependencies {
...
implementation (name:'androidlintlib-debug',ext:'aar')
}
复制代码
操作完之后rebuild项目,然后就可以使用了,看下效果:
结语
经过以上对lint规则和api的介绍,以及实战操作之后,相信你已经对Lint有了一个初步的了解,也发现了,其实这个东西并不难,只是学习的过程很痛苦,面向自己不熟悉的领域总是要经过一个阵痛期的,虽然lint在我们实际工作中使用频率并不高,可能也接触不到,但是学习一下对自己还是很有好处的,不仅可以扩展自己的技术栈,还能加深对java/kotlin代码编译过程有个更深入的了解。
参考资料: