1

一、概述

Android Studio 提供了一个名为 Lint 的代码扫描工具,可帮助开发者发现并更正代码结构质量方面的问题,并且无需您实际执行应用,也不必编写测试用例。系统会报告该工具检测到的每个问题并提供问题的描述消息和严重级别,以便开发者可以快速确定需要优先进行的关键改进。此外,我们还可以降低问题的严重级别以忽略与项目无关的问题,或者提高严重级别以突出特定问题。

作为一款代码检查工具,Lint 工具可以检查 Android 项目源文件是否有潜在的 bug,以及在正确性、安全性、性能、易用性、无障碍性和国际化方面是否需要优化改进。使用 Android Studio 时,无论何时构建应用,都会运行配置的 lint 和 IDE 检查。并且,您可以手动运行检查或从命令行运行 Lint。

下面是 Lint 工具执行代码扫码的工作示意图:

image.png

Lint 工具有几个基础的概念需要帮大家理清楚:

  • 应用的源文件:源文件包括组成 Android 项目的各种文件,例如 Java、Kotlin 和 XML 文件,以及图标和 ProGuard 配置文件。
  • lint.xml 文件:一个配置文件,可用于指定要排除的任何 lint 检查以及自定义问题严重级别。
  • lint 工具:一个静态代码扫描工具,可以从命令行或在 Android Studio 中对 Android 项目运行该工具来进行检查。lint 工具用于检查代码是否存在可能影响 Android 应用的质量和性能的结构问题。
  • lint 检查结果:Lint检查的结果,可以在控制台或 Android Studio 的 Inspection Results 窗口中查看 lint 检查结果。

二、Lint 对于团队开发的重要性

Android 官方对 Lint功能的描述非常准确:通过进行 lint 检查来改进代码 。这里关键词是改进,也就是当开发者写了一行语法正确并且运行正常的代码,但是它可能不是最优或最安全的写法,或最优的API组合,经过Lint 检测将发现这些坏代码,通过实时提示来辅助开发者改进为更健壮更安全的写法。下面是一些具体的使用场景说明:

2.1 案例1: 检测代码规范

在接入ViewBinding时,为了保证xml内的view Id 与 ViewBinding 保持一致性的驼峰命名,需要避免使用 btn_xx 下划线命名,这样方便阅读代码与检索代码。通常像这样的规范会在团队内集中同步,如何能保证在这个规范自同步后新提交的代码viewId 100% 是驼峰命名呢?注意这里说的是达到100%符合规范,要彻底杜绝因人为导致的疏漏。

在实践中从两个点下手就能100%达成这一目的,第一是本地借助IDE实时检测xml文件,辅助开发者快速改进代码,像下面这样:

image.png

2.2 案例2:屏蔽不安全的原生API

在使用 Integer.parseInt 等方法时有可能会触发 NumberFormatException 导致Crash。有两种方式来规避,一是使用统一封装的工具类,二是必须包一个try catch。下图是实现这个检测要求的Lint效果。

image.png

2.3 案例3:检测导致Crash的风险代码

在App侧一定会用到序列化用于解析接口数据,实现序列化会带来崩溃风险,如果一个实现了序列化接口的类,而它的成员变量应用的类未实现序列化接口,就有可能引发崩溃。这种问题靠code review 来杜绝难度是蛮大的,通常也希望线上code review 时更多关注业务逻辑而非低级的编码错误,这样能有效减少 code review耗时。

针对这类问题需要编写一个Lint规则,检测实现了序列化接口的类其成员变量的类型是否实现了序列化接口,比如下面这样:

image.png

上面三个例子分别从团队规范与风险代码管理阐述了Lint的作用,也体现了出来一点,Lint 天生是依赖团队来放大价值的,即将团队的沉淀的技术经验,通过Lint快速触达到每一个成员与每一个项目中,依此来不断拉高团队代码质量的底线,并长期保证不劣化。

三、基本使用

3.1 命令行运行 lint

我们可以使用Android Studio 来运行 Gradle命令,使用 Gradle 封装容器对项目调用 lint 任务,例如:

//Windows
gradlew lint 


// Linux 或 Mac
./gradlew lint

运行完之后,你将得到一份如下的报告:

> Task :app:lintDebug
Wrote HTML report to file:<path-to-project>/app/build/reports/lint-results-debug.html

当然,我们还可以针对Debug和Release版本进行检查,对应的命令如下:

./gradlew lintRelease

不过,运行上面命令的前提是你已经配置好了lint的检查规则。

3.2 lint 配置

