前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如何让你的lint检查更加高效?

如何让你的lint检查更加高效?

作者头像
腾讯技术工程官方号
发布2019-08-19 10:10:05
3.3K2
发布2019-08-19 10:10:05
举报

导语:在自定义lint规则的实践过程中,我们发现lint扫描的效率非常低,比如在项目中进行一次lint全量扫描,平均需要5分钟左右,而且这是在仅扫描自定义规则的情况下。我们将lint扫描集成到了流水线中,所有的MR操作都会触发扫描,并block住MR的流程。经常会发现这样一种情况,某个MR仅仅修改了一行代码,却仍要扫瞄整个项目,这会严重影响MR的效率。所以,大部分情况下并不需要进行lint的全量扫描,我们更关心的是新增代码是否存在问题。于是,我们需要探索一种lint增量扫描的解决方案。

前言

先来说说为什么会有这样一个题目吧。最近这大半年都在做项目crash收敛的事情,说到crash收敛,最简单的应该是Java相关的Crash了。在做的过程中就发现,其实很多Java Crash的产生都是开发同学犯的低级错误,比如数组越界、parseInt的裸调等等。那有没有一种方式可以避免开发同学犯这样的错误呢?后来就尝试接入静态代码扫描。公司级的静态代码扫描有CodeDog和CodeCC,当时CodeCC不支持kotlin,就选择了CodeDog,而CodeDog上的规则可以避免一部分问题,但很多项目相关的问题规避需要自定义规则才能解决,而CodeDog在自定义规则上的支持并不是特别友好。后来就开始调研如何自己做自定义规则,支持Kotlin的静态代码扫描工具主要有以下几种:

  1. Ktlint:只支持代码风格检查,如果要支持代码性能检查的话,需要大量扩展代码性能规则集。
  2. Detekt:支持代码风格检查和代码性能检查,代码风格检查完全复用Ktlint,代码性能检查规则集也比较完善,且支持规则集扩展。
  3. Lint:这个是Google官方提供的静态代码扫描工具。支持Kotlin和Java等多种语言,支持扩展规则集。

因为我们的项目其实是使用了Kotlin和Java混合开发,项目中有相当一部分使用Java开发的代码,而lint能同时支持Java和Kotlin,所以最后我们选择了lint。

在整个自定义lint规则的实践过程中,我们发现lint扫描的效率非常低,比如在项目中进行一次lint全量扫描,平均需要5分钟左右,而且这是在仅扫描自定义规则的情况下。

我们将lint扫描集成到了编译流水线中,所有的MR操作都会触发扫描,并block住MR的流程。经常会发现这样一种情况,某个MR仅仅修改了一行代码,却仍要扫瞄整个项目,这会严重影响MR的效率。所以,大部分情况下并不需要进行lint的全量扫描,我们更关心的是新增代码是否存在问题。于是,我们需要探索一种lint增量扫描的解决方案。

目标

通过查阅相关资料,发现Google官方并没有提供lint增量扫描能力,网上也没有相关的解决方案。于是只能自己动手,毕竟每次提交MR后要等很久的lint检查,实在不是一个很好的体验。我们的目标主要有以下两点:

  1. 报告增量问题
  2. 增量扫描文件
  3. 能方便的接入持续集成

思路演变

1.baseline

Google虽然没有提供lint增量扫描的能力,但是在lint2.3.0版本以后,提供了一个baseline的功能。一开始我以为这个就是增量扫描,但后了解后才发现,baseline本质上也是全量扫描,只不过baseline允许你创建一个基准问题集,之后所有的扫描结果集合会与基准问题集做对比,筛选出增量问题写入报告。因此,baseline的方案本质是无法提升扫描效率的。

2.现有的lint使用方式

目前来说,使用lint有以下几种方式:

  • Android Studio里的lint扫描
  • AndroidGradlePlugin里的lint任务
  • lint命令行工具

下面是几种使用方式的对比:

功能

Android Studio

AndroidGradlePlugin

lint命令行工具

增量问题报告

Yes

Yes(2.3.0以后)

No

增量扫描

Yes

No

No

接入持续集成

No

Yes

Yes

Android Studio的方式能支持增量问题报告和增量扫描,但是无法应用到流水线中,且无法强制开发同学人人去执行;AndroidGradlePlugin和命令行的方式,都能方便地继承到流水线中,但是它们都无法实现增量扫描,效率十分低下。因此,并没有一种方式可以完美契合我们的目标。既然如此,我们可以以现有工具为基础,开发一款能增量扫描和展示问题,又能方便接入流水线的工具。

3.新的解决方案

通过上面对三种现有lint使用方式的对比,发现AndroidGradlePlugin基于Gradle,是最易扩展到一种,因此,我们决定在AndroidGradlePugin的基础上进行扩展,开发一款完美的lint工具。 其实增量扫描的解决思路非常简单:

  1. 既然是基于Gradle,自然是通过自定义插件和自定义Task的方式;
  2. Task内首先需要找到增量代码,需要支持版本号之间的对比和分支之间的对比,MR就需要分支之间的对比;
  3. 最后对这些增量代码进行lint检查。

方案实现

下面来看下每一步如何实现。

1.寻找增量代码

目前大多数项目都采用git进行版本控制,所以寻找增量代码,可以简化为寻找两次git提交之间的版本差异。考虑到lint检查的最小单位是单个文件,所以我们找到增量代码文件集合即可,而git diff命令刚好能够满足我们的要求。

代码语言:javascript
复制
// 计算两次commit之间的差异文件,diff-filter=d是指除删除意外所有状态的文件git diff --name-only --diff-filter=d <commit-1> <commit-2>
// 计算两个分支之间的差异文件,适用于MR的增量扫描git diff --name-only --diff-filter=d <branch-1> <branch-2>

(左滑可查看完整代码,下同)

封装为工具方法如下:

代码语言:javascript
复制
// 计算两次git提交之间的差异文件static List<String> diffFileListFromTwoCommit(String revision, String baseline, String filter) {        return filterInvalidLine(runCmd("git diff --name-only --diff-filter=$filter $revision $baseline").split('\n'))}
// 计算两个git分支之间的差异文件static List<String> diffFileListFromTwoBranch(String revisionBranch, String baselineBranch, String filter) {    return filterInvalidLine(runCmd("git diff --name-only --diff-filter=$filter $revisionBranch $baselineBranch").split('\n'))    }
// 执行命令static String runCmd(String cmd) {    return Runtime.getRuntime().exec(self).text.trim().replaceAll("\"", "")    }

2.对增量文件进行lint检查

想要对增量文件进行lint检查,首先需要弄清楚android的gradle插件自带的lint任务是如何进行代码扫描的。通过查阅源码,可以看到lint任务的执行流程如下:

其中LintDriver.runFileDetectors()的源码如下:

代码语言:javascript
复制
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) {                checkIndividualJavaFiles(project, main, checks, files)            } else {                val sourceFolders = project.javaSourceFolders                val testFolders = if (scope.contains(Scope.TEST_SOURCES))                    project.testSourceFolders                else emptyList<File>()
                val generatedFolders = if (checkGeneratedSources)                    project.generatedSourceFolders                else emptyList<File>()                checkJava(project, main, sourceFolders, testFolders, generatedFolders, checks)            }        }    }    ...}

其中:

  • checkIndividualJavaFiles - 检查项目文件子集
  • checkJava - 检查整个项目

所以从这里可以看出,增量扫描是可以实现的,只要project.subset不为空!那这个subset是哪里赋值的呢?这里让我们来看下Project的源码:

