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) {
        //...
}
  1. create时,通过getType()返回的Class对象,反射创建Task对象,添加到TaskManager里
  2. Task的name,由scope产生,lint拼接scope的variant name,如lintMusicallyI18nDebug
  3. 最后调用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);
}
  1. 首先调用了抽象方法configureExtension(),每种Plugin进行自己的Extension初始化
  2. 把生成的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());
    }
}
  1. 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)
    }
}

这里需要说明:

  1. ReflectiveLintRunner、LintExecutionRequest这些类是属于lint-gradle-api这个依赖的
  2. 具体的实现类如LintGradleExecution,属于lint-gradle这个依赖
  3. 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;
}
  1. 创建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);
    //...
}
  1. LintCliFlags为lint执行器的配置项,只需要将LintOptions的配置项设置进来即可
  2. LintGradleClient为lint检测的执行器,继承自LintCliClient,run方法为执行lint检测的入口
  3. 检测结束后,如果有检测出来的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");
        }
    }
}
  1. 首先我们需要定义一个规则类,继承自Detector类
  2. 并且因为我们检查的是java/kotlin代码里的调用,所以需要实现Detector.UastScanner接口,表明这是一个需要源代码(java/kotlin)文件扫描的类
  3. 每一个需要检测的问题,都需要有一个上报时用到的Issue对象,用来指定这个问题的一些属性:
    1. Category.SECURITY代表这个问题的类别,有安全类别、UI类别、翻译问题等等
    2. priority代表问题的重要级别,数字(1-10)越大级别越高
    3. Severity.FATAL代表问题的重要性,FATAL为最严重,还有WARNING警告类型等,检测时会根据重要性决定是否上报等,如WARNING一般就是我们见到的报黄色的错误,ERROR和FATAL就会报红
    4. 需要定义一个Implementation对象,指明这个规则类的Class对象,用作反射创建,并指明改规则需要检测的具体文件范围(叫做Scope),这里是要检查源代码文件,所以是Scope.JAVA_FILE_SCOPE,除此之外还有很多类型:
      在这里插入图片描述
  4. 然后我们重写getApplicableMethodNames()方法,这个方法返回的是要检测的方法名字的集合,就是一个List,这里返回的是Log的相应方法
  5. 还要重写visitMethod方法,由于我们设置的该类是检测的源代码文件中,有关方法名为(v,d,i,w,e,wtf)的调用语句,所以再Lint检测到类似方法的调用时,会调用到该类的visitMethod方法;在这个方法中,我们再判断一下是不是android.util.Log的相关方法,是的话,我们通过JavaContext的report()方法,将这个语句上报即可:第一个参数就是我们检测的问题的Issue对象
  6. 除了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)
  1. Project就是我们执行lint的gradle命令时的Project,如app:lintDebug中的app,这是在LintRequest创建时设置的
  2. 而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)
        }
    }
}
  1. 我们引入的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;
}
  1. 除了依赖中的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()
}
  1. JarFileIssueRegistry.get()这个方法,会从jar文件的MANIFEST配置中,读取IssueRegistry类名,进行反射创建:
    在这里插入图片描述
  2. 最后,将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)
}
  1. scopeDetectors:记录每一种Scope和其对应的Detector关系
  2. 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
}
  1. 首先根据当前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)
  1. 遍历所有的Issue,收集其Detector的Class对象,并保存Class与其对应的所有Scope的映射关系;这里需要注意,不同Issue可以为相同的Detector设置不同的Scope,所以需要遍历所有Issue才知道一个Detector对应的所有Scope
  2. 然后通过反射创建每一个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)
        })
        //...
    }
}
  1. 当前Project及其所依赖的Library(也是一个Project)都会被检测,检测的核心方法为runFileDetectors()
  2. 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)
    }
}
  1. 首先会从Project目录里寻找所有的AndroidManifest.xml文件
  2. 然后创建XmlContext,传入File,Lint的Xml解析器会将File解析为Document对象
  3. 从Detector中收集所有Scope.MANIFEST对应的Detector,且必须为XmlScanner类型(这也是上面说的为啥要实现对应的接口)
  4. 创建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
}
  1. 最后调用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)
                }
            }
        }...
    }
}
  1. 循环subset的文件,如果是文件夹,则走checkResourceFolder()方法检测;如果是xml文件,则和manifest检测一样,走上述xml的检测流程
  2. 检测文件夹的话,其实本质差不多,先调用检测文件夹的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)
        }
    }
}
  1. 这里的Detector是SourceCodeScanner类型
  2. 此处的Parser是Uast依赖提供的,这个库是专门用于将源代码文件解析为抽象语法树(AST)的
  3. 每一个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
}
  1. messageToEntry是baseline在初始化时,读取文件,将每个baseline文件中的case包装为一个Entry对象,key为case的message
  2. 在上报某个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检测实现原理


版权声明:本文为qq_15827013原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。