Android Lint
Lint, 静态代码扫描工具,可以让我们在不执行应用或单元测试的情况下,找出代码中有问题,可以优化的地方。
常见的就是,我们写代码的过程中,Android Studio 有时候会把我们有问题的代码用黄色高亮,并提示我们修改。有的公司也会在代码提交前跑一遍 Lint ,检查你的代码是否有问题,或者是否符合规范。
Why
使用 Lint 可以:
- 尽早发现问题,解决问题
- 避免重复犯错
- 规范化代码
- SDK 中带 Lint 可以避免用户乱使用,提前指出错误
How
使用 Android Studio 自带 Lint
Inspect Code
我们可以直接点击 Android Studio 中 Analyze -> Inspect Code 执行 Lint.
我们可以自定义 Lint 执行的范围。
想知道会扫描哪些内容的,可以通过点击图片中的位置,查看有哪些检查项。
扫描完成后,扫描的结果就会展示在下方,现在你就可以根据提示对代码进行修改了。
Gradle Command
我们也可以在命令行中执行下面的命令
# input
./gradlew lintDebug
扫描结束后,会生成 HTML 和 XML 的文档.
# output
> Task :app:lintDebug
Wrote HTML report to file://***/code/JLint/app/build/reports/lint-results-debug.html
Wrote XML report to file://***/code/JLint/app/build/reports/lint-results-debug.xml
BUILD SUCCESSFUL in 15s
27 actionable tasks: 2 executed, 25 up-to-date
用浏览器打开链接就可以查看 Lint 报告,然后我们就可以根据报告对代码进行修改了。
自定义 Lint 规则
虽然默认的 Lint 已经带有很多有用的规则了。但是不一定适用于自己或者团队开发的需求,这时候就需要我们自己自定义了。
创建项目
这里建议,所有的规则可以写在一个 module 当中,便于不同项目使用。
File > new > new module > Java or Kotlin Library
首先我们先创建一个 module ,类型为 Java or Kotlin LIbrary。并在 build.gradle 中引入 Lint 需要的依赖。
compileOnly "com.android.tools.lint:lint-api:27.1.2"
compileOnly "com.android.tools.lint:lint-checks:27.1.2"
compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.30"
testImplementation "junit:junit:4.13.1"
testImplementation "com.android.tools.lint:lint:27.1.2"
testImplementation "com.android.tools.lint:lint-tests:27.1.2"
testImplementation "com.android.tools:testutils:27.1.2"
编写规则
这里会用一个检查日志工具使用是否正确的规则来作为例子。
主要对象
首先了解一下几个基本的类。
Issue
用于描述应用中潜在的问题.
通过内部的静态方法create(...)构建,主要的成员变量有:
id, Issue 唯一的标识briefDescription, 关于问题的简短描述explanation, 问题的总描述,包含修改建议category, Issue 的类别,有LINT,SECURITY,CORRCTNESS等priority, 1 - 10 ,10 表示最重要的severity, 严重程度,ERROR就必须要更正,WARNING则是会黄色高亮Implememtation, Issue 的实现,就是关联Detector并指定扫描范围。
Detector
可以用于发现一个特定的或者一组相关联的问题。每个问题都会被唯一地标记为一个 Issue
Detector 被调用的时候是有特定的顺序的:
- Manifest
- Resource (按字母顺序扫描,eg,先扫描
layout,再扫描values) - Java source
- Java classes
- Gradle
- Generic
- Proguard file
- Property file
具体的扫描规则可以通过 Scanner 来定义。
Scanner
不同的文件不同的 Scanner 进行分析
UastScanner, Java or Kotlin 文件ClassScanner, 字节码或编译后的 class 文件BinaryResourceScanner, 二进制资源文件ResourceFolderScannerXmlScannerGradleScannerOtherFileScanner
IssueRegistry
Issue 需要通过在 IssueRegistry 中注册,在执行 Lint 的时候才会被执行。
检查 Log 工具的使用
提示用封装过的 HLog, 避免直接使用系统的 Log 进行日志输出。
创建 Detector
因为我们是要检查代码文件中的内容,所以我们还要实现 UastScanner
class LogDetector : Detector(), Detector.UastScanner {
...
}
创建 Issue
编写 Issue, 描述我们检查的问题。
class LogDetector : Detector(), Detector.UastScanner {
...
companion object {
val ISSUE: Issue = Issue.create(
id = "LogChecker",
briefDescription = "Check the use of Log.",
explanation = "Use HLog instead of Log.",
category = Category.CORRECTNESS,
priority = 1,
severity = Severity.WARNING,
implementation = Implementation(
LogDetector::class.java,
Scope.JAVA_FILE_SCOPE
)
)
}
}
补充 Detector 的扫描代码
我们只需要找 Log 方法的使用代码,所以重写 getApplicableMethodNames, 返回 Log 的方法列表,这样当扫描到这些方法的时候,就会回调到 visitMethodCall 中。这可以帮助我们专注于那些可能有问题且需要修复的代码段。
class LogDetector : Detector(), Detector.UastScanner {
override fun getApplicableMethodNames(): List<String>? {
return listOf("i", "d", "e", "w", "v")
}
}
接着就是重写 visitMethodCall 方法,进一步判断代码是否有问题。
class LogDetector : Detector(), Detector.UastScanner {
override fun getApplicableMethodNames(): List<String>? {
...
}
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
super.visitMethodCall(context, node, method)
//判断调用者是不是 Log
if (!context.evaluator.isMemberInClass(method, "android.util.Log")) {
return
}
//代码有问题,抛出 Issue
context.report(
ISSUE,
method,
context.getCallLocation(node, includeReceiver = true, includeArguments = true),
"Use HLog instead of Log"//鼠标放在问题代码上的提示就是这个
)
}
注册 Issue
继承 IssueRegistry, 注册 Issue
class JIssueRegistry : IssueRegistry() {
override val issues: List<Issue>
get() = arrayListOf(
LogDetector.ISSUE
)
override val api: Int
get() = CURRENT_API
}
在模块的 module 中添加
jar {
manifest {
attributes("Lint-Registry-V2": "xyz.juncat.jlintrules.JIssueRegistry")
}
}
使用 Lint
使用的时候就在 build.gradle(:app) 中加入
dependencies {
lintChecks project(':JLintRules')
}
这时候你就会发现,:app 的代码中,使用了原生 Log 的方法,都会被黄色高亮起来。
每次修改 Lint 的代码后记得 Rebuild 一下
修改问题代码
平时写代码,如果没有 import 的话,在对应的类名上按一下 Alt+Enter 就可以自动给我们 import 了。自定义 Lint 也可以同样地直接修复代码中的问题。
只要我们在 Detector 抛出错误的地方加入 LintFix 就可以了
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
super.visitMethodCall(context, node, method)
if (!context.evaluator.isMemberInClass(method, "android.util.Log")) {
return
}
//获取方法中的参数列表
val args = node.valueArguments
val fix = LintFix.create()
.name("replace Log to HLog")
.replace()//替换为下面的代码
.with("HLog.${node.methodName}(${args[0].asSourceString()})")
.build()
context.report(
ISSUE,
method,
context.getCallLocation(node, includeReceiver = true, includeArguments = true),
"Use HLog instead of Log",
fix
)
}
Rebuild 代码,再看看用了 Log 的地方,就会提示修改的方法了。直接按下 Option+Shift+Enter(mac), Alt+Enter(linux,window) 就可以替换为我们自己的方法了。
Debug Lint
如果想通过 Debug 来检查 Lint 的执行情况,或者是通过 Debug 来看 Lint 中一些参数的具体值。这里提供两种方法。
修改 Gradle 执行参数
在引用了自定义 Lint 规则的项目中,编写不符合规则的代码,在 Detector 中加入断点。
通过下面的命令执行 Lint
./gradlew --no-daemon -Dorg.gradle.debug=true lintDebug
这时 Gradle 会暂停执行,直到你连入 debugger
双击 Shift ,输入 Attach Debugger to Process,回车,这时候 Lint 进程就会继续执行,程序也会正确的停在你的断点上了。
注意不要选了
Attach Debugger to Android Process
单元测试【推荐】
- 继承
LintDetectorTest() - 重写
getDetector()和getIssues() - 编写测试用的代码段,也可以是代码文件
- 调用
lint()进行测试,如果测试代码里用到了 Android SDK 里的类,要加上requireCompileSdk()和sdkHome()方法,指定 SDK。 - Debug 运行测试方法
class LogDetectorTest : LintDetectorTest() {
private var sdkDir = ""
override fun setUp() {
super.setUp()
val p = Properties(System.getProperties())
p.load(FileInputStream("../local.properties"))
sdkDir = p.getProperty("sdk.dir")
}
private val inCorrectMethodCallKt = """
package xyz.juncat.jlint
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.i("TAG", "onCreate: ")
}
}
""".trimIndent()
private val correctMethodCallKt = """
package xyz.juncat.jlint
class Test {
fun test() {
HLog.i("TAG","test");
}
}
""".trimIndent()
override fun getDetector(): Detector = LogDetector()
override fun getIssues(): MutableList<Issue> = mutableListOf(LogDetector.ISSUE)
@Test
fun testInCorrectLogCall() {
lint().requireCompileSdk()
.sdkHome(File(sdkDir))
.files(kotlin(inCorrectMethodCallKt).indented())
.run()
.expectWarningCount(1)
}
@Test
fun testCorrectLogCall() {
lint().requireCompileSdk()
.sdkHome(File(sdkDir))
.files(kotlin(correctMethodCallKt).indented())
.run()
.expectClean()
}
}
打包
为了方便复用,我们还可以把项目打包成 AAR
首先需要新建一个 Android Module ,如 JLintAAR
然后在其 build.gradle 中添加
dependencies {
lintPublish project(':JLintRules')
}
这样,只要我们构建这个 JLintAAR 就可以生成 AAR 包,别的项目直接引用就可以了。
最后
这里只是写出了基本的方法,还有很多功能需要去探索,所以有什么错误的地方,希望大家指出。有什么好的用法,也欢迎留言。
lint-checks 下也还有很多自带的 Detector,可以学习一下,只是 API 可能有些差异。
这里提供我的 Demo,以供参考。
Lint 的代码都是 Beta 版的,有不一样的地方最好还是看看源码,或者文档