增量Lint检测实现原理

Lint实现原理里已经知道,Lint检测的文件,默认是Project的javaSourceFolders和resourceFolders,但是这样会造成每次Lint检测的时间很长,我们pipeline的效率就很低;所以我们设想要做到一种增量检查:每次只检查改动的文件

一.基本思路

我们先回顾一下获取要检测文件的方式:

val files = project.subset
if (files != null) {
    checkIndividualJavaFiles(project, main, checks, files)
} else {
    val sourceFolders = project.javaSourceFolders
    checkJava(project, main, sourceFolders, testFolders, generatedFolders, checks)
}

可以发现,在取Project的代码目录前,会优先判断其subset这个List有没有值,有的话,就只检测这些文件,所以我们可以得出结论:将我们改动的文件,放到Project的subset字段里即可
在此,我们需要实现一个自定义gradle插件来实现这一套功能

二.实现

1.自定义LintClient

那Project在哪里可以获取到呢?这里要注意,检测时使用的Project,是Lint里的com.android.tools.lint.detector.api.Project,不是gradle的Project:

@Override
protected LintRequest createLintRequest(@NonNull List<File> files) {
    LintRequest lintRequest = new LintRequest(this, files);
    LintGradleProject.ProjectSearch search = new LintGradleProject.ProjectSearch();
    Project project =
            search.getProject(this, gradleProject, variant != null ? variant.getName() : null);
    lintRequest.setProjects(Collections.singletonList(project));
    //...
    return lintRequest;
}

往上倒可以发现,Project是在创建LintRequest时创建的,只有拿到LintRequest,才可以获取Project
LintCliClient:

public int run(@NonNull IssueRegistry registry, @NonNull List<File> files) throws IOException {
    //...
    LintRequest lintRequest = createLintRequest(files);
}

可以发现,createLintRequest()方法是LintCliClient的一个重写方法,而LintCliClient在Lint里的实现是LintGradleClient类,那么我们可以继承LintGradleClient写一个类,重写createLintRequest()方法,就可以拿到LintRequest对象了:

public class IncrementLintClient extends LintGradleClient {
    private final org.gradle.api.Project gradleProject;
    private IncrementLintExtension extension;
    public IncrementLintClient(...) {
        super(version, registry, flags, gradleProject, sdkHome, variant, variantInputs, buildToolInfoRevision, isAndroid, baselineVariantName);
        this.gradleProject = gradleProject;
        this.extension = extension;
    }

