前言
最近一直在做AGP7.0的升级适配工作,从3.5直接升7.0.4,各种大坑小坑,做过适配的应该都深有体会🤮🤮🤮。。。
刚适配完agp7.0上增量Lint的处理逻辑,写出来分享一下。
应该是最新、最简单的适配了。
这次agp7的lint适配相对之前的适配方案几乎重写了,因为lint-api也重新写了。但这并不意味适配工作很多,相反,整个适配代码过于简单。。。真的!!!
示例代码在这IncrementLint
之前3.5~4.2的增量逻辑,tianwailaike61、RocketZLY、sunshine8大佬写的很好了,里面学到很多,各位大佬有兴趣可以看看。
1、增量Lint的场景
在我们目前的开发流程中,除了在IDEA中集成一些checkStyle、lintCheck规则外,我们还会在开发人员提交前,通过githooks触发本地Lint,提交之后gitlab也会再触发一次,改动代码中如果有Error级别问题的话会给相关同学发送一封邮件。每次发版前,我们也会检测一次,避免将一些严重的问题带到线上去。
在该场景下,我们每次通过git diff出单次提交或两个commitid之间的变动文件进行lint,而不是全工程。
2、Lint流程(AGP7以下)
在agp3.5~4.2,lint-api 26、27的版本中,大体流程应该都类似这样:
- lint的流程从LintBaseTask、LintBaseTaskDescriptor这两个类开始,初始化LintGradleClient需要的参数大部分都是这个LintBaseTaskDescriptor对象提供的,LintBaseTask的TaskAction中触发 new ReflectiveLintRunner().runLint(),
- ReflectiveLintRunner这个类在lint-gradle-api包中,runLint方法就利用反射实例化LintGradleExecution对象,并调用analyze方法。
- LintGradleExecution在lint-gradle包中,LintGradleClient也在这里。analyze方法获取了ToolingRegistry和AndroidProject,并根据变种(variant)走不同的LintGradleClient初始化参数获取流程。
- LintGradleClient中有个createLintRequest。增量代码最关键的也就是这个createLintRequest方法,往project加入要扫描的文件
@Override
protected LintRequest createLintRequest(@NonNull List<File> files) {
//注意这个project是com.android.tools.lint.detector.api.project
LintRequest lintRequest = super.createLintRequest(files);
for (Project project : lintRequest.getProjects()) {
project.addFile(changefile);//加入要扫描的文件
addChangeFiles(project);
}
return lintRequest;
}
//copy from https://www.jianshu.com/p/4833a79e9396
后面获取Detector,遍历执行check文件的操作,本次不涉及,暂时不说了,有兴趣的可以自己看源码。
上面的一坨名词、流程、类,除了createLintRequest这段代码外,你都可以忽略,他们在agp7上基本都干掉了,是的,都没了
这是lint-gradle 27.2.0包截图
这是lint-gradle 30.0.4的
是不是看到是不是想说 what's up,上面提到的几个类直接删没了。
当时在4.2.2适配代码的基础上,直接改agp、lint api版本sync后。满屏的爆红,当时心里真是尼玛 什么xxsdk,会不会写sdk。
平复下心情后,agp7就算硬着头皮也得整完,要不把他们lint-api 27.2.0的代码 拷贝过来,自己实现一遍lint流程,反正IncrementLint库里面就是需要什么就自己实现什么。
一顿操作后放弃了,上面流程中其实还有一个很重要的类ProjectSearch,用来生成Lint专用的Project,他依赖lint-model、common-sdk,这两个库从27.2.0到30.0.4,改动很大。而且agp7.0直接依赖了这两个sdk,这意味着我们没法 引入agp7的时候exclude掉这两个类。
而且,对,就是这么多而且,每一个而且都是少则半天,多则一两天的试错😭😭😭。
而且AGP7的BasePlugin的实现接口里面直接移除了LintModelModuleLoaderProvider
这是AGP4.2的BasePlugin截图
这是AGP7.0的BasePlugin截图
尝试过好几次后,直接放弃了原来缺什么补什么的思路。
依赖的sdk重写了,业务层面的小改是不可能的了,还是老老实实看看AGP7吧
3、Lint流程(AGP7)
自AGP7.0、lint api 30开始,移除了很多熟悉类,也新增了不少,我们只说重要的几个
- LintPlugin
- AndroidLintTask
- AndroidLintAnalysisTask
在googlesample提供的custom-lint项目中,我们能看到check这个library 引入了com.android.lint这个插件,对应的就是LintPlugin它里面会创建AndroidLintTask、AndroidLintAnalysisTask。这个插件主要是针对哪些非android项目的,比如纯java、kotlin项目,android相关的插件其实会自动引入AndroidLintTask、AndroidLintAnalysisTask。
具体调用路径:BasePlugin.createAndroidTasks->TaskMananger.createTasks->lintTaskManager.createLintTasks
TaskManager截图
LintTaskManager
看完上面的,我们再来说说AndroidLintAnalysisTask、AndroidLintTask,两个类参数超级多,但对应的TaskAction 方法却很简短。
通过上面的几个截图,就会看到,最终这两个task任务都是封装参数,具体实现 "com.android.tools.lint.Main
"这个类实现的,里面的逻辑跟之前的LintGradleExecution类似,new了一个LintCliClient,来createLintRequest来执行。
看Main.run方法的截图是不是跟以前LintGradleExecution有点类似
这是createLintRequest截图,modules和files不能共存,之前尝试过绕过这块的限制,发现还是不行,又浪费了不少时间。
看到这里就在向是不是可以 拿到这个projects,然后在里面addFile就行了。
是的,就是这样
lint包中的Project依然保留了这个addFile类,看代码注释:如果给与了文件,就只check这个files,否则就chec whole project。
说完上面的这些再来说说,我们适配的关键 lintTool
。
我们先来看看AndroidLintTask中的lintTool是怎么初始化的
他后面会传递个AndroidLintWorkAction里面
最后在这里就加载Main class,然后调用run方法
最后看看这个classloader是怎么创建的。
这里的classloader实质上是一个URLClassLoader,他只会加载classpath.files里面的类,而这个files又是上面的project.configurations.getByName(LINT_CLASS_PATH)里面提供的
我们来看看这个LINT_CLASS_PATH 的configuration是在那里设置的?
在BasePlugin里面设置的,添加了一个"com.android.tools.lint:lint-gradle:版本号"的依赖
看到这里是不是就明白该怎么做了?
参考它添加依赖,让classloader 优先加载我们修改过的Main
4、AGP7增量lint适配
整体思路说完了,接着说下具体的代码逻辑,真的过于简短
1、新建lint_hook库
首先,建一个library,用来处理待会的com.twl.lintplugin:lint_hook依赖,然后我们自己写一个Main类,包名跟他完全一样,代码完全从里面拷过来,然后在createLintRequest里面遍历project 调用addFile添加我们diff出来的文件就行了。
然后在AndroidLintWorkAction里面,保证classloader首先加载我们修改过的Main就行了。
在库文件的依赖里面务必添加 lint依赖
如何保证优先加载我们的类?
看下文
2、自定义插件
自定义一个plugin,只需要在lintClassPath
这个configuration里面在添加一个dependence就行。
千万记住不要把resolutionStrategy设置成
ResolutionStrategy.SortOrder.DEPENDENCY_FIRST
。
下面直接贴代码
open class XLintPlugin : Plugin<Project> {
private val CUSTOM_LIB = "com.twl.lintplugin:lint_hook:1.0-SNAPSHOT";
override fun apply(project: Project) {
insertLintHookToClasspath(project)
}
private fun insertLintHookToClasspath(project: Project) {
val lintClassPath: Configuration = project.configurations.getByName(OpenAndroidLintTask.LINT_CLASS_PATH)
lintClassPath.incoming.beforeResolve {
lintClassPath.dependencies.add(project.dependencies.create(CUSTOM_LIB))
}
//千万不要设置为SortOrder.DEPENDENCY_FIRST,否则会导致无法加载修改过的Main
// lintClassPath.resolutionStrategy {
// it.sortArtifacts(ResolutionStrategy.SortOrder.DEPENDENCY_FIRST)
// }
}
}
保证自定义class优先加载的关键在resolutionStrategy。
SortOrder有三个
- DEFAULT
- CONSUMER_FIRST
- DEPENDENCY_FIRST
他们之间的主要区别在于DEPENDENCY_FIRST会将被依赖的放在前面
这是默认时,classpath里面的顺序截图,可以看到我们的lint-hook在 官方lint的前面
至此,整体适配流程结束,重写后的适配代码,不超过20行。
演示代码
更新示例代码 IncrementLint