自定义Lint
前言
不知道大家有没有注意项目中黄色代码块的提示,如下图所示:
或者红色标记的代码(并没有任何错误),如下图所示:
上文黄色的提醒和红色警告,都是来自Android Studio内置的Lint工具检查我们的代码后而作出的动作。 通过配置Lint,也可以消除上面的提醒。那么Lint是什么呢?
lint是什么
Android Studio 提供一个名为Lint的静态代码扫描工具,可以发现并纠正代码结构中的质量问题,而无需实际执行该应用,也不必编写测试用例。 Lint 工具可检查您的 Android 项目源文件是否包含潜在错误,以及在正确性、安全性、性能、易用性、便利性和国际化方面是否需要优化改进。
也就是说,通过Lint工具,我们可以写出更高质量的代码和代码潜在的问题,妈妈再也不用担心我的同事用中文命名了。也可以通过定制Lint相关配置,提高开发效率。
工作流程
上图是Lint工具的工作流程,下面了解相关概念。
-
App Source Files 源文件包含组成 Android 项目的文件,包括 Java 和 XML 文件、图标和 ProGuard 配置文件等。
-
lint.xml 文件 此配置文件可用于指定您希望排除的任何 Lint 检查以及自定义问题严重级别。
-
lint Tool 我们可以通过Android Studio 对 Android 项目运行此静态代码扫描工具。也可以手动运行。Lint 工具检查可能影响 Android 应用质量和性能的代码结构问题。
-
Lint 检查结果 我们可以在控制台(命令行运行)或 Android Studio 的 Inspection Results 窗口中查看 Lint 检查结果。
总的来说,lint Tool 作为整个流程的核心,通过输入源文件(App Source Files)和 lint 的配置(lint.xml),对源文件进行处理分析,得到最终的扫描结果,一般是以 HTML 形式输出。
使用方式
Lint 除了我们熟悉的./gradlew lint
这种运行方式,还可以通过命令行工具${SDK_HOME}/tools/bin/lint
和 Android Studio 的Inspect code
命令来运行。另外在 AS 在编辑器中也可以开启动态监测,Lint 会自动在后台运行,如果有错误会在开发的时候高亮出来。
根据源码,整理工作流程:
自定义规则
先学习相关api,可以快速理解一些概念,可以粗略看过,下结实践再回来看。
重点API
Issue
Issue 代表您想要发现并提示给开发者的一种问题,包含描述、更全面的解释、类型和优先级等等。官方提供了一个 Issue 类,您只需要实例化一个 Issue,并注册到 IssueRegistry 里。
private static final Issue ISSUE = Issue.create("NamingConventionWarning",
"命名规范错误",
"使用驼峰命名法,方法命名开头小写,类大写字母开头",
Category.USABILITY,
5,
Severity.WARNING,
new Implementation(NamingConventionDetecor.class,
EnumSet.of(Scope.JAVA_FILE)));
- id: 唯一的 id,简要表达当前问题。
- briefDescription: 简单描述当前问题。
- explanation: 详细解释当前问题和修复建议。
- category: 问题类别,在 Android 中主要有如下六大类:
- SECURITY: 安全性。例如在 AndroidManifest.xml 中没有配置相关权限等。
- USABILITY: 易用性。例如重复图标,一些黄色警告等。
- PERFORMANCE: 性能。例如内存泄漏,xml 结构冗余等。
- CORRECTNESS: 正确性。例如超版本调用 API,设置不正确的属性值等。
- A11Y: 无障碍 (Accessibility)。例如单词拼写错误等。
- I18N: 国际化 (Internationalization)。例如字符串缺少翻译等。
- priority: 优先级,从 1 到 10,10 最重要。
- severity: 严重程度
- FATAL
- ERROR
- WARNING
- INFORMATIONAL
- IGNORE
- implementation: Issue 和哪个 Detector 绑定,以及声明检查的范围。Scope有如下选择范围:
- RESOURCE_FILE(资源文件)
- BINARY_RESOURCE_FILE(二进制资源文件)
- RESOURCE_FOLDER(资源文件夹)
- ALL_RESOURCE_FILES(所有资源文件)
- JAVA_FILE(Java文件)
- ALL_JAVA_FILES(所有Java文件)
- CLASS_FILE(class文件)
- ALL_CLASS_FILES(所有class文件)
- MANIFEST(配置清单文件)
- PROGUARD_FILE(混淆文件)
- JAVA_LIBRARIES(Java库)
- GRADLE_FILE(Gradle文件)
- PROPERTY_FILE(属性文件)
- TEST_SOURCES(测试资源)
- OTHER(其他)
IssueRegistry
用于注册要检查的Issue(规则),只有注册了Issue,该Issue才能被使用。例如注册上文的命名规范规则。
public class Register extends IssueRegistry {
@NotNull
@Override
public List<Issue> getIssues() {
return Arrays.asList(NamingConventionDetector.ISSUE);
}
}
Detector
另外您还需要实现一个 Detector。Detector 负责扫描代码并找到有问题的地方,然后把它们报告出来。一个 Detector 可以报告多种类型的 Issue,可以针对不同类型的问题使用不同的严重程度,这样用户可以更精确地控制他们想要看到的内容。
Scanner
扫描并发现代码中的Issue,Detector需要实现Scaner,可以继承一个到多个。
- UastScanner: 扫描 Java 文件和 Kotlin 文件
- ClassScanner: 扫描 Class 文件
- XmlScanner: 扫描 XML 文件
- ResourceFolderScanner: 扫描资源文件夹
- BinaryResourceScanner: 扫描二进制资源文件
- OtherFileScanner: 扫描其他文件
- GradleScanner: 扫描 Gradle 脚本
旧版本的JavaScanner、JavaPsiScanner随着版本的更新已经被UastScanner替代了。
实践
新建lint module
新建一个 module,在选择 module 类型的界面,选择 Java or Kotlin Library,然后给新建的 module 命名,例如 lint。 在新建的 module 下的 build.gradle 文件添加依赖:
// Lint
compileOnly "com.android.tools.lint:lint-api:26.3.2"
compileOnly "com.android.tools.lint:lint-checks:26.3.2"
## 在 app module 添加 lintChecks
为了方便在写完 Lint 检查的代码后进行测试,在 app module 下的 build.gradle 文件添加依赖:
dependencies { lintChecks project(':lint')}
扫描xml文件
要分析一个 XML 文件,可以重写 visitDocument() 方法。这个方法每个 XML 文件都会调用一次,然后传入 XML DOM 模型,之后您就可以自己遍历并做分析。 但是呢,我们通常只关注一些特定的标签和一些特定的属性,为了让扫描更快,Detector 可以指定我们关注的元素和属性。
要筛选我们关注的元素或属性,只需实现 getApplicableElements() 或 getApplicableAttributes() 方法,并返回一个标签或属性名称的字符串列表。然后再实现 visitElement() 或 visitAttribute() 方法,这两个方法针对每个指定的元素和属性都会调用一次。
public class MyDetector extends ResourceXmlDetector {
public static final Issue ISSUE = Issue.create(
"MyId",
"My brief summary of the issue",
"My longer explanation of the issue",
Category.CORRECTNESS, 6, Severity.WARNING,
new Implementation(MyDetector.class, Scope.RESOURCE_FILE_SCOPE));
@Override
public Collection<String> getApplicableElements() {
return Collections.singletonList(
"com.google.io.demo.MyCustomView");
}
@Override
public void visitElement(XmlContext context, Element element) {
if (!element.hasAttributeNS(
"http://schemas.android.com/apk/res/com.google.io.demo",
"exampleString")) {
context.report(ISSUE, element, context.getLocation(element),
"Missing required attribute 'exampleString'");
}
}
}
扫描Java/Kotlin文件
public class NamingConventionDetector extends Detector implements Detector.UastScanner {
//定义命名规范规则
public static final Issue ISSUE = Issue.create("NamingConventionWarning",
"命名规范错误",
"使用驼峰命名法,方法命名开头小写",
Category.USABILITY,
5,
Severity.WARNING,
new Implementation(NamingConventionDetector.class,
EnumSet.of(Scope.JAVA_FILE)));
//返回我们所有感兴趣的类,即返回的类都被会检查
@Nullable
@Override
public List<Class<? extends UElement>> getApplicableUastTypes() {
return Collections.<Class<? extends UElement>>singletonList(UClass.class);
}
//重写该方法,创建自己的处理器
@Nullable
@Override
public UElementHandler createUastHandler(@NotNull final JavaContext context) {
return new UElementHandler() {
@Override
public void visitClass(@NotNull UClass node) {
node.accept(new NamingConventionVisitor(context, node));
}
};
}
//定义一个继承自AbstractUastVisitor的访问器,用来处理感兴趣的问题
public static class NamingConventionVisitor extends AbstractUastVisitor {
JavaContext context;
UClass uClass;
public NamingConventionVisitor(JavaContext context, UClass uClass) {
this.context = context;
this.uClass = uClass;
}
@Override
public boolean visitClass(@NotNull UClass node) {
//获取当前类名
char beginChar = node.getName().charAt(0);
int code = beginChar;
//如果类名不是大写字母,则触碰Issue,lint工具提示问题
if (97 < code && code < 122) {
context.report(ISSUE,context.getNameLocation(node),
"the name of class must start with uppercase:" + node.getName());
//返回true表示触碰规则,lint提示该问题;false则不触碰
return true;
}
return super.visitClass(node);
}
@Override
public boolean visitMethod(@NotNull UMethod node) {
//当前方法不是构造方法
if (!node.isConstructor()) {
char beginChar = node.getName().charAt(0);
int code = beginChar;
//当前方法首字母是大写字母,则报Issue
if (65 < code && code < 90) {
context.report(ISSUE, context.getLocation(node),
"the method must start with lowercase:" + node.getName());
//返回true表示触碰规则,lint提示该问题;false则不触碰
return true;
}
}
return super.visitMethod(node);
}
}
}
上文NamingConventionDetector类,已经是全部代码,只检查类名和方法名是否符合驼峰命名法,可以根据具体需求,重写抽象类AbstractUastVisitor的visitXXX方法。
如果处理特定的方法或者其他,也可以使用默认的处理器。重写Scanner相关方法。例如
@Override
public List<String> getApplicableMethodNames() {
return Arrays.asList("e","v");
}
表示e(),v()方法会被检测到,并调用visitMethod()方法,实现自己的逻辑。
@Override
public void visitMethod JavaContext context, JavaElementVisitor visitor, PsiMethodCallExpression call, PsiMethod method) {
//todo something
super.visitMethod(context, visitor, call, method);
}
报告
如果 Detector 定位到一个问题,需要使用 Context
对象(Detector 的每个方法都会传入进来)调用 report()
方法来报告错误。
private void reportError(XmlContext context, Element element) {
context.report(
ISSUE,
element,
context.getLocation(element),
"请不要在 AndroidManifest.xml 文件里同时设置方向和透明主题"
);
}
除了列出要报告的问题外,还需要提供位置、作用域节点和错误提示消息:
- 作用域节点:对于 XML 和 Java 源文件,是指发生的错误周围最近的 XML DOM 或 Parse AST 树节点,例如上面传入的
element
对象。 - 位置:是指错误发生的位置。一般只需将 AST/XML 节点传递给
context.getLocation()
方法,该方法将创建一个具有正确文件名和与给定节点相对应的偏移量的Location
。如果你的错误与某个属性有关,则传递该属性,以使该问题更好地指出错误发生的位置。
注册自定义的Issue
public class Register extends IssueRegistry {
@NotNull
@Override
public List<Issue> getIssues() {
return Arrays.asList(NamingConventionDetector.ISSUE);
}
@Override
public int getApi() {
return ApiKt.CURRENT_API;
}
}
gradle绑定
jar {
manifest {
attributes 'Lint-Registry-v2': 'com.liujian.lib.MyIssueRegistry'
}
}
检测
在MainActivity写一个名字首字母大写的方法
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Test()
}
private fun Test(){
}
}
然后在Terminal输入命令
./gradlew :app:lintDebug
观察BUILD SUCCESSFUL后在app/build/reports文件夹下有html报告,
在浏览器显示后可以看到我们刚写的测试错误的报告
Jenkins集成
增量检查
使用./gradlew lint
执行一次完整的 Lint 检查需要耗费很长时间,长时间的检查等待成为大家的心头之痛,阻碍静态检查在各项目上的推进。很容易想到,其实每次 commit 提交没必要执行完整的 Lint,只检查修改过的那部分代码就行。
首先,先来看下最简单的命令行方式是如何实现指定文件 Lint 的,找到命令的入口文件 Main,关键在于以下几行
LintGradleClient
内部封装了只有一个参数的 run 方法,并且在调用父类的 run 方法时传入了一个空列表。如果传空列表,这里会发生什么呢?沿着LintCliClient.run
的调用往下看,中间省略掉若干过程,最后在LintDriver
里找到这么一段:
当传入 files 不为空时,
LintDriver
会执行增量检查,否则会读取项目源文件夹下的所有文件,因此每次 gradle 执行 lint 都退化成了全量扫描。
所以这里实现增量扫描就很简单了,只需要继承LintGradleClient
,对 files 入参做一些定制处理就行。通过自定义 gradle plugin,封装了一个IncrementLintTask
,通过 Git diff 命令找到差异文件,然后对指定的差异文件执行 Lint,最后实现的执行方式就像这样
实战检测规则梳理
感谢
转载自:https://juejin.cn/post/7028762160645799950