文章目录
- Lint
- 自定义Lint
-
- Implementation
- Issue
- Detector
- 如何使用
- 这一切值得吗
原文地址:What is Android Lint and how it helps write maintainable code
一些开发人员由于不够谨慎,从而导致某些代码会有瑕疵。以下列举几个经常容易犯错的场景:比如旧版本的代码不支持新版本的功能,比如需要某些特定的权限,比如缺少翻译等等。
更加最重要的是,Java、 Kotlin 和其他的编程语言一样,都有自己的一套编程结构。如果不注意的话,在某种情况下,某些编码习惯,可能会导致性能低下。
Lint
我们可以通过使用Lint 去避免上述问题的发生。它是个用于在代码编译之前检测静态代码的工具。它针对源码进行多次检测,可以检测出类似:未使用的变量、方法参数,待简化的判断条件,错误的作用域,未定义的变量、方法,待优化代码等问题。对于安卓的开发人员来说,我们有一套已经写好的Lint checks,可以参考 此处。
但是有时候,我们需要检查代码中比较特殊的问题,而且已经存在的Lint check 并不满足要求的时候,我们需要自定义Lint check。
自定义Lint
在我们开始编码之前,我们先确定我们的目标,并了解如何根据Lint Api去实现我们的目标。 我们的目标是:创建一个Lint check 去检查一个对象的错的方法调用。具体来说就是检测在安卓视图组件上设置点击监听器的方法是否会多次连续点击,这样我们就可以避免打开相同的Activity 或者 多次调用API了。
自定义Lint check的工程和标准的java module类似,最简单的办法就是,创建一个简单的基于gradle的项目(并不一定是Android项目)。(可以新建java library,这个也可以)
下一步,你需要添加lint的依赖关系,再你刚新建的module 的build.gradle 文件中添加如下依赖:
compileOnly "com.android.tools.lint:lint-api:$lintVersion"
compileOnly "com.android.tools.lint:lint-checks:$lintVersion"
这个将会同时设置你的编写(writing)和测试(testing)相关的依赖,我通过研究这个话题,学到了一些技巧,如上面配置的 lintVersion ,必须是你的 gradlePluginVersion+ 23.0.0。 对于使用Android Studio 创建的项目来说,gradlePluginVersion 被定义在根文件下的项目级别的build.gradle文件夹内(比如:
classpath 'com.android.tools.build:gradle:3.1.2'
中gradlePluginVersion就是3.1.2)。 最后一个正式版本是3.3.0 这也就意味着lintVersion 必须是26.3.0
每一个 lint check 包含四部分
- Issue:一个需要我们尝试去阻止在我们代码中发生的问题。 当lint check 检测未通过的时候,它需要通知用户。 (将代码中的问题通知用户)
- Detector:一个使用Lint Api来发现我们源码中暴露的问题的工具 (使用lint api检查问题)
- Implementation:问题可能发生的范围(源码文件 xml文件 编译后的代码)(确定问题检查范围)
- Registry:自定义的lint check的 registry 同之前预定义的已经存在的 register 一起使用 (注册使其生效)
下面将分别介绍上面的四部分,跟随着一步步去实践,去完成对Lint的认识。
Implementation
让我们针对我们的自定义lint check,开始创建一个 implementation 。 对于每一个implementation 的实现都需要传入一个具体的实现detector 接口的类和指定的检查范围,如下方代码:
此处的Scope.JAVA_FILE_SCOPE 同样也适用于检查Kotlin
Issue
下一步就是使用我们的implementation 去创建我们的Issue,每个Issue 都包含以下几个部分
- ID:唯一标识
- Description:剪短的摘要(5-6个字)
- Explanation:完整的说明,包含建议和如何修复
- Category: 分类(性能、注释、安全)
- Priority:用于表明Issue的重要性,共10档,从1-10,10是最重要,这个可以在执行Lint
- Task之后生成的报告中用于排序。
- Severity:严重性(fatal(致命错误)、error(一般性错误)、warning(警告)、info(提示)、ignore(忽略))
- Implementation:用于检查代码寻找对应的问题
val ISSUE_CLICK_LISTENER = Issue.create( id = "UnsafeClickListener", briefDescription = "Unsafe click listener", explanation = """" This check ensures you call click listener that is throttled instead of a normal one which does not prevent double clicks. """.trimIndent(), category = Category.CORRECTNESS, priority = 6, severity = Severity.WARNING, implementation = correctClickListenerImplementation )
Detector
Lint Api提供的接口,你可以实现它的接口然后去检查你想要校验的范围内的东西。针对这些接口暴露的方法你可以针对你感兴趣的去重写(overrrid)或 获得其源代码。
- UastScanner: 用于扫描Java和kotlin的文件
- ClassScanner:用于扫描编译后的文件(字节码)
- BinaryResourceScanner:二进制的资源,类似 bitmaps 或者在res/raw的文件
- ResourceFolderScanner:资源文件
- XmlScanner: xml files
- GradleScanner: gradle files
- OtherFileScanner: 任何文件
此外,Detector类是一个基类,它具有上述每个接口公开的所有方法的虚拟实现(抽象类),因此,如果只需要一个方法,则不必强制实现完整的接口。
现在,我们准备实现一个Detector,它将用于实现刚才我们定的目标的那个功能。
private const val REPORT_MESSAGE = "Use setThrottlingClickListener"
/**
* Custom detector class that extends base Detector class and specific
* interface depending on which part of the code we want to analyze.
*/
class CorrectClickListenerDetector : Detector(), Detector.UastScanner {
/**
* Method that defines which elements of the code we want to analyze.
* There are many similar methods for different elements in the code,
* but for our use-case, we want to analyze method calls so we return
* just one element representing method calls.
*/
override fun getApplicableUastTypes(): List<Class<out UElement>>? {
return listOf<Class<out UElement>>(UCallExpression::class.java)
}
/**
* Since we've defined applicable UAST types, we have to override the
* method that will create UAST handler for those types.
* Handler requires implementation of an UElementHandler which is a
* class that defines a number of different methods that handle
* element like annotations, breaks, loops, imports, etc. In our case,
* we've defined only call expressions so we override just this one method.
* Method implementation is pretty straight-forward - it checks if a method
* that is called has the name we want to avoid and it reports an issue otherwise.
*/
override fun createUastHandler(context: JavaContext): UElementHandler? {
return object: UElementHandler() {
override fun visitCallExpression(node: UCallExpression) {
if (node.methodName != null && node.methodName?.equals("setOnClickListener", ignoreCase = true) == true) {
context.report(ISSUE_CLICK_LISTENER, node, context.getLocation(node), REPORT_MESSAGE, createFix())
}
}
}
}
/**
* Method will create a fix which can be trigger within IDE and
* it will replace incorrect method with a correct one.
*/
private fun createFix(): LintFix {
return fix().replace().text("setOnClickListener").with("setThrottlingClickListener").build()
}
}
我们需要做的最后一件事是向Registry中添加我们的Issue,并告诉Lint,我们有自定义的Registry,他应该和默认问题一起使用。
class MyIssueRegistry : IssueRegistry() {
override val issues: List<Issue> = listOf(ISSUE_CLICK_LISTENER)
}
在当前的module中的build.gradle中按照如下设置
jar {
manifest {
attributes("Lint-Registry-v2": "co.infinum.lint.MyIssueRegistry")
}
}
其中co.infinum.lint是MyIssueRegistry类的包。你需要根据你的实际情况,填写你的对应的包名类名,现在,您可以使用gradlew脚本运行jar任务,库应该出现在build / libs目录中。
如何使用
新的Lint checks已准备好用于项目。如果此Lint checks可以应用于所有项目,则可以将其放在.android / lint文件夹中(如果它不存在,则可以创建它),该文件夹应位于您的主文件夹中。(windows 中c 盘的位置,mac中全局的位置,不是project的根目录)
此外,您可以将检查作为项目中的模块进行开发,并使用lintChecks方法将该模块包含为任何其他依赖项。如果您将jar文件上传到Bintray,也可以使用此方法。
这一切值得吗
Lint是每个开发人员都应该非常熟练掌握的工具,因为它具有能够预先检测代码中潜在问题的能力。虽然因为其Api过于复杂导致自定义Lint checks并不容易,但是一旦学会使用并应用起来,可以节省大量的时间和精力,所以它们绝对值得去学习。