静态代码审查

本篇介绍静态代码审查的意义以及如何在Android studio中集成它们。需要注意的是,这些工具不是万能的,虽然它们能高效且全面地执行代码检查工作,但它们并不具备人类的“逻辑思维”优势。也就是说,静态代码审查工具是无法确保程序逻辑表达上的正确性的。除此之外,代码中的不安全(如某些条件下的死循环、空指针异常等)、代码的执行效率甚至编码风格、变量命名等都可以被静态代码审查工具检测出来。

  • 静态代码审查的意义
    静态代码审查可以说是整个软件开发过程中不可缺少的环节,但目前仍有很多公司忽视它。实际上,这种代码审查比动态测试(指通过运行被测程序、检查其运行结果是否符合预期,并符合运行效率和健壮性等要求的测试)更有效率。根据项目自身情况的不同,静态代码审查可以找到30%~70%的代码缺陷。
    静态代码审查通常在编译和进行动态测试之前进行,这样做能在产品正式发布之前发现缺陷,大大降低维护成本,被检查代码覆盖率高。同时,这种审查通过会花费较长时间,并需要由对项目代码有足够了解的工程师处理。

  • 安装静态代码审查工具
    审查工具有很多,例如Google官方的android lint、checkstyle、spotbugs以及pmd。本篇文章,我们主要介绍下android lint工具的使用。

  • Java静态代码分析工具对比

代码缺陷分类 示例 CheckStyle FindBugs PMD Jtest
引用操作 空指针引用
对像操作 对像比较(使用==,而非equals)
表达式复杂化 多余的if语句
数组使用 数组下标越界
未使用变量或代码段 未使用变量
资源回收 I/O未关闭
方法调用 未使用方法返回值
代码设计 空的try/catch/finally块

使用Android Lint进行代码审查

Android Studio 提供了一个名为 lint 的代码扫描工具,可帮助您发现并更正代码结构质量方面的问题,而无需您实际执行应用,也不必编写测试用例。系统会报告该工具检测到的每个问题并提供问题的描述消息和严重级别,以便您可以快速确定需要优先进行的关键改进。此外,您还可以降低问题的严重级别以忽略与项目无关的问题,或者提高严重级别以突出特定问题。
Link Check的问题按严重程度分为5种,分别为Fatal、Error、Warning、Information和Ignore,对Issue忽略操作本质就是降低该Issue的严重程度为Ignore。

lint 工具可以检查您的 Android 项目源文件是否有潜在的 bug,以及在正确性、安全性、性能、易用性、无障碍性和国际化方面是否需要优化改进。使用 Android Studio 时,无论何时构建应用,都会运行配置的 lint 和 IDE 检查。不过,您也可以手动运行检查或从命令行运行 lint。
静态代码审查_第1张图片
App源代码指整个工程的源代码文件,除了Java、Kotlin、XML外,还包括应用图标素材文件以及用于代码混淆的ProGuard配置文件。lint.xml文件是Lint检查的配置文件,当我们需要自定义检查规则时,通常会编辑这个文件。

运行Lint检查

查看Lint所有规则命令:

lint-list和lint-show

如果您使用的是 Android Studio 或 Gradle,您可以在项目的根目录下输入以下某个命令:

./gradlew lint

如果只需要对某个Debug版本或者Release版本进行代码检查,则可以使用

./gradlew lintDebug或者./gradlew lintRelease

随着程序逻辑日益复杂,代码量日益增多,我们还有可能需要只审查部分代码,或者采用不同的配置文件。在这种情况下,使用集成在Android Studio中的Lint工具更加方便。

静态代码审查_第2张图片
默认情况下,检查的项目和在命令行启动检查的项目大体一致。不同的是,在Android Studio中,执行代码检查的结果需要在Inspect Results视图中查看。
静态代码审查_第3张图片

自定义Lint检查范围

