背景
如前文《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
继承与 IssueRegistry
,IssueRegistry
中注册了 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 的 qmuilint
与 qmuilintrule
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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。