为了方便查看源码,新建一个工程,在build.gradle脚本中,添加如下依赖:
我们可以得到如下所示的依赖:
lint-api-26.1.1
Lint工具集的一个封装,实现了一组API接口,用于启动Lint。
lint-checks-26.1.1
一组内建的检测器,用于对这种描述好Issue进行分析处理。
lint-26.1.1
可以看做是依赖上面两个jar形成的一个基于命令行的封装接口形成的脚手架工程,我们的命令行、Gradle任务都是继承自这个jar包中相关类来做的实现。
lint-gradle-26.1.1
可以看做是针对Gradle任务这种运行方式,基于lint-26.1.1做了一些封装类。
lint-gradle-api-26.1.1
真正Gradle Lint任务在执行时调用的入口。
在理解清楚了以上几个jar的关系和作用之后,我们可以发现Lint的核心库其实是前三个依赖。后面两个其实是基于脚手架,对Gradle这种运行方式做的封装。最核心的逻辑在LintDriver的Analyze方法中。
主要是以下三个重要步骤:
registerCustomDetectors(projects)
Lint为我们提供了许多内建的检测器,除此之外我们还可以自定义一些检测器,这些都需要注册进Lint工具用于对目标文件进行扫描。这个方法主要做以下几件事情:
- 遍历每一个Project和它的依赖Library工程,通过client.findRuleJars来找出自定义的jar包;
- 通过client.findGlobalRuleJars找出全局的自定义jar包,可以作用于每一个Android工程;
- 从找到的jarFiles列表中,解析出自定义的规则,并与内建的Registry一起合并为CompositeIssueRegistry;需要注意的是,自定义的Lint的jar包存放位置是build/intermediaters/lint目录,如果是需要每一个工程都生效,则存放位置为~/.android/lint/。
computeDetectors(project)
这一步主要用来收集当前工程所有可用的检测器。
checkProject(project, main)
接下来这一步是最为关键的一步。在此方法中,调用runFileDetectors来进行文件扫描。Lint支持的扫描文件类型很多,因为是官方支持,所以针对Android工程支持的比较友好。一次Lint任务运行时,Lint的扫描范围主要由Scope来描述。具体表现在:
可以看到,如果Project的Subset为Null,Scope就为Scope.ALL,表示本次扫描会针对能检测的所有范围,相应地在扫描时也会用到所有全部的Detector来扫描文件;
如果Project的Subset不为Null,就遍历Subset的集合,找出Subset中的文件分别对应哪些范围。其实到这里我们已经可以知道,Subset就是我们增量扫描的突破点。接下来我们看一下runFileDetectors:
这里更加明确,如果project.subset不为空,就对单独的Java文件扫描,否则,就对源码文件和测试目录以及自动生成的代码目录进行扫描。整个runFileDetectors的扫描顺序入下:
- Scope.MANIFEST
- Scope.ALL_RESOURCE_FILES)|| scope.contains(Scope.RESOURCE_FILE) || scope.contains(Scope.RESOURCE_FOLDER) || scope.contains(Scope.BINARY_RESOURCE_FILE)
- scope.contains(Scope.JAVA_FILE) || scope.contains(Scope.ALL_JAVA_FILES)
- scope.contains(Scope.CLASS_FILE) || scope.contains(Scope.ALL_CLASS_FILES) || scope.contains(Scope.JAVA_LIBRARIES)
- scope.contains(Scope.GRADLE_FILE)
- scope.contains(Scope.OTHER)
- scope.contains(Scope.PROGUARD_FILE)
- scope.contains(Scope.PROPERTY_FILE)
与[官方文档]的描述顺序一致。
现在我们已经知道,增量扫描的突破点其实是需要构造project.subset对象。
注释也很明确的说明了只要Files不为Null,就会扫描指定文件,否则扫描整个工程。
Lint增量扫描Gradle任务实现
前面分析了如何获取差异文件以及增量扫描的原理,分析的重点还是侧重在Lint工具本身的实现机制上。接下来分析,在Gradle中如何实现一个增量扫描任务。大家知道,通过执行./gradlew lint命令来执行Lint静态代码检测任务。创建一个新的Android工程,在Gradle任务列表中可以在Verification这个组下面找到几个Lint任务,如下所示:
这几个任务就是 Android Gradle插件在加载的时候默认创建的。分别对应于以下几个Task:
- lint->LintGlobalTask:由TaskManager创建;
- lintDebug、lintRelease、lintVitalRelease->LintPerVariantTask:由ApplicationTaskManager或者LibraryTaskManager创建,其中lintVitalRelease只在release下生成。
所以,在Android Gradle 插件中,应用于Lint的任务分别为LintGlobalTask和LintPerVariantTask。他们的区别是前者执行的是扫描所有Variant,后者执行只针对单独的Variant。而我们的增量扫描任务其实是跟Variant无关的,因为我们会把所有差异文件都收集到。无论是LintGlobalTask或者是LintPerVariantTask,都继承自LintBaseTask。最终的扫描任务在LintGradleExecution的runLint方法中执行,这个类位于lint-gradle-26.1.1中,前面提到这个库是基于Lint的API针对Gradle任务做的一些封装。
我们在这个方法中看到了warnings = client.run(registry),这就是Lint扫描得到的结果集。总结一下这个方法中做了哪些准备工作用于Lint扫描:
1. 创建IssueRegistry,包含了Lint内建的BuiltinIssueRegistry;
2. 创建LintCliFlags;
3. 创建LintGradleClient,这里面传入了一大堆参数,都是从Gradle Android 插件的运行环境中获得;
4. 同步LintOptions,这一步是将我们在build.gralde中配置的一些Lint相关的DSL属性,同步设置给LintCliFlags,给真正的Lint 扫描核心库使用;
5. 执行Client的Run方法,开始扫描。
扫描的过程上面的原理部分已经分析了,现在我们思考一下如何构造增量扫描的任务。我们已经分析到扫描的关键点是client.run(registry),所以我们需要构造一个Client来执行扫描。一个想法是通过反射来获取Client的各个参数,当然这个思路是可行的,我们也验证过实现了一个用反射方式构造的Client。但是反射这种方式有个问题是丢失了从Gradle任务执行到调用Lint API开始扫描这一过程中做的其他事情,侵入性比较高,所以我们最终采用继承LintBaseTask自行实现增量扫描任务的方式。
FindBugs扫描简介
FindBugs是一个静态分析工具,它检查类或者JAR 文件,通过Apache的[BCEL]库来分析Class,将字节码与一组缺陷模式进行对比以发现问题。FindBugs自身定义了一套缺陷模式,目前的版本3.0.1内置了总计300多种缺陷,详细可参考[官方文档]。FindBugs作为一个扫描的工具集,可以非常灵活的集成在各种编译工具中。接下来,我们主要分析在Gradle中FindBugs的相关内容。
Gradle FindBugs任务属性分析
在Gradle的内置任务中,有一个FindBugs的Task,我们看一下[官方文档]对Gradle属性的描述。
选几个比较重要的属性介绍:
- Classes
该属性表示我们要分析的Class文件集合,通常我们会把编译结果的Class目录用于扫描。
- Classpath
分析目标集合中的Class需要用到的所有相关的Classes路径,但是并不会分析它们自身,只用于扫描。
- Effort
包含MIN,Default,MAX,级别越高,分析得越严谨越耗时。
- findbugsClasspath
Finbugs库相关的依赖路径,用于配置扫描的引擎库。
- reportLevel
报告级别,分为Low,Medium,High。如果为Low,所有Bug都报告,如果为High,仅报告High优先级。
- Reports
扫描结果存放路径。
通过以上属性解释,不难发现要FindBugs增量扫描,只需要指定Classes的文件集合就可以了。
FindBugs任务增量扫描分析
在做增量扫描任务之前,我们先来看一下FindBugs IDEA插件是如何进行单个文件扫描的。
我们选择Analyze Current File对当前文件进行扫描,扫描结果如下所示: