乐趣区

关于代码规范:Android-Lint-实践之二-自定义-Lint

背景

如前文《Android Lint 实际 —— 简介及常见问题剖析》所述,为保障代码品质,团队在开发过程中引入了 代码扫描工具 Android Lint,通过对代码进行动态剖析,帮忙发现代码品质问题和提出改良倡议。Android Lint 针对 Android 我的项目和 Java 语法曾经封装好大量的 Lint 规定(issue),但在理论应用中,每个团队因不同的编码标准和性能偏重,可能仍需一些额定的规定,基于这些思考,咱们钻研并开发了自定义的 Lint 规定。

根底

创立自定义 Lint 须要创立一个纯 Java 我的项目,引入相干的包后能够基于 Android Lint 提供的根底类编写规定,最终把我的项目以 jar 的模式输入后就能够被主我的项目援用。这里咱们以 QMUI Android 中的一个理论场景来阐明如何进行自定义 Lint:咱们在我的项目中应用了 Vector Drawable,在 Android 5.0 以下版本的零碎中,Vector Drawable 不被间接反对,这时应用 ContextCompat.getDrawable() 去获取一个 Vector Drawable 会导致 crash,而这种状况因为只在 5.0 以下的零碎中才会产生,往往不易被发现,因而咱们须要在编写代码的阶段就能及时发现并作出揭示。在 QMUI Android 中,提供了 QMUIDrawableHelper.getVectorDrawable 办法,基于 support 包封装了平安的获取 Vector Drawable 的办法,因而咱们最终的需要是查看出所有应用 ContextCompat.getDrawable()getResources().getDrawable() 去获取 Vector Drawable 的中央,进行揭示并要求替换为 QMUIDrawableHelper.getVectorDrawable 办法。

创立工程

如下面所述,创立自定义 Lint 须要创立一个 Java 我的项目,我的项目中须要引入 Android Lint 的包,我的项目的 build.gradle 如下:

apply plugin: 'java'

configurations {lintChecks}

dependencies {
    compile "com.android.tools.lint:lint-api:25.1.2"
    compile "com.android.tools.lint:lint-checks:25.1.2"

    lintChecks files(jar)
}

jar {
    manifest {attributes('Lint-Registry': 'com.qmuiteam.qmui.lint.QMUIIssueRegistry')
    }
}

其中 lint-api 是 Android Lint 的官网接口,基于这些接口能够获取源代码信息,从而进行剖析,lint-checks 是官网已有的查看规定。Lint-Registry 示意给自定义规定注册,以及打包为 jar,这个上面会具体解释。

Detector

Detector 是自定义规定的外围,它的作用是扫描代码,从而获取代码中的各种信息,而后基于这些信息进行揭示和报告,在本场景中,咱们须要扫描 Java 代码,找到 getDrawable 办法的调用,而后剖析其中传入的 Drawable 是否为 Vector Drawable,如果是则须要进行报告,残缺代码如下:

/**
 * 检测是否在 getDrawable 办法中传入了 Vector Drawable,在 4.0 及以下版本的零碎中会导致 Crash
 * Created by Kayo on 2017/8/24.
 */

public class QMUIJavaVectorDrawableDetector extends Detector implements Detector.JavaScanner {

    public static final Issue ISSUE_JAVA_VECTOR_DRAWABLE =
            Issue.create("QMUIGetVectorDrawableWithWrongFunction",
                    "Should use the corresponding method to get vector drawable.",
                    "Using the normal method to get the vector drawable will cause a crash on Android versions below 4.0",
                    Category.ICONS, 2, Severity.ERROR,
                    new Implementation(QMUIJavaVectorDrawableDetector.class, Scope.JAVA_FILE_SCOPE));

    @Override
    public List<String> getApplicableMethodNames() {return Collections.singletonList("getDrawable");
    }

    @Override
    public void visitMethod(@NonNull JavaContext context, AstVisitor visitor, @NonNull MethodInvocation node) {StrictListAccessor<Expression, MethodInvocation> args = node.astArguments();
        if (args.isEmpty()) {return;}

        Project project = context.getProject();
        List<File> resourceFolder = project.getResourceFolders();
        if (resourceFolder.isEmpty()) {return;}

        String resourcePath = resourceFolder.get(0).getAbsolutePath();
        for (Expression expression : args) {String input = expression.toString();
            if (input != null && input.contains("R.drawable")) {
                // 找出 drawable 相干的参数

                // 获取 drawable 名字
                String drawableName = input.replace("R.drawable.", "");
                try {
                    // 若 drawable 为 Vector Drawable,则文件后缀为 xml,依据 resource 门路,drawable 名字,文件后缀拼接出残缺门路
                    FileInputStream fileInputStream = new FileInputStream(resourcePath + "/drawable/" + drawableName + ".xml");
                    BufferedReader reader = new BufferedReader(new InputStreamReader(fileInputStream));
                    String line = reader.readLine();
                    if (line.contains("vector")) {
                        // 若文件存在,并且蕴含首行蕴含 vector,则为 Vector Drawable,抛出正告
                        context.report(ISSUE_JAVA_VECTOR_DRAWABLE, node, context.getLocation(node), expression.toString() + "为 Vector Drawable,请应用 getVectorDrawable 办法获取,防止 4.0 及以下版本的零碎产生 Crash");
                    }
                    fileInputStream.close();} catch (Exception ignored) {}}
        }
    }
}