    @Override
    protected LintRequest createLintRequest(List<File> files) {
        LintRequest lintRequest = super.createLintRequest(files);
        if (lintRequest != null) {
            Collection<Project> projects = lintRequest.getProjects();
            if (projects != null) {
                for (Project project : projects) {
                    try {
                        addFiles(project);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return lintRequest;
    }
}

2.增量检查文件

拿到LintRequest,从而拿到Project,就可以向其添加待检测的File对象,那么这些文件我们如何获取呢?
我们可以写一个本地文本文件,以某种方式在Lint检查开始前,将待检测的文件路径写入到这个文件,然后在添加subset时,读取这个文件里的内容就好:
在这里插入图片描述
那么这些文件路径怎么获取呢,这里可以简单举个例子:比如我们是在提交了一个MR,跑pipeline时进行Lint检查时,我们可以添加另一个Task,这个Task是请求MR的相关api,获取此次MR的diff改动文件路径,然后写入到commitFiles.txt这个文本文件即可,当然要记得让Lint的Task依赖于这个Task先执行
有了这个文本文件,我们首先应该在gradle插件的extension里指定一个变量,设置这个文件的名字:

open class IncrementLintExtension {
    var recordPath: String? = ''
}

这样我们可以在配置插件时设置:

apply plugin: 'com.android.incrementlint'
IncrementLint {
    recordPath = 'commitFiles.txt'
}

然后我们添加subset:

private void addFiles(Project project) throws IOException {
    File file = new File(gradleProject.getRootDir(), extension.getRecordPath());
    if (file.exists()) {
        FileInputStream inputStream = null;
        BufferedReader bufferedReader = null;
        try {
            inputStream = new FileInputStream(file);
            bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
            //读取文本文件
            String lineStr;
            while ((lineStr = bufferedReader.readLine()) != null) {
                //将File添加到project的subset
                File commitFile = new File(lineStr);
                if (commitFile.exists() && shouldCheck(commitFile)) {
                    project.addFile(commitFile);
                }
            }
            //如果一个增量文件都没有,添加一个占位的文件防止走全量lint检查
            if (project.getSubset() == null || project.getSubset().isEmpty()) {
                project.addFile(new File("NotLintCheckStub.file"));
            }
        }...
    }
    //manifest的特殊处理
    List<File> manifestFiles = new ArrayList<>();
    if (project.getSubset() != null && !project.getSubset().isEmpty()) {
        for (File f : project.getSubset()) {
            if (f.exists() && SdkConstants.ANDROID_MANIFEST_XML.equals(f.getName())) {
                manifestFiles.add(f);
            }
        }
    }
    if (!manifestFiles.isEmpty()) {
        Field field;
        try {
            field = Project.class.getDeclaredField("manifestFiles");
            field.setAccessible(true);
            field.set(project, manifestFiles);
        }...
    }
}

这里的逻辑很简单,就是读取文本文件,每一行是一个File对象,添加到subset中即可
这里有一个针对AndroidManifest.xml的特殊处理:把要检测的AndroidManifest.xml文件,通过反射放到Project的manifestFiles字段,才可以检测到这些文件
这样的原因是在LintDriver进行检测时,会调用的project.getManifestFiles()方法获取默认的AndroidManifest.xml文件进行检测:

for (manifestFile in project.manifestFiles) {
    //...
}

@Override
public List<File> getManifestFiles() {
    if (manifestFiles == null) {
        manifestFiles = Lists.newArrayList();
        //每个variant的AndroidManifest.xml
        for (SourceProvider provider : getSourceProviders()) {
            File manifestFile = provider.getManifestFile();
            if (manifestFile.exists()) {
                manifestFiles.add(manifestFile);
            }
        }
    }
    return manifestFiles;
}

而在project文件夹外的AndroidManifest.xml文件就不会被检测,所以需要手动通过反射将我们要检测的AndroidManifest.xml设置到manifestFiles这个字段里

3.其他自定义类

实现了自定义的LintClient,还需要看原生Lint是如何构建LintGradleClient的
LintGradleExecution:

private Pair<List<Warning>, LintBaseline> runLint(...) {
    LintGradleClient client = new LintGradleClient(...);
}

是一个private方法,直接new了LintGradleClient,而我们是继承自LintGradleClient的子类,所以需要copy一份LintGradleExecution:

private Pair<List<Warning>, LintBaseline> runLint(...) {
    IncrementLintClient client = new IncrementLintClient(...);
}

再看LintGradleExecution是怎么被调用的
ReflectiveLintRunner:

fun runLint(...) {
    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)
    }...
}

是通过被反射调用的,而且runLint方法不能被重写,所以我们也需要copy一份LintRunner进行改动:

void runLint(...) {
    try {
        //...
        Class cls = loader.loadClass("com.android.incrementlint.IncrementLintGradleExecution");
        //...
        Method analyzeMethod = driver.getClass().getDeclaredMethod("analyze");
        analyzeMethod.invoke(driver);
    }...
}

最后再来看ReflectiveLintRunner怎么被调用的
LintBaseTask:

protected void runLint(LintBaseTaskDescriptor descriptor) {
    FileCollection lintClassPath = getLintClassPath();
    if (lintClassPath != null) {
        new ReflectiveLintRunner().runLint(getProject().getGradle(),
                descriptor, lintClassPath.getFiles());
    }
}

我们需要继承自LintBaseTask实现一个自定义的LintTask,重写runLint方法:

protected void runLint(LintBaseTaskDescriptor descriptor) {
        FileCollection lintClassPath = getLintClassPath();
        if (lintClassPath != null) {
            new LintRunner().runLint(getProject().getGradle(), descriptor, lintClassPath.getFiles(), extension);
        }
    }

4.自定义Gradle插件

有了自定义的Task、extension,就可以通过gradle plugin将其包装,为每个variant添加LintTask,形成一个sdk了

public class IncrementLintPlugin implements Plugin<Project> {
    public final static String EXTENSION_NAME = "IncrementLint";

    @Override
    public void apply(Project project) {
        IncrementLintExtension incrementLintExtension = project.getExtensions().create(EXTENSION_NAME, IncrementLintExtension.class);
        project.afterEvaluate(p -> {
            //为application module添加LintTask
            AppExtension appExtension = p.getExtensions().findByType(AppExtension.class);
            if (appExtension != null) {
                DomainObjectSet<ApplicationVariant> variants = appExtension.getApplicationVariants();
                for (ApplicationVariant variant : variants) {
                    if (variant instanceof ApplicationVariantImpl) {
                        ApplicationVariantImpl variantImpl = (ApplicationVariantImpl) variant;
                        VariantScope globalScope = variantImpl.getVariantData().getScope();
                        applyLintTask(project, collectAllFilesTask, incrementLintExtension, globalScope);
                    }
                }
            }
            //为library module添加LintTask
            LibraryExtension libraryExtension = p.getExtensions().findByType(LibraryExtension.class);
            if (libraryExtension != null) {
                DefaultDomainObjectSet<LibraryVariant> variants = libraryExtension.getLibraryVariants();
                for (LibraryVariant variant : variants) {
                    if (variant instanceof LibraryVariantImpl) {
                        LibraryVariantImpl variantImpl = (LibraryVariantImpl) variant;
                        LibraryVariantData libraryVariantData = getVariantData(variantImpl);
                        if (libraryVariantData != null) {
                            VariantScope globalScope = libraryVariantData.getScope();
                            applyLintTask(project, collectAllFilesTask, incrementLintExtension, globalScope);
                        }
                    }
                }
            }
        });
    }

    private void applyLintTask(Project project, Task collectAllFilesTask, IncrementLintExtension incrementLintExtension, VariantScope globalScope) {
        LintTask lintTask = project.getTasks().create(globalScope.getTaskName(LintTask.NAME), LintTask.class);
        new LintTask.VitalCreationAction(globalScope, null, project, incrementLintExtension).configure(lintTask);
    }
}

gradle插件相关可以参考Gradle及插件使用


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