Lint增量扫描实践
Posted zhuliyuan丶
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Lint增量扫描实践相关的知识,希望对你有一定的参考价值。
Lint增量扫描实践
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模块,有不明白的地方欢迎留言提问。
5. 参考
以上是关于Lint增量扫描实践的主要内容,如果未能解决你的问题,请参考以下文章