Android|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 getApplicableMethodNames() { return Collections.singletonList("getDrawable"); }@Override public void visitMethod(@NonNull JavaContext context, AstVisitor visitor, @NonNull MethodInvocation node) {StrictListAccessor args = node.astArguments(); if (args.isEmpty()) { return; }Project project = context.getProject(); List 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 等信息。
Android|Android Lint 实践之二 —— 自定义 Lint
文章图片

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 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 才能确保新代码已经生效。
【Android|Android Lint 实践之二 —— 自定义 Lint】完整的示例代码可以参考 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

    推荐阅读