代码语言:javascript
复制
/** * 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 */@Nullablepublic List<File> getSubset() {    return files;}
/** * 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);}

所以,如果在初始化Project的时候通过addFile方法添加过文件子集,我们就可以进行代码增量扫描了。然而,我们发现addFile这个方法,竟然只在单元测试代码中调用过!所以这个能力google并没有开放出来。那我们需要自己想办法,在合适的时机将我们通过git diff计算出来的增量文件路径,通过Project.addFile方法添加到Project.subset中,就可以完成增量扫描的任务了。那什么时机最合适呢?先看看Project的创建时机,在LintGradleClient的createLintRequest方法中:

代码语言:javascript
复制
@Override@NonNullprotected 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);    }
    return lintRequest;}

这是一个protected方法,所以我们是不是可以继承LintGradleClient,重写createLintReqeust方法来完成增量文件的写入呢?现在思路清晰多了,于是我们写了一个自定义的LintGradleClient:

代码语言:javascript
复制
public class LintGradleClient extends com.android.tools.lint.gradle.LintGradleClient {
    public List<File> incrementFiles = null;
    @Override    protected LintRequest createLintRequest(List<File> files) {        LintRequest request =  super.createLintRequest(files);        if (request != null && incrementFiles != null) {            for (Project project: request.getProjects()) {                for (File file: incrementFiles) {                    project.addFile(file);                }            }        }        return request;    }}

如何将LintGradleClient替换为我们自定义的类呢?继续看源码,发现LintGradleClient的实例化发生在LintGradleExecution的analyze()->runLint()过程中,但是这个过程并没有很好的时机去替换LintGradleClient的实例化,怎么办?那继续看LintGradleExecution的创建时机,在ReflectiveLintRunner().runLint()方法中,源码如下:

代码语言:javascript
复制
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) {        ...    } catch (t: Throwable) {        ...    }}
private fun getLintClassLoader(gradle: Gradle, lintClassPath: Set<File>): ClassLoader {    if (loader == null) {        ...        val urls = computeUrlsFromClassLoaderDelta(lintClassPath)                    ?: computeUrlsFallback(lintClassPath)        loader = DelegatingClassLoader(urls.toTypedArray())    }    return loader}

看到这段代码的时候,我立马眼前一亮,觉得这事妥了!这里做了一件什么事情呢:通过DelegateClassLoader去加载com.android.tools.lint.gradle.LintGradleExecution这个类,然后通过反射的方式来实例化LintExecution对象,传入一个LintExecutionRequest参数,并执行analyze方法。

这里假如我们自定义一个LintGradleExecution类,并在这个类中使用我们之前自定义的LintGradleClient实例替代官方的实例,就可以达到狸猫换太子的效果,完成增量扫描了。而LintGradleExecution这个类的实例化是通过ClassLoader动态加载完成的,这意味着,我们可以hook这个ClassLoader加载类的过程,让其加载我们自定义的LintGradleExecution类。

这里使用的DelegatingClassLoader实际上是一个URLClassLoader,而URLClassLoader寻找类的原理,是在一个URL列表中按顺序寻找目标类,找到即止。因此,我们可以将含有自定义类LintGradleExecution的url插入到url列表的最前面,这样在执行loader.loadClass("com.android.tools.lint.gradle.LintGradleExecution")时,加载到的class就是我们自定义的类了。

那如何插入自定义的url?我们可以看下DelegatingClassLoader的url列表是如何计算的:

代码语言:javascript
复制
private fun computeUrlsFallback(lintClassPath: Set<File>): List<URL> {    val urls = mutableListOf<URL>()
    for (file in lintClassPath) {        val name = file.name
        // The set of jars that lint needs that *aren't* already used/loaded by gradle-core        if (name.startsWith("uast-") ||            name.startsWith("intellij-core-") ||            name.startsWith("kotlin-compiler-") ||            name.startsWith("asm-") ||            name.startsWith("kxml2-") ||            name.startsWith("trove4j-") ||            name.startsWith("groovy-all-") ||
            // All the lint jars, except lint-gradle-api jar (self)            name.startsWith("lint-") &&            // Do *not* load this class in a new class loader; we need to            // share the same class as the one already loaded by the Gradle            // plugin            !name.startsWith("lint-gradle-api-")        ) {            urls.add(file.toURI().toURL())        }    }
    return urls}

说白了,就是lintClassPath这个参数里所有的file转成List<URL>,并且file命名要符合"lint-"开头的规范。那lintClassPath是怎么来的?继续看源码:

代码语言:javascript
复制
lintTask.lintClassPath = globalScope.getProject().getConfigurations().getByName("lintClassPath");

原来是取的一个名为"lintClassPath"的配置项下所有的依赖的集合,而"lintClassPath"配置项是在AndroidGradlePlugin配置阶段配置的,如下:

代码语言:javascript
复制
project.getDependencies().add("lintClassPath", "com.android.tools.lint:lint-gradle:" +                Version.ANDROID_TOOLS_BASE_VERSION);

因此,我们可以在整个gradle配置完成后,删除以上配置,新增我们自定义的配置:

代码语言:javascript
复制
class LintPlugin implements Plugin<Project> {    @Override    void apply(Project project) {        ...        project.getDependencies().add("lintClassPath", "com.tencent.nijigen:lint-nice-gradle:0.0.1")        ...    }}

这样,DelegatingClassLoader在loadClass的时候,就会加载到我们自定义的LintGradleExecution类,从而实例化自定义的LintExecutionClient,完成自定义lint检查。

3.新增Gradle Task完成增量扫描的入口

通过上述分析,我们可以完成lint任务的增量扫描了。但是我们需要一个自定义Task,作为增量扫描的任务,可以方便的通过./gradlew lintIncrement的方式来触发增量扫描。

通过查阅源码,可以知道所有lint任务都有一个父类LintBaseTask,这个类封装了基本的lint任务的相关配置和执行操作。所以我们可以继承LintBaseTask,派生一个LintIncrementTask子类,源码如下:

代码语言:javascript
复制
public class LintIncrementTask extends LintBaseTask {    private VariantInputs variantInputs;
    @TaskAction    public void lint() {        // 读取参数        String baseline = "";        String revision = "";        if (project.hasProperty("revision") && project.hasProperty("baseline")) {            baseline = (String) project.property("baseline");            revision = (String) project.property("revision");        }        // 寻找变更文件集合        List<String> files = GitUtil.diffFileListFromTwoBranch(revision, baseline);        if (files.isEmpty()) {            getLogger().warn("no files was modified, skip lint incremental task");        } else {            LintCheckTaskDescriptor descriptor = new LintCheckTaskDescriptor();            descriptor.incrementFiles = files;            // 执行lint检查            runLint(descriptor);        }    }
    public class LintCheckTaskDescriptor extends LintBaseTaskDescriptor {        List<File> incrementFiles;        ...        public List<File> getIncrementFiles() {            return incrementFiles;        }    }}

但是当你执行./gradlew lintIncremnt -Prevision=xxxx -Pbaseline=xxxx命令时,会报错~嗯,理想很丰满,现实很骨感!参考Lint任务的其他实现,比如LintPerVariantTask,我们可以发现,每个lint任务都需要进行配置,如下:

代码语言:javascript
复制
public abstract static class BaseConfigAction<T extends LintBaseTask> implements TaskConfigAction<T> {    @NonNull private final GlobalScope globalScope;
    public BaseConfigAction(@NonNull GlobalScope globalScope) {        this.globalScope = globalScope;    }
    @Override    public void execute(@NonNull T lintTask) {        lintTask.setGroup(JavaBasePlugin.VERIFICATION_GROUP);        lintTask.lintOptions = globalScope.getExtension().getLintOptions();        File sdkFolder = globalScope.getSdkHandler().getSdkFolder();        if (sdkFolder != null) {            lintTask.sdkHome = sdkFolder;        }
        lintTask.toolingRegistry = globalScope.getToolingRegistry();        lintTask.reportsDir = globalScope.getReportsDir();        lintTask.setAndroidBuilder(globalScope.getAndroidBuilder());
        lintTask.lintClassPath = globalScope.getProject().getConfigurations()                .getByName(LINT_CLASS_PATH);    }}

通过源码发现,每个Lint任务需要配置sdkHome/toolingregistry/androidBuilder等一系列Android环境相关的变量,而继续对这些变量进行追本溯源,发现它们是在AndroidGradlePlugin在配置阶段就已经设置好的,并且设置代码相当复杂。

我最开始的思路是针对每一个变量,参考AndroidGradlePlugin的实现对其进行赋值,发现需要拷贝大量AndroidGradlePlugin里的代码实现,并且经过多次尝试,总有赋值错误或者赋值不完全的情况存在。为什么这三个变量的设置会非常复杂呢?因为每个变量的类型里又有很多其他的属性需要设置,层层嵌套之后,对这些属性赋值就变得异常繁琐。最终这种方案以失败告终。

有没有一种省时省力又不会出错的方案呢?当然有了。经过多次尝试和摸索之后,我试着换了一种思路。因为LintIncrementTask和其他标准的LintTask一样,都是继承了LintBaseTask,所以说其他LintTask在配置完成后,都会将sdkHome/toolingregistry/androidBuilder等一系列变量都设置好,而自定义的LintIncrementTask的这些变量能和这些标准LintTask的变量值一致就可以了。后来想到gradle任务都有配置和执行两个阶段,而这些变量的设置都是在配置阶段完成的,所以在整个gradle的配置阶段完成后,取到标准LintTask的这些变量值,直接赋值给LintIncrementTask就好了!什么?你说这些变量值都是私有的,怎么取?哈哈,反射大法好呀。不废话,上代码:

代码语言:javascript
复制
public void config() {    Object lintTask = getProject().getTasks().getByName("lintDebug");    sdkHome = ReflectiveUtils.getFieldValue(LintBaseTask.class, "sdkHome", lintTask);    reportsDir = ReflectiveUtils.getFieldValue(LintBaseTask.class, "reportsDir", lintTask);    toolingRegistry = ReflectiveUtils.getFieldValue(LintBaseTask.class, "toolingRegistry", lintTask);    variantInputs = ReflectiveUtils.getFieldValue(LintPerVariantTask.class, "variantInputs", lintTask);    setAndroidBuilder(ReflectiveUtils.getFieldValue(AndroidBuilderTask.class, "androidBuilder", lintTask));}

完美搞定!现在就可以正常运行lintIncremnt任务了~

数据对比

通过在波洞项目中应用lint全量扫描和增量扫描,耗时数据对比如下:

可以发现,对波洞项目进行一次lint全量扫描的平均耗时在5分钟左右,而使用lint增量扫描平均耗时仅需20s,效率提升了15倍以上。

总结

本文主要讨论了在自定义lint规则框架的基础上,一种实现Lint增量扫描的解决方案,解决了如下两个问题:

  1. 生成lint问题的增量报告
  2. lint增量检查,提升效率

lint 2.3.0新增的baseline能力,也可以实现lint问题的增量报告,但是其本质也是全量扫描,并不能提升扫描效率。因此在项目的实际应用中,可以结合baseline和本方案共同使用:对项目中遗留的暂时没有时间修复的大量lint问题,可以使用baseline的功能,生成lint问题基准文件,同时应用本文介绍的方案,提升扫描效率。

参考文档

  1. Lint介绍文档:https://developer.android.com/studio/write/lint?hl=zh-cn
  2. Lint源码:https://android.googlesource.com/platform/tools/base/+/master/lint
  3. 官方Lint规则:https://android.googlesource.com/platform/tools/base/+/master/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks
  4. Google官方自定义lint规则指南:http://tools.android.com/tips/lint-custom-rules
  5. 美团的Android自定义Lint实践:https://tech.meituan.com/android_custom_lint.html
  6. Google官方自定义lint升级版本:https://github.com/googlesamples/android-custom-lint-rules/tree/master/android-studio-3
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-08-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯技术工程 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.baseline
  • 2.现有的lint使用方式
  • 3.新的解决方案
  • 1.寻找增量代码
  • 2.对增量文件进行lint检查
  • 3.新增Gradle Task完成增量扫描的入口
相关产品与服务
腾讯云代码分析
腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,支撑团队传承代码文化。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档