QMUIJavaVectorDrawableDetector 继承于 Detector,并实现了 Detector.JavaScanner 接口,实现什么接口取决于自定义 Lint 须要扫描什么内容,以及心愿从扫描的内容中获取何种信息。Android Lint 提供了大量不同范畴的 Detector

  • Detector.BinaryResourceScanner 针对二进制资源,例如 res/raw 等目录下的各种 Bitmap
  • Detector.ClassScanner 绝对于 Detector.JavaScanner,更针对于类进行扫描,能够获取类的各种信息
  • Detector.GradleScanner 针对 Gradle 进行扫描
  • Detector.JavaScanner 针对 Java 代码进行扫描
  • Detector.ResourceFolderScanner 针对资源目录进行扫描,只会扫描目录自身
  • Detector.XmlScanner 针对 xml 文件进行扫描
  • Detector.OtherFileScanner 用于除下面 6 种状况外的其余文件

不同的接口定义了各种办法,实现自定义 Lint 实际上就是实现 Detector 中的各种办法,在下面的例子中,getApplicableMethodNames 的返回值指定了须要被查看的办法,visitMethod 则能够接管查看到的办法对应的信息,这个办法蕴含三个参数,其作用别离是:

  • context 这里的 context 是一个 JavaContext,次要的性能是获取主我的项目的信息,以及进行报告(包含获取须要被报告的代码的地位等)。
  • visitor visitor 是一个 ASTVisitor,即 AST(形象语法树)的访问者类,Android Lint 把扫描到的代码形象成 AST,不便开发者以节点 – 属性的模式获取信息,visitor 则能够不便地获取以后节点的相干节点。
  • node 这是一个 MethodInvocation 实例,MethodInvocation 是 Android Lint 里的 AST 子类,在下面的例子中,node 示意的是被扫描到的办法,所以咱们能够通过节点 – 属性的模式获取被扫描的办法的参数等各种信息。

在例子中咱们获取办法的参数,通过遍历参数拿到 Drawable 参数,合成出 Drawable 的文件名,而后通过 context 获取主我的项目的资源门路,配合 Drawable 的文件名拼接文件的理论门路,确定文件存在后查看文件内容结尾是否蕴含“vector”这个字符串,如果是则示意开发者在一般的 getDrawable 办法中传入了 Vector Drawable,最初调用 context 的 report 办法进行报告。

值得注意的是,在例子中咱们并没有间接实例 Drawable,而后通过 Drawable 的办法判断是否为 Vector Drawable,而是通过较为繁琐的步骤查看文件内容, 这是因为 Android Lint 的我的项目是一个纯 Java 我的项目,不能应用 android.graphics 等包 ,因此开发时会比拟繁琐。

Issue

在下面的例子中,在查看出问题须要进行报告时,context.report 办法中传入了一个 ISSUE_JAVA_VECTOR_DRAWABLE,这里的 ”issue” 是申明一个规定,因而自定义一个 Lint 规定就须要定义一个 issue。issue 由类办法 Issue.create 创立,参数如下:

  • id:标记 issue 的惟一值,语义上要能简短形容问题,应用 Java 注解和 XML 属性屏蔽 Lint 时,就须要应用这个 id。
  • summary:详情地形容问题,不须要给出解决办法。
  • explanation:具体地形容问题以及给出解决办法。
  • category:问题类别,在零碎给出的分类中抉择,前面会详述。
  • priority:1-10 的数字,示意优先级,10 为最重大。
  • severity:重大级别,在 Fatal,Error,Warning,Informational,Ignore 中抉择一个。
  • Implementation:Detector 与 Issue 的映射关系,须要传入以后的 Detector 类,以及扫描代码的范畴,例如 Java 文件、Resource 文件或目录等范畴。

如下图,产生问题时,问题的揭示信息就就会显示相干的 Issue 的 id 等信息。

Category