默认情况下,当我们运行 lint 扫描时,凡是 lint 可帮助检查的问题,lint 工具都会检查是否存在。不过,我们可以限定让 lint 只检查是否存在某些问题,并为这些问题指定严重级别。例如,我们可以禁止 lint 检查是否存在与项目无关的特定问题,还可以将 lint 配置为以较低的严重级别报告非关键问题。支持的Lint检查的级别如下:

  • 全局(整个项目)
  • 项目模块
  • 生产模块
  • 测试模块
  • 打开的文件
  • 类层次结构
  • 版本控制系统 (VCS) 范围

3.2.1 在 Android Studio 中配置 lint

我们可以使用 Android Studio内置的 lint 工具来检查代码,并且可以通过以下两种方式查看警告和错误:

  • 在代码编辑器中查看以弹出的文本形式显示的警告和错误。lint 发现问题后,会用黄色突出显示有问题的代码,而对于更严重的问题,它会在代码下面添加红色下划线。
  • 依次点击 Analyze > Inspect Code 后,在 lint Inspection Results 窗口中查看。

image.png

3.2.2 lint 文件配置

我们可以在 lint.xml 文件中指定 lint 检查偏好设置,如果是手动创建此文件,可以将其放置在 Android 项目的根目录下。lint.xml 文件由封闭的 <lint> 父标记组成,此标记包含一个或多个 <issue> 子元素,如下所示。

<?xml version="1.0" encoding="UTF-8"?>
    <lint>
        <!-- list of issues to configure -->
</lint>

当然,我们还可以通过在 <issue> 标记中设置严重级别属性来更改某个问题的严重级别或禁止对该问题进行 lint 检查。下面是一个示例:

<?xml version="1.0" encoding="UTF-8"?>
<lint> 
    <issue id="IconMissingDensityFolder" severity="ignore" />
    <issue id="ObsoleteLayoutParam">
        <ignore path="res/layout/activation.xml" />
        <ignore path="res/layout-xlarge/activation.xml" />
    </issue>
   
    <issue id="UselessLeaf">
        <ignore path="res/layout/main.xml" />
    </issue>
    <issue id="HardcodedText" severity="error" />
</lint>   

3.2.3 配置 Java、Kotlin 和 XML 源文件的 lint 检查

有时候,我们需要专门对 Android 项目中的某个类或方法停用 lint 检查,此时只需要在代码块中添加 @SuppressLint 注解即可。以下示例展示了如何对 onCreate 方法中的 NewApi 问题停用 lint 检查。

@SuppressLint("NewApi")
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.main)

要禁止 lint 检查文件中的所有问题,请使用 all 关键字,比如:

@SuppressLint("all")

而对于XML文件来说,可以使用 tools:ignore 属性对 XML 文件的特定部分停用 lint 检查,比如,下面的示例中对 <TextView> 子元素停用 lint 检查:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    tools:ignore="UnusedResources" >


    <TextView
        android:text="@string/auto_update_prompt" />
</LinearLayout>

同样,要禁止 lint 检查 XML 元素中的所有问题,请使用 all 关键字。

tools:ignore="all"

3.3 通过 Gradle 配置 lint 选项

通过 Android Plugin for Gradle工具,开发者可以使用模块级 build.gradle 文件中的 lint{} 代码块配置某些 lint 选项,例如要运行或忽略哪些检查。

android {
    ...
    lintOptions {
        // Turns off checks for the issue IDs you specify.
        disable("TypographyFractions")
        disable("TypographyQuotes")
        // Turns on checks for the issue IDs you specify. These checks are in
        // addition to the default lint checks.
        enable("RtlHardcoded")
        enable("RtlCompat")
        enable("RtlEnabled")
        // To enable checks for only a subset of issue IDs and ignore all others,
        // list the issue IDs with the 'check' property instead. This property overrides
        // any issue IDs you enable or disable using the properties above.
        checkOnly("NewApi", "InlinedApi")
        // If set to true, turns off analysis progress reporting by lint.
        quiet = true
        // If set to true (default), stops the build if errors are found.
        abortOnError = false
        // If true, only report errors.
        ignoreWarnings = true
        // If true, lint also checks all dependencies as part of its analysis. Recommended for
        // projects consisting of an app with library dependencies.
        isCheckDependencies = true
    }
}
...

3.4 示例应用

使用Lint前,在相应module的build.gradle文件的android配置项中,通过lintOptions来配置相应的属性,比如:

android {
    lintOptions {
        // 设置为 true 后,release 构建都会以 Fatal 的设置来运行 Lint。
        // 如果构建时发现了致命(Fatal)的问题,会中止构建(具体由 abortOnError 控制)
        checkReleaseBuilds true
        // 设置为 true,则当 Lint 发现错误时停止 Gradle 构建
        abortOnError false
        // 重新指定 Lint 规则配置文件
        lintConfig file("default-lint.xml")
        // 仅检查指定的问题(根据规则的 id 指定)
        check 'NewApi', 'InlinedApi'
        // 设置为 true 则检查所有的问题,包括默认不检查问题
        checkAllWarnings true
        // 不检查指定的问题(根据规则的 id 指定)
        disable 'TypographyFractions','TypographyQuotes'
        // 检查指定的问题(根据规则的 id 指定)
        enable 'RtlHardcoded','RtlCompat', 'RtlEnabled'
        // 在报告中是否返回对应的 Lint 说明
        explainIssues true
        // 设置为 true 则错误报告中不包括源代码的行号
        noLines true
        // 设置为 true 时 Lint 将不报告分析的进度
        quiet true
        // 设置为 true 则显示一个问题所在的所有地方,而不会截短列表
        showAll true
        // 设置为 true 则只报告错误
        ignoreWarnings true
        // 设置为 true,则当有错误时会显示文件的全路径或绝对路径 (默认情况下为true)
        absolutePaths true
        // 写入报告的路径,默认为构建目录下的 lint-results.html
        htmlOutput file("lint-report.html")
        // 设置为 true 则会生成一个 HTML 格式的报告
        htmlReport true
        // 写入检查报告的文件(不指定默认为 lint-results.xml)
        xmlOutput file("lint-report.xml")
        // 设置为 true 则会生成一个 XML 报告
        xmlReport false
        // 配置写入输出结果的位置,格式可以是文件或 stdout
        textOutput 'stdout'
        // 设置为 true,则生成纯文本报告(默认为 false)
        textReport false
        // 设置为 true,则会把所有警告视为错误处理
        warningsAsErrors true
        // 覆盖 Lint 规则的严重程度,例如:
        severityOverrides ["MissingTranslation": LintOptions.SEVERITY_WARNING]
        // 将指定问题(根据 id 指定)的严重级别(severity)设置为 Fatal
        fatal 'NewApi', 'InlineApi'
        // 将指定问题(根据 id 指定)的严重级别(severity)设置为 Error
        error 'Wakelock', 'TextViewEdits'
        // 将指定问题(根据 id 指定)的严重级别(severity)设置为 Warning
        warning 'ResourceAsColor'
        // 将指定问题(根据 id 指定)的严重级别(severity)设置为 ignore
        ignore 'TypographyQuotes'
    }
}

然后,在项目根目录创建一个名为default-lint.xml的文件,在这个文件中可以自己定义规则的应用场景、报警级别、是否启用等。

<?xml version="1.0" encoding="UTF-8"?>
<lint>
    <!-- list of issues to configure -->
    <!-- Ignore the UselessLeaf issue in the specified file -->
    <issue id="ApplySharedPref" severity="ignore"><!--对指定文件,忽略该issue-->
    </issue>
    <issue id="LogUsage" severity="error" />
</lint>

然后,使用前面介绍的Gradle命令行执行Lint检查即可。

./gradlew lint
./gradlew app:lintDebug
./gradlew app:lintTiktokI18nDebug

四、自定义Lint

4.1 方案设计

自定义 Lint 规则最终都会打成 JAR 包,只需将该输出 JAR 提供给其他组件使用即可。目前有两种方式可供选择:全局方案和aar方案。

全局方案

把此 jar 拷贝到 ~/.android/lint/ 目录中即可。缺点显而易见:针对所有工程生效,会影响同一台机器其他工程的 Lint 检查。即便触发工程时拷贝过去,执行完删除,但其他进程或线程使用 ./gradlew lint 仍可能会受到影响。

aar方案

另一种实现方式是将 jar 置于一个 aar 中,如果某个工程想要接入执行自定义的 lint 规则,只需依赖这个发布后的 aar 即可,如此一来,新增的 lint 规则就可将影响范围控制在单个项目内了,此方案也是Google 目前推荐的方式。

image.png

参考文档:创建 Android 库

Android的Lint有很多默认的规则,分为了不同的规则组和级别,从检测结果中我们就可以看出来。比如规则层面有Correctness(正确性),Performance(性能),Security(安全)等。当然,我们也可以自己定义新的规则,比如不允许直接使用android.util.Log相关的方法,因为可能会有安全性问题。

4.2 自定义Lint示例

4.2.1 添加依赖

首先,我们需要创建一个java的module,用来实现自定义lint规则,需要引入lint相关的依赖。

