本篇介绍静态代码审查的意义以及如何在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 Studio 提供了一个名为 lint 的代码扫描工具,可帮助您发现并更正代码结构质量方面的问题,而无需您实际执行应用,也不必编写测试用例。系统会报告该工具检测到的每个问题并提供问题的描述消息和严重级别,以便您可以快速确定需要优先进行的关键改进。此外,您还可以降低问题的严重级别以忽略与项目无关的问题,或者提高严重级别以突出特定问题。
Link Check的问题按严重程度分为5种,分别为Fatal、Error、Warning、Information和Ignore,对Issue忽略操作本质就是降低该Issue的严重程度为Ignore。
lint 工具可以检查您的 Android 项目源文件是否有潜在的 bug,以及在正确性、安全性、性能、易用性、无障碍性和国际化方面是否需要优化改进。使用 Android Studio 时,无论何时构建应用,都会运行配置的 lint 和 IDE 检查。不过,您也可以手动运行检查或从命令行运行 lint。
App源代码指整个工程的源代码文件,除了Java、Kotlin、XML外,还包括应用图标素材文件以及用于代码混淆的ProGuard配置文件。lint.xml文件是Lint检查的配置文件,当我们需要自定义检查规则时,通常会编辑这个文件。
查看Lint所有规则命令:
lint-list和lint-show
如果您使用的是 Android Studio 或 Gradle,您可以在项目的根目录下输入以下某个命令:
./gradlew lint
如果只需要对某个Debug版本或者Release版本进行代码检查,则可以使用
./gradlew lintDebug或者./gradlew lintRelease
随着程序逻辑日益复杂,代码量日益增多,我们还有可能需要只审查部分代码,或者采用不同的配置文件。在这种情况下,使用集成在Android Studio中的Lint工具更加方便。
默认情况下,检查的项目和在命令行启动检查的项目大体一致。不同的是,在Android Studio中,执行代码检查的结果需要在Inspect Results视图中查看。
接下来,我们来探讨如何自定义Lint的检查范围。在Android Studio中,已经预设了很多检查范围供我们选择,为方便扩展和自定义检查需求,还开放了自定义Lint检查范围,主要通过广泛使用的Scope窗口实现。
<?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文件由封闭的
在源代码文件中添加忽略项
除了上述在lint.xml中定义检查规则外,我们还可以直接在源代码中添加指定的忽略规则,支持,java、kotlin和xml三种类型的源代码。
具体可参考下面链接:
配置 Java、Kotlin 和 XML 源文件的 lint 检查
在整个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
}
}
...
首先新建一个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
Scanner 类型 | Desc |
---|---|
UastScanner | 扫描 Java、Kotlin 源文件 |
XmlScanner | 扫描 XML 文件 |
ResourceFolderScanner | 扫描资源文件夹 |
ClassScanner | 扫描 Class 文件 |
BinaryResourceScanner | 扫描二进制资源文件 |
GradleScanner | 扫描Gradle脚本,借此可以检测组件之间的依赖关系 |
JavaScanner | 扫描Java文件 |
OtherFileScanner | 其他文件 |
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