Category 用于给 Issue 分类,零碎曾经提供了几个罕用的分类,零碎 Issue(即 Android Lint 自带的查看规定)也是应用这个 Category:

  • Lint
  • Correctness (子分类 Messages)
  • Security
  • Performance
  • Usability (子分类 Typography, Icons)
  • A11Y (Accessibility)
  • I18N (Internationalization,子分类 Rtl)

如果系统分类不能满足需要,也能够创立自定义的分类:

public class QMUICategory {public static final Category UI_SPECIFICATION = Category.create("UI Specification", 105);
}

应用如下:

public static final Issue ISSUE_JAVA_VECTOR_DRAWABLE =
        Issue.create("QMUIGetVectorDrawableWithWrongFunction",
                "Should use the corresponding method to get vector drawable.",
                "Using the normal method to get the vector drawable will cause a crash on Android versions below 4.0",
                QMUICategory.UI_SPECIFICATION, 2, Severity.ERROR,
                new Implementation(QMUIJavaVectorDrawableDetector.class, Scope.JAVA_FILE_SCOPE));

Registry

创立自定义 Lint 的最初一步是“Lint-Registry”,如后面所述,build.gradle 中须要申明 Regisry 类,打包成 jar:

jar {
    manifest {attributes('Lint-Registry': 'com.qmuiteam.qmui.lint.QMUIIssueRegistry')
    }
}

而 registry 类中则是注册创立好的 Issue,以 QMUIIssueRegistry 为例:

public final class QMUIIssueRegistry extends IssueRegistry {@Override public List<Issue> getIssues() {
        return Arrays.asList(
                QMUIFWordDetector.ISSUE_F_WORD,
                QMUIJavaVectorDrawableDetector.ISSUE_JAVA_VECTOR_DRAWABLE,
                QMUIXmlVectorDrawableDetector.ISSUE_XML_VECTOR_DRAWABLE,
                QMUIImageSizeDetector.ISSUE_IMAGE_SIZE,
                QMUIImageScaleDetector.ISSUE_IMAGE_SCALE
        );
    }
}

QMUIIssueRegistry 继承与 IssueRegistryIssueRegistry 中注册了 Android Lint 自带的 Issue,而自定义的 Issue 则能够通过 getIssues 系列办法传入。

到这一步,这个用于自定义 Lint 的 Java 我的项目编写结束了。

接入我的项目

依照下面的步骤,实现自定义 Lint 的编写后,编译 Gradle 能够失去对应的 jar 文件,那么 jar 应该如何接入我的项目,使得执行我的项目 Lint 时能够辨认到这些自定义的规定呢?

Google 官网的计划是把 jar 文件放到 ~/.android/lint/,如果本地没有 lint 目录能够自行创立,这个应用形式较为简单,但也使得 Android Lint 作用于本地所有的我的项目,不大灵便。

因而咱们举荐应用 Google adt-dev 论坛中被探讨举荐的计划,在主我的项目中新建一个 Module,打包为 aar,把 jar 文件放到该 aar 中,这样各个我的项目能够以 aar 的形式自行引入自定义 Lint,比拟灵便,我的项目之间不会造成烦扰。

Module 的 build.gradle 内容如下(以 QMUI Lint 为例):

apply plugin: 'com.android.library'

configurations {lintChecks}

dependencies {lintChecks project(path: ':qmuilintrule', configuration: 'lintChecks')
}

task copyLintJar(type: Copy) {from(configurations.lintChecks) {rename { 'lint.jar'}
    }
    into 'build/intermediates/lint/'
}

project.afterEvaluate {def compileLintTask = project.tasks.find { it.name == 'compileLint'}
    compileLintTask.dependsOn(copyLintJar)
}

其中 qmuilintrule 是自定义 Lint 规定的 Module,这样这个须要进行 aar 打包的 Module 即可获取到 jar 文件,并放到 build/intermediates/lint/ 这个门路中。把 aar 公布到 Bintray 后,须要用到自定义 Lint 的中央只须要引入 aar 即可,例如:

compile 'com.qmuiteam:qmuilint:1.0.0'

另外须要留神, 在编写自定义规定的 Lint 代码时,编写后从新构建 gradle,新代码也不肯定失效,须要重启 Android Studio 能力确保新代码曾经失效。

残缺的示例代码能够参考 QMUI Android 的 qmuilintqmuilintrule module。

参考资料

  • Writing Custom Lint Rules – Android Studio Project Site
  • googlesamples/android-custom-lint-rules: This sample demonstrates how to create a custom lint checks and corresponding lint tests
  • Specify custom lint JAR outside of lint tools settings directory
  • Writing Custom Lint Checks with Gradle | LinkedIn Engineering
退出移动版