接下来,我们来探讨如何自定义Lint的检查范围。在Android Studio中,已经预设了很多检查范围供我们选择,为方便扩展和自定义检查需求,还开放了自定义Lint检查范围,主要通过广泛使用的Scope窗口实现。

  1. 使用预置的检查范围
    静态代码审查_第4张图片
    从图中可以看到,由多个菜单项可以选择,我们先了解下它们都是些什么含义。
  • Project Files: 当前工程的所有文件。
  • Proiect Source Files: 当前工程的所有源代码文件。
  • Project Production Files: 当前工程中的所有生产文件。
  • Project Test Files: 当前工程中的所有测试文件。
  • Scratches and Consoles: 提供了两种临时的文件编辑环境,通常用来存放文本或代码片,该选项在实际开发中很少用到。
  • Module ‘app’:仅app模块的文件。
    Class Hierarchy:当我们选取这个菜单项并单击OK按钮时,会弹出新的窗口,窗口里显示当前工程中的所有类。我们可以使用窗口里的搜索功能过滤要检查的类。在未过滤的情形下,Lint会检查所有类。
  1. 创建自定义检查范围
    当预设的检查范围无法满足我们的自定义需求时,可以进一步对代码检查范围进行自定义。
    单击上图中下拉菜单右侧的三个点按钮,弹出Scope窗口,我们在这里配置自定义检查范围。
    静态代码审查_第5张图片
    默认情况下,Scope配置为空。单击界面左上角的“+”,然后选在Local来添加新范围。
    Local和Share的共同点是给定代码检查范围,区别是Share还可用于具有范围字段的其他项目。也就是说,Local类型的Scope配置是个人使用的,保存在个人的workspace中,默认保存在/config/projects//.idea/workspace.xml中;Share类型的Scope配置是整个工程的,可以通过版本控制系统被团队成员共享,它的默认路径在/config/projects//.idea/scopes/中。
    此处我们以Local类型的Scope为例进行介绍,名称为View,即仅检查和UI视图相关的类。
    静态代码审查_第6张图片
    如果读者对正则表达式比较熟悉,那么完全可以直接填写合法的正则表达式,达到定义范围的目的,或者使用窗口右侧的4个按钮来控制检查范围,分别说明如下:
  • Include:包含此文件夹及其文件,不包含于文件夹中的内容。
  • Include Recursively:包含此文件夹及其文件,递归包含所有自文件夹及其文件。
  • Exclude:排除此文件夹及其文件,不递归排除所有子文件夹及其文件。
  • Exclude Recursively:排除此文件夹及其文件,递归排除所有子文件夹及其文件。

自定义Lint检查类型

  1. 使用和自定义Lint配置文件
    在Android Lint中内置了多种静态代码检查的配置文件。我们可以直接使用它们,也可以更改它们的名称、说明、范围甚至是严重级别,也可以随时启用活禁用某个配置文件,达到跳过某种检查的目的。
    静态代码审查_第7张图片
    静态代码审查_第8张图片
    检查的项目、说明以及启用状态等信息一目了然地罗列在这个窗口中。我们除了可以通过复选框启用/禁用某个检查外,还需关注一个地方,就是位于左上角的Profile下拉菜单。
    展开Profile下拉菜单,默认情况下,预置了Default和project Default两个配置。显而易见,前者是对整个Android Studio而言的,将影响所有的过程;后者是对单个工程而言的。在未做任何自定义配置前,二者是一样的。
    同时,我们还可以单击下拉菜单右侧的小齿轮,添加更多自定义的菜单项。由Default复制而来的配置依旧影响所有工程,由Project Default复制而来的配置则仅对当前工程有效。当然,我们还可以对其进行重命名、删除、导入以及导出操作。
  2. 配置lint.xml文件
    通过前面的学习,我们已经可以实现特定范围,特定检查种类的自定义了。看上去似乎已经满足了静态代码检查的需要,事实上也确实如此。那为什么这里还要介绍limt xmnl 文件呢?
    想象这样一种情况,假设我们的工程有当个 XML 布局文件,要求某个或某几个布局文件需要单独定义检查类型。根据现有的知识,我们需要定义不止一个 scope,然后定义不止一个profile,最后挨个启动检查。是不是很烦项?有没有办法简化呢?答案是肯定的——借助lint.xml 定义规则,即可完成快捷方便的检查。
    lint xml位于整个工程根目录下,默认不会自动创建,需要我们手动添加这个文件,格式遵循标准的XML,一个规则定义的范例如下:
<?xml version="1.0" encoding="UTF-8"?>
<lint>
    <!-- Disable the given check in this project -->
    <issue id="IconMissingDensityFolder" severity="ignore" />

    <!-- Ignore the ObsoleteLayoutParam issue in the specified files -->
    <issue id="ObsoleteLayoutParam">
        <ignore path="res/layout/activation.xml" />
        <ignore path="res/layout-xlarge/activation.xml" />
    </issue>

    <!-- Ignore the UselessLeaf issue in the specified file -->
    <issue id="UselessLeaf">
        <ignore path="res/layout/main.xml" />
    </issue>

    <!-- Change the severity of hardcoded strings to "error" -->
    <issue id="HardcodedText" severity="error" />
</lint>

