1. 背景
在上一篇Android Lint代码检查实践中说到了Lint全量扫描项目的耗时在3.5m,执行时机是在mr的时候,所以在大多数时候,不会因为Lint检查阻塞开发流程。
但是,特殊情况下,比如你只提交了几行代码需要mr的时候,review只需要10秒完事了,而Lint检查却需要3.5m,这个时候你就需要浪费宝贵的3分多钟进行等待,这种事情是我不希望看到的,本着对极致的追求,我决定支持增量扫描的功能,来压缩Lint扫描的时间。
老规矩先看下成果,不然被我骗着做完后发现没卵用怎么办,滑稽脸。
效果是不是很显著,从3分30秒压缩到了30秒左右,23333成功勾起了各位看官的欲望,接下来看看怎么实现的吧。
先来波广告上篇文章中所有功能和本篇的增量扫描都在AndroidLint实现了,欢迎star。
2. 怎么做
2.1 如何找到变更文件
先从简单的入手,对于找到变更文件我们可以通过git diff命令,git diff支持两个分支或者不同commit节点等方式对比修改。我这里的命令是git diff $baseline $revision --name-only --diff-filter=ACMRTUXB
,--name-only
是只展示文件名,--diff-filter=ACMRTUXB
是用来过滤掉删除的文件只要增改的文件,其他的不过多赘述官方文档有详细说明。
2.2 增量扫描思路的形成
变更文件找到了,接下来是做Lint增量扫描,首先想到的肯定是去看LintOptions有没有提供这个功能,很遗憾没有,那也就意味着需要自定义了,但我们并不知道怎么起手,所以就只能先看看AGP提供的LintTask怎么做的。最好情况是他有提供这个功能只是没有开放api给我们,那反射反射程序员的快乐一下就O了,最差的就是得照着他源码自己手撸一套有增量扫描功能的LintTask,这里我直接给出结果,AGP提供的LintTask有这个功能只是没开放api。
接下来我们将debug一遍LintTask执行流程,看看如何开启增量扫描功能,(ps:源码全贴的话特别多不容易抓住重点,所以非重点的就直接展示调用流程了,重点的地方在贴源码)
2.3 Debug Lint Task
LintTask默认有三个实现类
不管哪个都是调到LintBaseTask#runLint()进行lint扫描,具体调用流程如下:
LintBaseTask#runLint()
ReflectiveLintRunner#runLint()
LintGradleExecution#analyze()
LintGradleExecution#runLint()//这里其实跳了一步,三个Task实现类稍有不同但都会调到这
LintGradleClient#run()
LintCliClient#run()
LintDriver#analyze()
LintDriver#checkProject()
LintDriver#runFileDetectors()
我们着重看LintDriver#runFileDetectors()对源码检测的部分
private fun runFileDetectors(project: Project, main: Project?) {
...
if (scope.contains(Scope.JAVA_FILE) || scope.contains(Scope.ALL_JAVA_FILES)) {
val checks = union(
scopeDetectors[Scope.JAVA_FILE],
scopeDetectors[Scope.ALL_JAVA_FILES]
)
if (checks != null && !checks.isEmpty()) {
val files = project.subset
if (files != null) {//如果project.subset不为空
checkIndividualJavaFiles(project, main, checks, files)//则进行自定义文件的扫描
}
}
}
...
}
可以看到他在project.subset不为null的时候进行自定义文件的扫描,那么我们要做的就是将变更的文件插入到Project中,于是来看Project.subset取的什么
/**
* Adds the given file to the list of files which should be checked in this project. If no files
* are added, the whole project will be checked.
*
* @param file the file to be checked
*/
public void addFile(@NonNull File file) {
if (files == null) {
files = new ArrayList<>();
}
files.add(file);
}
/**
* The list of files to be checked in this project. If null, the whole project should be
* checked.
*
* @return the subset of files to be checked, or null for the whole project
*/
@Nullable
public List<File> getSubset() {
return files;
}
注释我特意没删,上面明确说明了如果getSubset()返回值不为null将只对files中的文件扫描,再次印证了我们方向没错,而addFile只在单元测试的时候被调用了,所以默认情况下是全量扫描,那我们要做的是在Project创建后调用addFile将变更文件传入来实现增量扫描。Project的创建是在LintGradleClient#createLintRequest
@Override
@NonNull
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);//创建Project
lintRequest.setProjects(Collections.singletonList(project));
registerProject(project.getDir(), project);
for (Project dependency : project.getAllLibraries()) {
registerProject(dependency.getDir(), dependency);
}
return lintRequest;
}
那其实只要我们拿到LintRequest然后遍历Project调用addFile传入变更文件即可,但一路debug下来会发现基本都是通过局部变量传递没有能反射修改的点。
到此全剧终!!!我逗你的。
在调用链的第二个方法ReflectiveLintRunner#runLint()中是通过创建ClassLoader去加载的LintGradleExecution类进行后面的操作
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)
}
}
看到ClassLoader是不是有一阵窃喜,也就意味着后面用到的类,很有可能都是在这个时候才加载进来,那么我们只要像Tinker那样,自己造一个LintGradleClient类插入到ClassLoader数组的最前面,稍微修改下createLintRequest方法,在方法中加上一段逻辑,遍历Project再调用addFile传入变更文件就完成了增量扫描的工作。
那接下来就是确认LintGradleClient是由该ClassLoader加载么,我Debug确认正是。
然后我们在看该ClassLoader是什么类型的类加载器,确认我们能不能把类插入到他的最前面,这里就不看源码了,直接给出答案是URLClassLoader,这种类加载器是通过URL数组去加载类,那目标很明确了,就是把我们修改过的LintGradleClient插入到URL数组的前面就好了。
如果你对URLClassLoader源码感兴趣的话可以看这篇博客。
2.4 代码插入
这里有两种思路,第一个是拿到ClassLoader的URL数组把我们的LintGradleClient加入在数组最前面,debug发现该ClassLoader并不好获取暂且搁置,第二个是看该ClassLoader构造时传入的URL数组怎么生成的,是来源于getProject().getConfigurations().getByName(LINT_CLASS_PATH)
,那找到Lint_Class_Path
的赋值处
public static void createLintClasspathConfiguration(@NonNull Project project) {
Configuration config = project.getConfigurations().create(LintBaseTask.LINT_CLASS_PATH);
config.setVisible(false);
config.setTransitive(true);
config.setCanBeConsumed(false);
config.setDescription("The lint embedded classpath");
project.getDependencies().add(config.getName(), "com.android.tools.lint:lint-gradle:" +
Version.ANDROID_TOOLS_BASE_VERSION);
}
也就是通过project.getDependencies().add(LintBaseTask.LINT_CLASS_PATH, $依赖)
就可以将代码插入URL数组。
接下来我们copy一份LintGradleClient代码,只修改createLintRequest方法,在方法中加上一段逻辑,遍历Project再调用addFile传入变更文件。
public class LintGradleClient extends LintCliClient{
...
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));
registerProject(project.getDir(), project);
for (Project dependency : project.getAllLibraries()) {
registerProject(dependency.getDir(), dependency);
}
IncrementUtils.inject(gradleProject, lintRequest);//增量扫描逻辑
return lintRequest;
}
...
}
变更文件插入代码如下
class IncrementUtils {
companion object {
const val TAG = "lint增量信息"
@JvmStatic
fun inject(project: Project, lintRequest: LintRequest) {
//增量扫描逻辑
printSplitLine(TAG)
var revision = project.properties["revision"]
var baseline = project.properties["baseline"]
val command =
"git diff $baseline $revision --name-only --diff-filter=ACMRTUXB"
println("开始执行:")
println(command)
val byteArray = Runtime.getRuntime()
.exec(command)
.inputStream
.readBytes()
val diffFileStr = String(byteArray, Charsets.UTF_8)
val diffFileList = diffFileStr.split("\n")
println("diff结果:")
println(diffFileStr.removeSuffix("\n"))
lintRequest.getProjects()?.forEach { p ->
diffFileList.forEach {
p.addFile(File(it))
}
}
printSplitLine(TAG)
}
}
}
fun printSplitLine(tag: String) {
println("--------------------------------------------日志分割线:$tag--------------------------------------------")
}
然后把这两个类作为一个工程上传jcenter,在通过project.getDependencies().add(LintBaseTask.LINT_CLASS_PATH, $依赖)
加入就插入到了URLClassLoader的URL数组中。
那此刻你可能有个问题,如何保证你加入的LintGradleClient能插在URL数组最前面,首先Dependencies这个map是LinkedHashMap能记录插入顺序,其次apply plugin: 'com.android.application'
我们一般都是写在Gradle脚本的最上面的,所以他的config方法是最先执行的,如果你不放心可以在配置完成后在执行project.getDependencies().add(LintBaseTask.LINT_CLASS_PATH, $依赖)
这样替换的类就能保证一定会在URL数组的第一个。
2.5 Task入口
通过上面的步骤增量扫描功能基本就完成了,现在只差一个入口了,这里就可以参照Lint的LintPerVariantTask,写一个自己的LintTask,基本照着他写就好了,注释都有我就不过多说明了
open class LintTask : LintBaseTask() {
private var allInputs: ConfigurableFileCollection? = null
private var variantName: String? = null
private var variantInputs: VariantInputs? = null
@InputFiles
@Optional
open fun getAllInputs(): FileCollection? {
return allInputs
}
@TaskAction
fun lint() {
val descriptor = object : LintBaseTaskDescriptor() {
/**
* com.android.tools.lint.gradle.LintGradleExecution#analyze会判断
*/
override val variantName: String? = this@LintTask.variantName
/**
* com.android.tools.lint.gradle.LintGradleExecution#lintSingleVariant用来作为lint扫描参数
*/
override fun getVariantInputs(variantName: String): VariantInputs? = variantInputs
}
runLint(descriptor)
}
open class CreationAction(
private val taskName: String,
private val scope: VariantScope,
private val variantScopes: List<VariantScope>
) : BaseCreationAction<LintTask>(scope.globalScope) {
override val name: String = taskName
override val type: Class<LintTask> = LintTask::class.java
override fun configure(task: LintTask) {
super.configure(task)
task.apply {
variantName = scope.fullVariantName//lint检测时会判断有没有该值,必须有
variantInputs = VariantInputs(scope)//lint检测时会取该值,必须有
allInputs = scope.globalScope.project.files()
.from(this.variantInputs!!.allInputs)//gradle增量任务
for (variantScope in variantScopes) {//不知道干嘛的,反正是模拟LintPerVariantTask就直接照抄了
addJarArtifactsToInputs(allInputs, variantScope)
}
description = "run lint scanner"
}
}
}
}
最后既然是找到增量代码,是需要有两个分支做对比的,我这边是通过命令入参的,
执行./gradlew lintTask -Pbaseline="xxx" -Prevision="xxx"
即可进行增量扫描。
如果不知道Gradle怎么获取命令行参数可以参照下面这个图
3. 注意点
3.1 Ci缓存问题clean策略
如果你已经支持了增量扫描,但是时间还是很长,先别打我,请注意是不是因为CI在执行新流水线的时候执行了git clean把Gradle缓存给删了导致Gradle所有Task全部重新执行了遍。
3.2 gradle版本适配问题
由于我们代码是基于AGP去自定义的,所以必然存在适配的问题,目前我是参照AGP3.5.3的代码自定义的,那么后面AGP升级后,就意味着可能需要修改来适配新版AGP。
4. Demo
上面所述所有功能都可以参照AndroidLint中的Plugin模块,有不明白的地方欢迎留言提问。