Lint实现原理
一.LintTask
apply plugin: 'com.android.library'
apply plugin: 'com.android.application'
当我们在gradle中,apply一个library或者application的plugin时,对应的其实是一个LibraryPlugin或AppPlugin对象,他们都继承自BasePlugin,在BasePlugin的apply()方法中,会调用createTasks()方法:
private void createTasks() {
//...
project.afterEvaluate(
project -> {
sourceSetManager.runBuildableArtifactsActions();
threadRecorder.record(
ExecutionType.BASE_PLUGIN_CREATE_ANDROID_TASKS,
project.getPath(),
null,
() -> createAndroidTasks());
});
}
在gradle都执行完(afterEvaluate)后,调用createAndroidTasks方法,最终会调用到VariantManager的createTasksForVariantScope抽象方法,为每一个variant创建Tasks:
for (final VariantScope variantScope : variantScopes) {
createTasksForVariantData(variantScope);
}
无论TaskManager是LibraryTaskManager还是ApplicationTaskManager类型,都会在该方法中调用createLintTasks方法,创建Lint的Task:
public void createLintTasks(final VariantScope scope) {
//...
taskFactory.register(new LintPerVariantTask.CreationAction(scope, variantScopes));
}
LintPerVariantTask.CreationAction:
public static class CreationAction extends BaseCreationAction<LintPerVariantTask> {
//...
@Override
@NonNull
public String getName() {
//generate task name->lintMusicallyI18nDebug
return scope.getTaskName("lint");
}
@Override
@NonNull
public Class<LintPerVariantTask> getType() {
//create new instance by reflect
return LintPerVariantTask.class;
}
@Override
public void configure(@NonNull LintPerVariantTask lint) {
//...
}
- create时,通过getType()返回的Class对象,反射创建Task对象,添加到TaskManager里
- Task的name,由scope产生,lint拼接scope的variant name,如lintMusicallyI18nDebug
- 最后调用CreationAction的configure方法进行Task的配置
二.LintOptions配置
lint相关的配置,我们需要配置在gradle中,通过extension的形式定义:
android {
lintOptions {
//checked rules
check 'HardcodedText'
//results
xmlReport true
xmlOutput file("lint-results.xml")
}
}
1.创建
同样是BasePlugin的apply()方法中,进行了Extension的初始化:
threadRecorder.record(
ExecutionType.BASE_PLUGIN_PROJECT_BASE_EXTENSION_CREATION,
project.getPath(),
null,
this::configureExtension);
private void configureExtension() {
extension =
createExtension(
project,
...);
globalScope.setExtension(extension);
}
- 首先调用了抽象方法configureExtension(),每种Plugin进行自己的Extension初始化
- 把生成的Extension对象放入globalScope中
LibrayPlugin的configureExtension():
protected BaseExtension createExtension(@NonNull Project project, ...) {
return project.getExtensions()
.create(
"android",
getExtensionClass(),
...);
}
其中,"android"作为Extension的名字,然后getExtensionClass()返回的Class对象,会通过反射方式创建一个实例,作为Extension对象,LibraryPlugin的该方法,返回的是LibraryExtension.class,在LibraryExtension初始化时,会创建一个LintOptions对象:
lintOptions = objectFactory.newInstance(LintOptions.class);
2.配置
在gradle解析时,会将我们配置的lintOptions选项,配置到LintOptions对象中:
public void lintOptions(Action<LintOptions> action) {
action.execute(lintOptions);
}
3.获取
在CreationAction的configure方法配置Task时,从globalScope中的Extension,取出LintOptions,设置给Task:
public void configure(@NonNull T lintTask) {
lintTask.lintOptions = globalScope.getExtension().getLintOptions();
//...
}
三.Lint执行触发
ReflectiveLintRunner
LintPerVariantTask作为一个Task,执行时会调用标有@TaskAction注解的方法:
@TaskAction
public void lint() {
//LintPerVariantTaskDescriptor是Task的一些参数,包括LintOptions可以从中获取
runLint(new LintPerVariantTaskDescriptor());
}
protected void runLint(LintBaseTaskDescriptor descriptor) {
FileCollection lintClassPath = getLintClassPath();
if (lintClassPath != null) {
new ReflectiveLintRunner().runLint(getProject().getGradle(),
descriptor, lintClassPath.getFiles());
}
}
- getLintClassPath:Lint相关依赖,在BasePlugin的apply()时进行初始化
public static void createLintClasspathConfiguration(@NonNull Project project) {
Configuration config = project.getConfigurations().create(LintBaseTask.LINT_CLASS_PATH);
//...
project.getDependencies().add(config.getName(), "com.android.tools.lint:lint-gradle:" +
Version.ANDROID_TOOLS_BASE_VERSION);
}
lint相关的依赖:com.android.tools.lint:lint-gradle:26.2.1
执行时,调用ReflectiveLintRunner的runLint()方法:
fun runLint(gradle: Gradle, request: LintExecutionRequest, lintClassPath: Set<File>) {
try {
val loader = getLintClassLoader(gradle, lintClassPath)
val cls = loader.loadClass("com.android.tools.lint.gradle.LintGradleExecution")
val constructor = cls.getConstructor(LintExecutionRequest::class.java)
val driver = constructor.newInstance(request)
val analyzeMethod = driver.javaClass.getDeclaredMethod("analyze")
analyzeMethod.invoke(driver)
} catch (e: InvocationTargetException) {
if (e.targetException is GradleException) {
// Build error from lint -- pass it on
throw e.targetException
}
throw wrapExceptionAsString(e)
} catch (t: Throwable) {
// Reflection problem
throw wrapExceptionAsString(t)
}
}
这里需要说明:
- ReflectiveLintRunner、LintExecutionRequest这些类是属于lint-gradle-api这个依赖的
- 具体的实现类如LintGradleExecution,属于lint-gradle这个依赖
- gradle插件直接依赖了lint-gradle-api,没有直接依赖lint-gradle,所以需要通过反射方式调用LintGradleExecution
LintGradleExecution
public void analyze() throws IOException {
if (toolingRegistry != null) {
//...
String variantName = descriptor.getVariantName();
if (variantName != null) {
for (Variant variant : modelProject.getVariants()) {
if (variant.getName().equals(variantName)) {
lintSingleVariant(variant);
return;
}
}
}...
}...
}
public void lintSingleVariant(@NonNull Variant variant) {
//...
runLint(variant, variantInputs, true, true, true);
}
private Pair<List<Warning>, LintBaseline> runLint(
@Nullable Variant variant,
@NonNull VariantInputs variantInputs,
boolean report,
boolean isAndroid,
boolean allowFix) {
//创建Android的默认ISSUES
IssueRegistry registry = createIssueRegistry(isAndroid);
//创建Lint里需要的配置model
LintCliFlags flags = new LintCliFlags();
//创建Lint执行器的对象
LintGradleClient client =
new LintGradleClient(
descriptor.getGradlePluginVersion(),
registry,
flags,
...);
//...
LintOptions lintOptions = descriptor.getLintOptions();
boolean fix = false;
if (lintOptions != null) {
//同步LintOptions到LintCliFlags中
syncOptions(
lintOptions,
client,
flags,
...);
}...
//...
try {
//执行Lint检测
warnings = client.run(registry);
} catch (IOException e) {
//...
}
//如果检测有问题且配置中允许report,则抛出GradleException终止命令的执行
if (report && client.haveErrors() && flags.isSetExitCode()) {
abort(client, warnings.getFirst(), isAndroid);
}
return warnings;
}
- 创建Android默认的规则ISSUE(如对TextView、R.id的检测),这里的ISSUES是每个lint规则需要定义的一个对象,将规则实现时会细说
private static BuiltinIssueRegistry createIssueRegistry(boolean isAndroid) {
if (isAndroid) {
return new BuiltinIssueRegistry();
}...
}
//BuiltinIssueRegistry
static {
List<Issue> issues = new ArrayList<>(INITIAL_CAPACITY);
issues.add(MissingIdDetector.ISSUE);
issues.add(TextViewDetector.ISSUE);
//...
}
- LintCliFlags为lint执行器的配置项,只需要将LintOptions的配置项设置进来即可
- LintGradleClient为lint检测的执行器,继承自LintCliClient,run方法为执行lint检测的入口
- 检测结束后,如果有检测出来的error以及需要report,进行abort,实质就是抛出一个GradleException
LintGradleClient(extends LintCliClient)
public int run(@NonNull IssueRegistry registry, @NonNull List<File> files) throws IOException {
//...
//创建LintRequest,获取Project
LintRequest lintRequest = createLintRequest(files);
//创建LintDriver---Lint检测解析器
driver = createDriver(registry, lintRequest);
//...
//执行真正的Lint分析
driver.analyze();
//输出结果,html、xml
for (Reporter reporter : flags.getReporters()) {
reporter.write(stats, warnings);
}
//...
//baseline写入相关...
return flags.isSetExitCode() ? (hasErrors ? ERRNO_ERRORS : ERRNO_SUCCESS) : ERRNO_SUCCESS;
}
四.Lint规则定义
在讲解检测流程之前,我们需要先了解一下Lint提供的检测方法API的定义,我们以自定义一个检测Log方法调用的规则为例进行讲解。
Detector
public class LogUsageDetector extends Detector implements Detector.UastScanner {
public static final Issue ISSUE = Issue.create(
"LogUsage",
"请勿直接调用android.util.Log",
"请勿直接调用android.util.Log,应该使用统一工具类com.common.utility.Logger",
Category.SECURITY, 10, Severity.FATAL,
new Implementation(LogUsageDetector.class, Scope.JAVA_FILE_SCOPE));
@Override
public List<String> getApplicableMethodNames() {
return Arrays.asList("v", "d", "i", "w", "e", "wtf");
}
@Override
public void visitMethod(@NotNull JavaContext context, @NotNull UCallExpression node, @NotNull PsiMethod method) {
if (context.getEvaluator().isMemberInClass(method, "android.util.Log")) {
context.report(ISSUE, node, context.getLocation(node), "请勿直接调用android.util.Log");
}
}
}
- 首先我们需要定义一个规则类,继承自Detector类
- 并且因为我们检查的是java/kotlin代码里的调用,所以需要实现Detector.UastScanner接口,表明这是一个需要源代码(java/kotlin)文件扫描的类
- 每一个需要检测的问题,都需要有一个上报时用到的Issue对象,用来指定这个问题的一些属性:
- Category.SECURITY代表这个问题的类别,有安全类别、UI类别、翻译问题等等
- priority代表问题的重要级别,数字(1-10)越大级别越高
- Severity.FATAL代表问题的重要性,FATAL为最严重,还有WARNING警告类型等,检测时会根据重要性决定是否上报等,如WARNING一般就是我们见到的报黄色的错误,ERROR和FATAL就会报红
- 需要定义一个Implementation对象,指明这个规则类的Class对象,用作反射创建,并指明改规则需要检测的具体文件范围(叫做Scope),这里是要检查源代码文件,所以是Scope.JAVA_FILE_SCOPE,除此之外还有很多类型:

- 然后我们重写getApplicableMethodNames()方法,这个方法返回的是要检测的方法名字的集合,就是一个List,这里返回的是Log的相应方法
- 还要重写visitMethod方法,由于我们设置的该类是检测的源代码文件中,有关方法名为(v,d,i,w,e,wtf)的调用语句,所以再Lint检测到类似方法的调用时,会调用到该类的visitMethod方法;在这个方法中,我们再判断一下是不是android.util.Log的相关方法,是的话,我们通过JavaContext的report()方法,将这个语句上报即可:第一个参数就是我们检测的问题的Issue对象
- 除了visitMethod外,还有很多其他的方法,如源代码的visitConstructor、visitReference等,Xml的visitElement、visitAttribute等,当然也有对应的getApplicableXxx()方法,这些都取决于你的规则的Scope
Registry
实现完一个规则后,我们需要将其Issue注册到IssueRegistry中,才会在Lint检测时应用。
public class MTIssueRegistry extends IssueRegistry {
@Override
public List<Issue> getIssues() {
return Arrays.asList(LogUsageDetector.ISSUE);
}
}
如何将自定义Lint规则打包、引入、应用,可以参考Android Lint基本使用和自定义规则
五.Lint检测流程
了解了Scope和Detector,下面来看看LintDriver执行检测时的流程。
Scope
先来看执行时的Project的Scope是什么:
private val projectRoots: Collection<Project>
init {
projectRoots = request.getProjects() ?: computeProjects(request.files)
}
var scope: EnumSet<Scope> = request.getScope() ?: Scope.infer(projectRoots)
- Project就是我们执行lint的gradle命令时的Project,如app:lintDebug中的app,这是在LintRequest创建时设置的
- 而Scope默认情况下,由于LintRequest中没有指定Scope,所以会通过Project中的所有待检测的文件,汇总出来的:
fun infer(projects: Collection<Project>?): EnumSet<Scope> {
//...
var scope = EnumSet.noneOf(Scope::class.java)
for (project in projects) {
//subset是Project里的其他自定义文件(不是文件夹里的文件),一般为null
val subset = project.subset
if (subset != null) {
for (file in subset) {
val name = file.name
if (name == ANDROID_MANIFEST_XML) {
scope.add(MANIFEST)
} else if (name.endsWith(DOT_XML)) {
scope.add(RESOURCE_FILE)
} else if (name.endsWith(DOT_JAVA) || name.endsWith(DOT_KT)) {
scope.add(JAVA_FILE)
}//...
}
} else {
//default is all scope
scope = Scope.ALL
break
}
}
return scope
}
会根据Project里所有的文件名决定有哪些Scope需要检测,默认情况下,Project的subset为null,所以Scope为ALL,会检测所有Detector
analyze()
fun analyze() {
val projects = projectRoots
//解析Project中自定义的规则集
registerCustomDetectors(projects)
try {
for (project in projects) {
val main = request.getMainProject(project)
//解析该Project中所有需要的Detector
computeDetectors(project)
//检查所有的Detector规则
checkProject(project, main)
//...
}
}//...
}
1.解析自定义规则
private fun registerCustomDetectors(projects: Collection<Project>) {
val jarFiles = Sets.newHashSet<File>()
for (project in projects) {
//project中引入的lint.jar
jarFiles.addAll(client.findRuleJars(project))
//...
}
//本地android环境的lint.jar
jarFiles.addAll(client.findGlobalRuleJars())
if (!jarFiles.isEmpty()) {
//解析所有jar的IssueRegistry类
val extraRegistries = JarFileIssueRegistry.get(
client, jarFiles,
currentProject ?: projects.firstOrNull()
)
if (extraRegistries.isNotEmpty()) {
val registries = ArrayList<IssueRegistry>(jarFiles.size + 1)
//将自定义Registry和android默认的Registry统一包装
registries.add(registry)
for (extraRegistry in extraRegistries) {
registries.add(extraRegistry)
}
registry = CompositeIssueRegistry(registries)
}
}
}
- 我们引入的aar中的lint.jar,会放入build/intermediates/lint_jar/global/prepareLintJar/lint.jar中,在LintPreVariantTask初始化配置时,会创建VariantInputs,内部会记录下这个文件夹路径作为input的一部分:
allInputs.from(
//build/intermediates/lint_jar/global/prepareLintJar/lint.jar
localLintJarCollection =
variantScope
.getGlobalScope()
.getArtifacts()
.getFinalArtifactFiles(LINT_JAR));
在findRuleJars方法中,会从中找所有lint.jar:
@Override
public List<File> findRuleJars(@NonNull Project project) {
return variantInputs.getRuleJars();
}
@Override
public List<File> getRuleJars() {
if (lintRuleJars == null) {
lintRuleJars =
Streams.concat(
dependencyLintJarCollection.getFiles().stream(),
localLintJarCollection.getFiles().stream())
.filter(File::isFile)
.collect(Collectors.toList());
}
return lintRuleJars;
}
- 除了依赖中的jar,lint还会从本地android环境的lint目录下寻找jar文件:~/.android/lint/xxx.jar
open fun findGlobalRuleJars(): List<File> {
var files: MutableList<File>? = null
try {
val androidHome = AndroidLocation.getFolder()
val lint = File(androidHome + File.separator + "lint")
if (lint.exists()) {
val list = lint.listFiles()
if (list != null) {
for (jarFile in list) {
if (endsWith(jarFile.name, DOT_JAR)) {
if (files == null) {
files = ArrayList()
}
files.add(jarFile)
}
}
}
}
}...
return if (files != null) files else emptyList()
}
- JarFileIssueRegistry.get()这个方法,会从jar文件的MANIFEST配置中,读取IssueRegistry类名,进行反射创建:

- 最后,将Android自带IssueRegistry和所有自定义IssueRegistry封装为一个CompositeIssueRegistry对象,保证可以应用所有规则
2.解析所有Detector
private fun computeDetectors(project: Project) {
//...
val map = EnumMap<Scope, MutableList<Detector>>(Scope::class.java)
scopeDetectors = map
applicableDetectors = registry.createDetectors(client, configuration, scope, platforms, map)
}
- scopeDetectors:记录每一种Scope和其对应的Detector关系
- applicableDetectors:记录所有需要检测的Detector
internal fun createDetectors(
client: LintClient,
configuration: Configuration,
scope: EnumSet<Scope>,
platforms: EnumSet<Platform>,
scopeToDetectors: MutableMap<Scope, MutableList<Detector>>?
): List<Detector> {
//1.根据此次检测的Scope,筛选出需要的所有Issue对象
val issues = getIssuesForScope(scope)
val detectorClasses = HashSet<Class<out Detector>>()
val detectorToScope = HashMap<Class<out Detector>, EnumSet<Scope>>()
for (issue in issues) {
val implementation = issue.implementation
var detectorClass: Class<out Detector> = implementation.detectorClass
val issueScope = implementation.scope
if (!detectorClasses.contains(detectorClass)) {
//2.记录所有Detector的Class对象
detectorClasses.add(detectorClass)
}
if (scopeToDetectors != null) {
//3.记录每个Detector对应的所有的Scope
val s = detectorToScope[detectorClass]
if (s == null) {
detectorToScope[detectorClass] = issueScope
} else if (!s.containsAll(issueScope)) {
val union = EnumSet.copyOf(s)
union.addAll(issueScope)
detectorToScope[detectorClass] = union
}
}
}
val detectors = ArrayList<Detector>(detectorClasses.size)
for (clz in detectorClasses) {
try {
//4.反射创建Detector对象
val detector = clz.newInstance()
detectors.add(detector)
//5.记录每个Scope对应的所有Detector实例,放入传进来的Map
if (scopeToDetectors != null) {
val union = detectorToScope[clz] ?: continue
for (s in union) {
var list: MutableList<Detector>? = scopeToDetectors[s]
if (list == null) {
list = ArrayList()
scopeToDetectors[s] = list
}
list.add(detector)
}
}
}...
}
//6.返回所有Detector实例对象
return detectors
}
- 首先根据当前Scope,筛选出所有需要的Issue:
protected open fun getIssuesForScope(scope: EnumSet<Scope>): List<Issue> {
var list: List<Issue>? = scopeIssues[scope]
if (list == null) {
//issues就是CompositeIssueRegistry里的所有Issues
val issues = issues
//如果Scope是ALL,那么所有Issue都要检测
if (scope == Scope.ALL) {
list = issues
} else {
list = ArrayList(getIssueCapacity(scope))
for (issue in issues) {
//只检测能匹配上Scope的Issue
if (issue.implementation.isAdequate(scope)) {
list.add(issue)
}
}
}
scopeIssues[scope] = list
}
return list
}
前面说过,每个Issue会指定一个Implementation对象,包含了改规则的Class、Scope等信息,他的isAdequate()方法,用来判断改规则的Scope是否能匹配上当前Project的Scope:
public boolean isAdequate(@NonNull EnumSet<Scope> scope) {
//规则的Scope是Project的Scope的子集
if (scope.containsAll(this.scope)) {
return true;
}
//创建Implementation时可以额外传入多组Scope作为analysisScope
if (analysisScopes != null) {
for (EnumSet<Scope> analysisScope : analysisScopes) {
//也需要为Project的Scope的子集
if (scope.containsAll(analysisScope)) {
return true;
}
}
}
return false;
}
这里有一个坑点:上面说过,如果Project的subset为null,那么Scope为ALL;如果不为null,即为subset里包含的所有文件类型的Scope集合。此时假设有一个Detector是要检测多种文件,比如java+xml,那么其Scope为JAVA+RESOURCE,而如果subset里只有其中一种类型的文件,那么第一条规则就匹配失败,即Detector的Scope并不是Project的Scope的子集,所以该Detector(Issue)就会被忽略。这种情况下,我们可以在设置Issue的Implementation对象时,除了传入Scope外,再额外传入每种文件类型的Scope作为analysisScope,这样就可以满足第二个条件,应用Issue。new Implementation(XxxDetector.class, Scope.JAVA_AND_RESOURCE_FILES, Scope.JAVA_FILE_SCOPE, Scope.RESOURCE_FILE_SCOPE)
- 遍历所有的Issue,收集其Detector的Class对象,并保存Class与其对应的所有Scope的映射关系;这里需要注意,不同Issue可以为相同的Detector设置不同的Scope,所以需要遍历所有Issue才知道一个Detector对应的所有Scope
- 然后通过反射创建每一个Detector的实例,并保存每一个Scope对应的所有Detector实例,放入传入的Map中scopeDetectors,后续检测使用;最后返回所有的Detector实例applicableDetectors
3.Lint规则检查
收集完所有Scope及其对应的Detector后,就可以正式开始检查了
private fun checkProject(project: Project, main: Project) {
//创建并记录所有的Project、Library依赖以及相关的Context对象
val projectDir = project.dir
val projectContext = Context(this, project, null, projectDir)
val allLibraries = project.allLibraries
val allProjects = HashSet<Project>(allLibraries.size + 1)
allProjects.add(project)
allProjects.addAll(allLibraries)
currentProjects = allProjects.toTypedArray()
currentProject = project
//调用每个Detector的检查前方法
for (check in applicableDetectors) {
check.beforeCheckRootProject(projectContext)
check.beforeCheckEachProject(projectContext)
//...
}
//检查project
runFileDetectors(project, main)
//检查所有Library依赖
if (checkDependencies && !Scope.checkSingleFile(scope)) {
val libraries = project.allLibraries
for (library in libraries) {
val libraryContext = Context(this, library, project, projectDir)
currentProject = library
//检查前方法
for (check in applicableDetectors) {
check.beforeCheckEachProject(libraryContext)
//...
}
//检查Library
runFileDetectors(library, main)
//检查后方法
for (check in applicableDetectors) {
check.afterCheckEachProject(libraryContext)
//...
}
}
}
currentProject = project
//检查后方法
for (check in applicableDetectors) {
client.runReadAction(Runnable {
check.afterCheckEachProject(projectContext)
check.afterCheckRootProject(projectContext)
})
//...
}
}
- 当前Project及其所依赖的Library(也是一个Project)都会被检测,检测的核心方法为runFileDetectors()
- Project检测前后,每个Detector都会有一个回调方法被调用
runFileDetectors()
我们先整体看一下这个方法的执行流程:
private fun runFileDetectors(project: Project, main: Project?) {
//Android module才会检测Manifest和Resource
if (project.isAndroidProject) {
for (manifestFile in project.manifestFiles) {
//解析并检测Manifest.xml
}
//检测所有资源文件和文件夹
if (scope.contains(Scope.ALL_RESOURCE_FILES) ||
scope.contains(Scope.RESOURCE_FILE) ||
scope.contains(Scope.RESOURCE_FOLDER) ||
scope.contains(Scope.BINARY_RESOURCE_FILE)
) {//...
}
}
//检测Java、Kotlin源代码
if (scope.contains(Scope.JAVA_FILE) || scope.contains(Scope.ALL_JAVA_FILES)) {//...
}
//检测CLASS文件
if (scope.contains(Scope.CLASS_FILE) ||
scope.contains(Scope.ALL_CLASS_FILES) ||
scope.contains(Scope.JAVA_LIBRARIES)
) {//...
}
//检测gradle文件
if (scope.contains(Scope.GRADLE_FILE)) {//...
}
//检测其他未知文件
if (scope.contains(Scope.OTHER)) {
}
//检测proguard文件
if (project === main && scope.contains(Scope.PROGUARD_FILE) &&
project.isAndroidProject
) {//...
}
//检测.properties文件
if (project === main && scope.contains(Scope.PROPERTY_FILE)) {//...
}
}
这个方法会根据Scope和要检测的Project类型,进行相应类型的检测,这里我们主要说一下资源文件和源码文件的检测流程
(1)AndroidManifest.xml
//收集Project中所有AndroidManifest.xml文件
for (manifestFile in project.manifestFiles) {
//Xml解析器
val parser = client.xmlParser
//创建Context,解析File
val context = createXmlContext(project, main, manifestFile, null, parser)
//获取所有检测MANIFEST这个Scope的Detector
val detectors = scopeDetectors[Scope.MANIFEST]
if (detectors != null) {
//Detector必须为XmlScanner接口类型
val xmlDetectors = ArrayList<XmlScanner>(detectors.size)
for (detector in detectors) {
if (detector is XmlScanner) {
xmlDetectors.add(detector)
}
}
//创建Visitor
val v = ResourceVisitor(parser, xmlDetectors, null)
//开始检查规则
v.visitFile(context)
}
}
- 首先会从Project目录里寻找所有的AndroidManifest.xml文件
- 然后创建XmlContext,传入File,Lint的Xml解析器会将File解析为Document对象
- 从Detector中收集所有Scope.MANIFEST对应的Detector,且必须为XmlScanner类型(这也是上面说的为啥要实现对应的接口)
- 创建Visitor对象,在其构造方法中,会根据Detector的getApplicableAttributes()、getApplicableElements()方法返回的具体检查属性/元素,将所有Detector整理为两个Map:
<Attribute, List> <Element, List>
for (XmlScanner detector : allDetectors) {
XmlScanner xmlDetector = (XmlScanner) detector;
Collection<String> elements = xmlDetector.getApplicableElements();
for (String element : elements) {
List<XmlScanner> list = elementToCheck.get(element);
if (list == null) {
list = new ArrayList<>();
elementToCheck.put(element, list);
}
list.add(xmlDetector);
}
//attribute
}
- 最后调用Visitor的visitFile()方法开始检查,由于已经整理好了Detector的分类,检测起来就很快了:
void visitFile(@NonNull XmlContext context) {
try {
//检查前回调
for (XmlScanner check : allDetectors) {
check.beforeCheckFile(context);
check.visitDocument(context, context.document);
}
//检查element和attribute
if (!elementToCheck.isEmpty()
|| !attributeToCheck.isEmpty()
|| !allAttributeDetectors.isEmpty()
|| !allElementDetectors.isEmpty()) {
visitElement(context, context.document.getDocumentElement());
}
//检查后回调
for (XmlScanner check : allDetectors) {
check.afterCheckFile(context);
}
}...
}
private void visitElement(@NonNull XmlContext context, @NonNull Element element) {
//拿到element对应的Detector
List<XmlScanner> elementChecks = elementToCheck.get(element.getLocalName());
if (elementChecks != null) {
//调用Detector的visitElement
for (XmlScanner check : elementChecks) {
check.visitElement(context, element);
}
}
//同理检查element的每一个attribute
if (!attributeToCheck.isEmpty() || !allAttributeDetectors.isEmpty()) {
NamedNodeMap attributes = element.getAttributes();
for (int i = 0, n = attributes.getLength(); i < n; i++) {
Attr attribute = (Attr) attributes.item(i);
String name = attribute.getLocalName();
//拿到attribute对应的Detector
List<XmlScanner> list = attributeToCheck.get(name);
if (list != null) {
//调用Detector的visitAttribute
for (XmlScanner check : list) {
check.visitAttribute(context, attribute);
}
}
}
}
//递归检查element的子element
NodeList childNodes = element.getChildNodes();
for (int i = 0, n = childNodes.getLength(); i < n; i++) {
Node child = childNodes.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE) {
visitElement(context, (Element) child);
}
}...
}
(2)Resource .xml
再来看资源文件如layout.xml、values.xml等文件的检测
if (scope.contains(Scope.RESOURCE_FILE) ||
scope.contains(Scope.RESOURCE_FOLDER)
) {
//取出Scope对应的Detector
val dirChecks = scopeDetectors[Scope.RESOURCE_FOLDER]
val checks = scopeDetectors[Scope.RESOURCE_FILE]
var haveXmlChecks = !checks.isEmpty()
val xmlDetectors: MutableList<XmlScanner> = mutableListOf()
if (haveXmlChecks) {
xmlDetectors = ArrayList(checks.size)
for (detector in checks) {
//Detector必须是XmlScanner类型
if (detector is XmlScanner) {
xmlDetectors.add(detector)
}
}
haveXmlChecks = !xmlDetectors.isEmpty()
}
if (haveXmlChecks ||
dirChecks != null && !dirChecks.isEmpty()
) {
//优先检测Project的subset
val files = project.subset
if (files != null) {
checkIndividualResources(
project, main, xmlDetectors, dirChecks,
binaryChecks, files
)
} else {//否则检查Project的resources folder
val resourceFolders = project.resourceFolders
if (!resourceFolders.isEmpty()) {
for (res in resourceFolders) {
checkResFolder(
project, main, res, xmlDetectors, dirChecks,
binaryChecks
)
}
}...
}
}
}
这里的检测流程与Manifest大体一致,不同的是检查的目标文件,优先取Project的subset文件集,这个字段一般是null,通常我们可以添加一些Project外的文件到subset进行检测,但是这样的话就不会检测Project的默认资源文件夹下的文件;而默认检测的就是Project的资源文件夹下的文件
i.subset检测
private fun checkIndividualResources(
project: Project,
main: Project?,
xmlDetectors: List<XmlScanner>,
dirChecks: List<Detector>?,
binaryChecks: List<Detector>?,
files: List<File>
) {
for (file in files) {
if (file.isDirectory) {
val type = ResourceFolderType.getFolderType(file.name)
//res/下的目录
if (type != null && File(file.parentFile, RES_FOLDER).exists()) {
checkResourceFolder(
project, main, file, type, xmlDetectors, dirChecks,
binaryChecks
)
}...
} else if (file.isFile && isXmlFile(file)) {//检测xml文件
val folderName = file.parentFile.name
val type = ResourceFolderType.getFolderType(folderName)
if (type != null) {
//visitor
val visitor = getVisitor(type, xmlDetectors, binaryChecks)
if (visitor != null) {
//parser
val parser = visitor.parser
//context
val context = createXmlContext(project, main, file, type, parser)
visitor.visitFile(context)
}
}
}...
}
}
- 循环subset的文件,如果是文件夹,则走checkResourceFolder()方法检测;如果是xml文件,则和manifest检测一样,走上述xml的检测流程
- 检测文件夹的话,其实本质差不多,先调用检测文件夹的Detector,然后遍历文件夹里面的所有xml文件,进行xml文件的检测
private fun checkResourceFolder(
project: Project,
main: Project?,
dir: File,
type: ResourceFolderType,
xmlChecks: List<XmlScanner>,
dirChecks: List<Detector>?,
binaryChecks: List<Detector>?
) {
//检测dir
if (dirChecks != null && !dirChecks.isEmpty()) {
val context = ResourceContext(this, project, main, dir, type, "")
val folderName = dir.name
for (check in dirChecks) {
if (check.appliesTo(type)) {
check.beforeCheckFile(context)
check.checkFolder(context, folderName)
check.afterCheckFile(context)
}
}
}
//检测files
val files = dir.listFiles()
val visitor = getVisitor(type, xmlChecks, binaryChecks)
if (visitor != null) {
val parser = visitor.parser
for (file in files) {
if (isXmlFile(file)) {
val context = createXmlContext(project, main, file, type, parser) ?: continue
visitor.visitFile(context)
}...
}
}
}
ii.资源文件夹检测
其实就是一个文件夹的检测的流程,最终也是checkResourceFolder()
private fun checkResFolder(
project: Project,
main: Project?,
res: File,
xmlChecks: List<XmlScanner>,
dirChecks: List<Detector>?,
binaryChecks: List<Detector>?
) {
val resourceDirs = res.listFiles() ?: return
//遍历res/下的所有dir
for (dir in resourceDirs) {
val type = ResourceFolderType.getFolderType(dir.name)
if (type != null) {
//进行dir的检测
checkResourceFolder(project, main, dir, type, xmlChecks, dirChecks, binaryChecks)
}
}
}
(3)Java/Kotlin
我们再来看源代码的检测流程
if (scope.contains(Scope.JAVA_FILE)) {
val checks = scopeDetectors[Scope.JAVA_FILE]
if (checks != null && !checks.isEmpty()) {
val files = project.subset
if (files != null) {
checkIndividualJavaFiles(project, main, checks, files)
} else {
val sourceFolders = project.javaSourceFolders
checkJava(project, main, sourceFolders, testFolders, generatedFolders, checks)
}
}
}
可以看到,Detector的筛选,subset的使用,和资源文件如出一辙,我们直接来看检测方法:收集完所有的.java/.kt文件后,最终会调用到visitJavaFiles()方法
private fun visitJavaFiles(
checks: List<Detector>,
project: Project,
main: Project?,
allContexts: List<JavaContext>,
srcContexts: List<JavaContext>,
testContexts: List<JavaContext>
) {
//收集Detector,必须为SourceCodeScanner类型
val uastScanners = ArrayList<Detector>(checks.size)
for (detector in checks) {
if (detector is SourceCodeScanner) {
uastScanners.add(detector)
}
}
if (!uastScanners.isEmpty()) {
//Uast Parser解析器
val parser = client.getUastParser(currentProject)
//每个JavaContext都对用一个源代码文件(.java/.kt)
for (context in allContexts) {
context.uastParser = parser
}
//Visitor
val uElementVisitor = UElementVisitor(parser, uastScanners)
for (context in srcContexts) {
//检测文件
uElementVisitor.visitFile(context)
}
}
}
- 这里的Detector是SourceCodeScanner类型
- 此处的Parser是Uast依赖提供的,这个库是专门用于将源代码文件解析为抽象语法树(AST)的
- 每一个JavaContext都对应一个.java/.kt文件
UIElementVisitor
fun visitFile(context: JavaContext) {
try {
val uastParser = context.uastParser
//Uast Parser将File解析为一个UFile对象
val uFile = uastParser.parse(context)
val client = context.client
try {
for (v in allDetectors) {
//检查前调用
v.detector.beforeCheckFile(context)
}
//检查superclass的Detector
if (!superClassDetectors.isEmpty()) {
val visitor = SuperclassPsiVisitor(context)
uFile.accept(visitor)
}
//检查method、field、construct等
if (!methodDetectors.isEmpty() ||
!resourceFieldDetectors.isEmpty() ||
!constructorDetectors.isEmpty() ||
!referenceDetectors.isEmpty() ||
annotationHandler != null
) {
val visitor = DelegatingPsiVisitor(context)
uFile.accept(visitor)
}//...
for (v in allDetectors) {
v.detector.afterCheckFile(context)
}
}//...
}//...
}
可以看到,具体检测流程和检测xml也是类似的,交由不同的Visitor进行检测:
SuperclassPsiVisitor
var list: List<VisitingDetector>? = superClassDetectors[cls.qualifiedName]
if (list != null) {
for (v in list) {
val uastScanner = v.uastScanner
if (uClass != null) {
uastScanner.visitClass(context, uClass)
} else {
uastScanner.visitClass(context, lambda!!)
}
}
}
DelegatingPsiVisitor
val methodName = node.methodName
if (methodName != null) {
val list = methodDetectors[methodName]
if (list != null) {
val function = node.resolve()
if (function != null) {
for (v in list) {
val scanner = v.uastScanner
scanner.visitMethodCall(mContext, node, function)
}
}
}
}
可以看到,当识别到superclass、method等,需要调用Detector时,Visitor会从一个Map里直接找到相应的Detector集合进行直接调用,这是不是时曾相识?没错,和检测xml一样,在创建Visitor时,构造方法中就会将Detector,依据getApplicableXxx()进行分类存储:
UElementVisitor
val names = detector.getApplicableMethodNames()
if (names != null) { for (name in names) {
val list = methodDetectors.computeIfAbsent(name) { ArrayList(SAME_TYPE_COUNT) }
list.add(v)
}
}
val applicableSuperClasses = detector.applicableSuperClasses()
if (applicableSuperClasses != null) {
for (fqn in applicableSuperClasses) {
val list =
superClassDetectors.computeIfAbsent(fqn) { ArrayList(SAME_TYPE_COUNT) }
list.add(v)
}
}
Java和Kotlin的源码检测,是将源码解析为AST抽象语法树,使用的是Uast依赖,具体就不展开讲了https://github.com/JetBrains/intellij-community/tree/master/uast
其他类型的Scope检测也都大同小异,也就不赘述了
Report/Baseline
当我们在Detector中要上报一个错误时,会调用JavaContext的report方法,传入Issue和Location以及提示message:
context.report(ISSUE, declaration, context.getLocation(locationNode),"message");
最终会调到LintClientWrapper的report()方法:
override fun report(
context: Context,
issue: Issue,
severity: Severity,
location: Location,
message: String,
format: TextFormat,
fix: LintFix?
) {
//ignore级别的Issue不上报
if (severity == Severity.IGNORE) {
return
}
//过滤掉baseline中已有的case
val baseline = baseline
if (baseline != null) {
val filtered = baseline.findAndMark(
issue, location, message, severity,
context.project
)
if (filtered) {
return
}
}
//正常上报
delegate.report(context, issue, severity, location, message, format, fix)
}
baseline会作为一个File,在我们gradle中的lintOptions{}中指定,里面内容的格式与上报时的一致:
<issues format="5" by="lint 3.5.3">
<issue
id="LogUsage"
severity="Fatal"
message="请勿直接调用android.util.Log"
<location
file="/Users/cwj/AndroidStudioProjects/SDK/services/liveui/src/main/java/com/android/live/core/widget/LiveAutoRTLTextView.java"
line="24"
column="9"/>
</issue>
</issues>
这里有一个步骤是检测要上报的case是否在baseline中:
findAndMark():
private fun findAndMark(
issue: Issue,
location: Location,
message: String,
severity: Severity?
): Boolean {
//根据message提示消息找出baseline中的所有case
val entries = messageToEntry.get(message)
if (entries == null || entries.isEmpty()) {
return false
}
val file = location.file
val path = file.path
val issueId = issue.id
for (entry in entries) {
//如果case的Issue Id相等
if (entry!!.issueId == issueId) {
if (isSamePathSuffix(path, entry.path)) {//且case的Location的file一样
//则认为是baseline中的一条,就忽略上报
var curr = entry
while (curr.previous != null) {
curr = curr.previous
}
while (curr != null) {
//移除baseline中这条存在的case,继续比较
messageToEntry.remove(curr.message, curr)
curr = curr.next
}
return true
}
}
}
return false
}
- messageToEntry是baseline在初始化时,读取文件,将每个baseline文件中的case包装为一个Entry对象,key为case的message
- 在上报某个case时,比对的方案是按照message、Issue Id以及file的path做比较,这就有个很大的问题,同一个文件的同一种Issue检测出来的问题,都会被认为是同一个,这样就会造成:如果baseline里A文件有两b问题的case,然后在A文件里又新增了一个b问题的case,这样在检测A文件时,如果新增的这个case的率先被检测到,就会被认为是baseline里已有的问题忽略掉,而baseline里真正有的case可能会被报出来,也可以说是按个数抵消的检测方式,误差很大。。。
如果baseline没有命中,则会调用到LintCliClient的report()方法:
public void report(
@NonNull Context context,
@NonNull Issue issue,
@NonNull Severity severity,
@NonNull Location location,
@NonNull String message,
@NonNull TextFormat format,
@Nullable LintFix fix) {
//记录每种错误的count
if (severity.isError()) {
hasErrors = true;
errorCount++;
} else if (severity == Severity.WARNING) {
warningCount++;
}
//放入warnings列表
message = format.convertTo(message, TextFormat.RAW);
Warning warning = new Warning(issue, message, severity, context.getProject());
warnings.add(warning);
//设置Warning的Location
}
在LintDriver的analyze()执行完后,所有的case都会被记录在warnings里,只需要调用每种Report方法进行写文件即可:
LintBaseline baseline = driver.getBaseline();
LintStats stats = LintStats.Companion.create(warnings, baseline);
//HtmlReporter/XmlReporter/TextReporter
for (Reporter reporter : flags.getReporters()) {
reporter.write(stats, warnings);
}
最后,就是我们上面说到的,如果有错误输出,就会抛出一个GradleException终止命令;否则就正常结束Task:
if (report && client.haveErrors() && flags.isSetExitCode()) {
abort(client, warnings.getFirst(), isAndroid);
}
private void abort(...) {
//...
throw new GradleException(message);
}
至此,整个Lint检测流程就结束了~
六.问题
通过上面的Lint执行流程我们可以发现,Lint检测文件,默认是取自project的javaSourceFolders和resourceFolders,也就是说会检测Project里所有的文件,这样的话可能会造成检测时间很长的问题,那我们能不能做到只检测改动文件的增量检测呢?请参考增量Lint检测实现原理