可见,lint.xml文件由封闭的标记包裹,其中包含多个子原生定义了唯一id。

  1. 在源代码文件中添加忽略项
    除了上述在lint.xml中定义检查规则外,我们还可以直接在源代码中添加指定的忽略规则,支持,java、kotlin和xml三种类型的源代码。
    具体可参考下面链接:
    配置 Java、Kotlin 和 XML 源文件的 lint 检查

  2. 在整个module中添加忽略项
    在某些情况下,整个工程可能包含多于一个module,统一的检查规则可能不适用于所有module。因此,我们需要一种方法对单个module进行规则定义,秘诀就在于每个module的build.gradle文件。
    要定义某个module的检查规则是很容易的,只需在android节点下添加lintOptions代码块即可。

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
    }
}
...

自定义Lint规则

首先新建一个java依赖库,然后在build.gradle文件种添加如下脚本:

apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    compileOnly 'com.android.tools.lint:lint-api:30.0.0'//必须是compileOnly
    compileOnly 'com.android.tools.lint:lint-checks:30.0.0'//必须是compileOnly
}
jar {
    manifest {
        attributes("Lint-registry-v2": "com.brett.lintcheck.SSIssueRegistry")
    }
}

sourceCompatibility = "1.8"
targetCompatibility = "1.8"

继承IssueRegistry类:

package com.brett.lintcheck;

import com.android.tools.lint.client.api.IssueRegistry;
import com.android.tools.lint.detector.api.Issue;

import org.jetbrains.annotations.NotNull;

import java.util.Arrays;
import java.util.List;

public class SSIssueRegistry extends IssueRegistry {
    @NotNull
    @Override
    public List<Issue> getIssues() {
        return Arrays.asList(SSLogDetector.sISSUE);
    }
}

继承Detector类并实现Scanner接口:

package com.brett.lintcheck;

import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.ClassContext;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;

import java.util.Arrays;
import java.util.List;

public class SSLogDetector extends Detector implements Detector.ClassScanner {
    public static final Issue sISSUE = Issue.create(
            "LogUse",
            "You use android.util.Log not LogUtils",
            "Logging should be avoided in production for security and performance reasons." +
                    "Therefore, we created a LogUtils that wraps all our calls to Logger and disable them for release flavor.", Category.MESSAGES,//category
            6,//must be[1,10]
            Severity.ERROR,//severity of the issue
            new Implementation(SSLogDetector.class, Scope.CLASS_FILE_SCOPE)
    );

    @Nullable
    @Override
    public List<String> getApplicableCallNames() {
        return Arrays.asList("v","d","i","w","e","wtf");
    }

    @Nullable
    @Override
    public List<String> getApplicableMethodNames() {
        return Arrays.asList("v","d","i","w","e","wtf");
    }

    @Override
    public void checkCall(@NotNull ClassContext context, @NotNull ClassNode classNode, @NotNull MethodNode method, @NotNull MethodInsnNode call) {
        String owner = call.owner;
        if(owner.startsWith("android/util/Log")){
            context.report(sISSUE,method,call,context.getLocation(call),
                    "You must be our 'LogUtils' instend of android.util.Log");
        }
    }
}

完成上面三步便可以自定义Lint。
接着,我们在任何模块种引入该依赖库,然后调用Log相关的方法,接着在终端执行如下命令:

gradlew lint

接着终端就会出现如下log:
静态代码审查_第9张图片

  1. 自定义Detector可以实现一个或多个Scanner接口,选择实现哪种接口取决于你想要的扫描范围。
    Lint API 中内置了很多 Scanner:
Scanner 类型 Desc
UastScanner 扫描 Java、Kotlin 源文件
XmlScanner 扫描 XML 文件
ResourceFolderScanner 扫描资源文件夹
ClassScanner 扫描 Class 文件
BinaryResourceScanner 扫描二进制资源文件
GradleScanner 扫描Gradle脚本,借此可以检测组件之间的依赖关系
JavaScanner 扫描Java文件
OtherFileScanner 其他文件
  1. Lint扫描顺序(Detector调用顺序):Manifest file->Resource files(按照资源目录字母顺序)->Java sources->Java classes->Gradle files->Generic files(其他所有文件)->Proguard files->Property files。
  2. Issue。用于Detector发现并输出Bug。各个参数含义如下:
 public static final Issue ISSUE = Issue.create(
            "LogId", //第一无二的id即可 
            "不要直接使用Log", //描述信息
            "不要直接使用Log",     // 描述信息
            Category.MESSAGES,
            5,//优先级[1-10]
            Severity.WARNING,
            new Implementation(LogDetector.class, Scope.JAVA_FILE_SCOPE)//文件类型意味着只扫描java文件
    );

官方文档

https://developer.android.google.cn/studio/write/lint?hl=zh_cn

参考资料

https://www.jianshu.com/p/ae906ed4b7db

你可能感兴趣的:(性能优化,android,android,studio,ide)