在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及插件使用