背景
如前文《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