AGP7.0增量Lint适配

发布于 2022年 05月 05日 02:05

前言

最近一直在做AGP7.0的升级适配工作,从3.5直接升7.0.4,各种大坑小坑,做过适配的应该都深有体会🤮🤮🤮。。。

刚适配完agp7.0上增量Lint的处理逻辑,写出来分享一下。

应该是最新、最简单的适配了。

这次agp7的lint适配相对之前的适配方案几乎重写了,因为lint-api也重新写了。但这并不意味适配工作很多,相反,整个适配代码过于简单。。。真的!!!

之前3.5~4.2的增量逻辑,tianwailaike61RocketZLYsunshine8大佬写的很好了,里面学到很多,各位大佬有兴趣可以看看。

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行。

推荐文章