阅读 494

Android Lint

Android Lint

Lint, 静态代码扫描工具,可以让我们在不执行应用或单元测试的情况下,找出代码中有问题,可以优化的地方。

常见的就是,我们写代码的过程中,Android Studio 有时候会把我们有问题的代码用黄色高亮,并提示我们修改。有的公司也会在代码提交前跑一遍 Lint ,检查你的代码是否有问题,或者是否符合规范。

Why

使用 Lint 可以:

  • 尽早发现问题,解决问题
  • 避免重复犯错
  • 规范化代码
  • SDK 中带 Lint 可以避免用户乱使用,提前指出错误

How

使用 Android Studio 自带 Lint

Inspect Code

我们可以直接点击 Android Studio 中 Analyze -> Inspect Code 执行 Lint.

Inspect Code

我们可以自定义 Lint 执行的范围。

想知道会扫描哪些内容的,可以通过点击图片中的位置,查看有哪些检查项。

Inspection Profile

扫描完成后,扫描的结果就会展示在下方,现在你就可以根据提示对代码进行修改了。

Inspection Result

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 报告,然后我们就可以根据报告对代码进行修改了。 HTML doc

自定义 Lint 规则

虽然默认的 Lint 已经带有很多有用的规则了。但是不一定适用于自己或者团队开发的需求,这时候就需要我们自己自定义了。

Lint 的 api 目前都还是 Beta 版本的,建议还是留意一下官方的文档Demo

创建项目

这里建议,所有的规则可以写在一个 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(...)构建,主要的成员变量有:

  1. id, Issue 唯一的标识
  2. briefDescription, 关于问题的简短描述
  3. explanation, 问题的总描述,包含修改建议
  4. category, Issue 的类别,有 LINT, SECURITY, CORRCTNESS
  5. priority, 1 - 10 ,10 表示最重要的
  6. severity, 严重程度,ERROR 就必须要更正,WARNING 则是会黄色高亮
  7. Implememtation, Issue 的实现,就是关联 Detector 并指定扫描范围。
Detector

可以用于发现一个特定的或者一组相关联的问题。每个问题都会被唯一地标记为一个 Issue

Detector 被调用的时候是有特定的顺序的:

  1. Manifest
  2. Resource (按字母顺序扫描,eg,先扫描 layout,再扫描 values)
  3. Java source
  4. Java classes
  5. Gradle
  6. Generic
  7. Proguard file
  8. Property file

具体的扫描规则可以通过 Scanner 来定义。

Scanner

不同的文件不同的 Scanner 进行分析

  • UastScanner, Java or Kotlin 文件
  • ClassScanner, 字节码或编译后的 class 文件
  • BinaryResourceScanner, 二进制资源文件
  • ResourceFolderScanner
  • XmlScanner
  • GradleScanner
  • OtherFileScanner
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 一下

Example

修改问题代码

平时写代码,如果没有 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) 就可以替换为我们自己的方法了。

Replace

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

Attach to Process

单元测试【推荐】

  1. 继承 LintDetectorTest()
  2. 重写 getDetector()getIssues()
  3. 编写测试用的代码段,也可以是代码文件
  4. 调用 lint() 进行测试,如果测试代码里用到了 Android SDK 里的类,要加上 requireCompileSdk()sdkHome() 方法,指定 SDK。
  5. 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 可能有些差异。

lint-checks

这里提供我的 Demo,以供参考。

Lint 的代码都是 Beta 版的,有不一样的地方最好还是看看源码,或者文档

参考资料

文章分类
Android
文章标签