implementation "com.android.tools.lint:lint-api:30.2.1"
implementation "com.android.tools.lint:lint-checks:30.2.1"

4.2.2 添加Lint规则

然后,我们新建一个规则类,继承Detector并实现Detector.UastScanner接口,作用是实现代码检测的入口,代码如下。

public class LogDetector extends Detector implements Detector.UastScanner{
    public static final Issue ISSUE = Issue.create(
            "LogUse",
            "避免直接使用android.util.Log",
            "避免直接使用android.util.log,请使用xxx 代替",
            Category.SECURITY,
            5,
            Severity.ERROR,
            new Implementation(LogDetector.class, Scope.JAVA_FILE_SCOPE)
    );
   
    @Nullable
    @Override
    public List<String> getApplicableMethodNames() {
        return Arrays.asList("v", "d", "i", "w", "e");
    }
   
    @Override
    public void visitMethod(@NotNull JavaContext context, @NotNull UCallExpression node, @NotNull PsiMethod method) {
        if (context.getEvaluator().isMemberInClass(method, "android.util.Log")) {
            context.report(ISSUE, node, context.getLocation(node), "请勿直接调用Log,应该使用统一Log工具类");
        }
    }
}

在上面的代码中,我们创建了一个LogDetector类,该类继承自Detector,实现了Scanner的接口,这里的UastScanner是对Java/kotlin 源文件进行扫描。然后,我们重写了getApplicableMethodNames和visitMethod。其中,getApplicableMethodNames方法的作用是返回要检测的方法名;visitMethod方法会在上述方法名的方法被调用时调用,所以在此方法内,我们可以进行一些自定义的处理逻辑。最后,就是完成了一个规则的定义,检测到该问题时,通过context.report()方法抛出。Issue的参数说明如下:

  • id:唯一值,应该能简短描述当前问题。利用Java注解或者XML属性进行屏蔽时,使用的就是这个id。
  • summary:简短的总结,描述问题而不是修复措施。
  • explanation:完整的问题解释和修复建议。
  • category:问题类别。比如Security 、Performance、Usability、Accessibility和Correctness
  • priority:优先级。1-10的数字,10为最重要/最严重。
  • severity:严重级别:Fatal, Error, Warning, Informational, Ignore。
  • Implementation:为Issue和Detector提供映射关系,Detector就是当前Detector。

同时,Scanner支持的种类有以下几种:

  • ClassScanner:Class文件扫描器
  • GradleScanner:扫描Gradle脚本
  • ResourceFolderScanner:资源文件扫描器
  • XmlScanner:扫描XML文件
  • JavaScanner/JavaPsiScanner/UastSanner:扫描源文件
  • OtherFileScanner:扫描其他类型文件

4.2.3 注册Lint

现在,规则的实现和配置都已完成,要怎么注册到Lint之中呢?其实也很简单,我们只需要继承IssueRegistry实现一个注册类,在getIssues方法中,注册我们的规则配置即可,代码如下。

public class LogIssueRegistry extends IssueRegistry {
    @Override
    public List<Issue> getIssues() {
        return Arrays.asList(
                LogDetector.ISSUE
        );
    }
    @Override
    public int getMinApi() {
        return 1;
    }
    @Override
    public int getApi() {
        return ApiKt.CURRENT_API;
    }
}

4.2.4 生成jar包

接着,在build.gradle中声明 Lint-Registry,注意声明时包名要匹配,如下所示。

jar {
    manifest {
        attributes("Lint-Registry": "com.xzh.lint.LogIssueRegistry")
    }
}

4.2.5 引入jar

对于Demo工程,我们可以使用下面的方式引入:

lintChecks project(':lint-wrapper')

除此之外,我们还可以使用LinkedIn方案进行引入,将jar包放到一个aar中,这样就可以通过引入aar的方式实现。

plugins {
   id 'com.android.library'
   id 'org.jetbrains.kotlin.android'
}


dependencies {
   lintPublish project(':lint-wrapper')
   // other dependencies
}

当我们运行项目的时候,在输出的aar包中,就可以看到lint.jar。在指定模块中引入aar,自定义的Lint就可以生效了。

image.png

最后,我们在 app 模块中依赖一下 lint-aar 这个组件,就可以编写测试代码了。

参考:https://www.jianshu.com/p/0ae667d30e75


xiangzhihong
5.9k 声望15.3k 粉丝

著有《React Native移动开发实战》1,2,3、《Kotlin入门与实战》《Weex跨平台开发实战》、《Flutter跨平台开发与实战》1,2和《Android应用开发实战》