概述
在日常的项目开发迭代中,相信每个人对与代码质量都是有着高要求的。但是,在所有事情中,人往往就是其中最大的变量因素,个人各异,如何去保障代码质量以及统一规范呢?开发团队也许会严格要求Code-Review以及MR来把控前期质量,也许会要求大量的自动化测试、单元测试、人工测试等来保障业务稳定性,也许还会集成异常捕获及时发现并修复线上问题。。。
任何的问题修复都需要有对应资源的付出,问题发现的越早,付出的成本也就越低。在 STICKYMINDS 网站上的一篇名为 《 The Shift-Left Approach to Software Testing 》 的文章中提到,假如在编码阶段发现的缺陷只需要 1 分钟就能解决,那么单元测试阶段需要 4 分钟,功能测试阶段需要 10 分钟,系统测试阶段需要 40 分钟,而到了发布之后可能就需要 640 分钟来修复 。
那有没有什么方法可以帮助我们更早地发现问题呢?有!那就是静态代码检查!
1、什么是静态代码检查
根据维基百科介绍,静态代码检查又称静态程序分析(英语:Static program analysis)是指在不运行程序的条件下,进行程序分析的方法。静态代码检查工具会以源代码为检查对象,从命名、语法、语义等多个维度进行扫描分析,发现可能存在的问题,并根据检查规则对问题进行严重等级划分,给出不同的标识和提示。
因为静态代码检查的对象为源代码,所以在编码阶段我们就能够发现并修复问题,时间节点的提前对应着修复问题所付出的成本大大降低。
2、价值
- 提前发现代码问题,降低修复成本;
- 相较于动态分析,静态代码检查速度更快;
- 自定义规则实现,可用于编码规范限制,统一风格;
常用静态代码检查工具
不同的平台,不同的语言都会有一种或者多种代码检查工具,如iOS的Clang Static Analyzer、OCLint、Infer,前端的ESLint、JShint,Python的PyCharm等等,这里不做一一举例。下面将简单对比一下在Android开发中常见的几款检查工具。
维度 | FindBugs/SpotBugs | CheckStyle | Lint |
---|---|---|---|
扫描对象 | Java字节码 | 源代码文件 | Java、XML、Kotlin、Java字节码、Gradle、图片资源、Manifest文件等 |
原理 | 基于BCEL库通过扫描字节码完成代码检查,主要做缺陷模式匹配和数据流分析 | 使用Antlr库对源码文件做词语法分析生成抽象语法树,遍历整个语法树匹配检测规则 | 基于抽象语法树分析 |
内置规则种类 | 300+检测规则 | 100+检测规则 | 300+检测规则 |
优点 | 针对字节码检查,对JDK定制化程度高,能发现Java代码中潜在的错误和缺陷 | 耗时相对较少、轻量、针对代码风格检查有有优势 | 官方支持、检测全面、扩展性强、支持自定义规则、配套工具完善 |
缺点 | 定制规则门槛高,依赖编译代码,扫描耗时 | 检查规则相对简单,无法检查潜在问题 | 检测字节码时依赖编译代码,全量检测耗时,版本迭代API较大 |
Lint使用
Android Lint 是 ADT 16(和 Tools 16)中引入的一个新工具,用于静态代码扫描发现 Android 项目源中的潜在错误,以及在正确性、安全性、性能、易用性、无障碍性和国际化方面是否需要优化改进。
Lint 既可以用作命令行工具,也可以与 Eclipse 和 IntelliJ 集成在一起。它被设计成独立于 IDE 的工具,我们可以在 Android Studio 中非常方便的使用它。
1、Lint源文件扫描工作流
应用源文件:源文件包含组成 Android 项目的文件,包括 Java、Kotlin 和 XML 文件、图标以及 ProGuard 配置文件。
lint.xml 文件:一个配置文件,可用于指定要排除的任何 lint 检查以及自定义问题严重级别。
lint 工具:一个静态代码扫描工具,您可以从命令行或在 Android Studio 中对 Android 项目运行该工具。lint 工具检查可能会影响 Android 应用的质量和性能的代码结构问题。强烈建议您先更正 lint 检测到的所有错误,然后再发布您的应用。
lint 检查结果:可以在控制台或 Android Studio 的 Inspection Results 窗口中查看 lint 检查结果。
2、Lint工具使用
-
从菜单栏中,依次选择 Analyze > Inspect Code
-
在 Specify Inspection Scope 对话框中,查看设置。在 Inspection profile 下,选择配置文件。
-
结果查看
-
lint扫描结果主要有下面几大类
Accessibility:无障碍,例如 ImageView 缺少 contentDescription 描述,String 编码字符串等问题。
Correctness:正确性,例如 xml 中使用了不正确的属性值,Java 代码中直接使用了超过最低 SDK 要求的 API 等。
Internationalization:国际化,如字符缺少翻译等问题。
Performance:性能,例如在 onMeasure、onDraw 中执行 new,内存泄露,产生了冗余的资源,xml 结构冗余等。
Security:安全性,例如没有使用 HTTPS 连接 Gradle,AndroidManifest 中的权限问题等。
Usability:易用性,例如缺少某些倍数的切图,重复图标等。
3、自定义Lint规则实现
Android studio 内置了许多Lint规则,但当内置规则无法完全匹配我们的需求时,尤其针对一些编码严谨性要求和编码规范要求,这就需要我们实现Lint规则的自定义;
下面就让我们来看看具体怎么实现规则的自定义吧。
3.1 新建module
自定义Lint需要一个纯Java项目,Module类型选择Java or Kotlin Library, 暂时命名 lint_customize。
3.2 build.gradle依赖配置
plugins {
id 'java-library'
id 'kotlin'
id 'kotlin-kapt'
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
compileOnly 'com.android.tools.lint:lint-api:30.2.1'
compileOnly 'com.android.tools.lint:lint-checks:30.2.1'
}
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
jar {
manifest {
attributes("Lint-Registry-v2": "com.jiang.lint.customize.IMockIssueRegistry")
}
}
-
模块中添加了Lint相关依赖:
com.android.tools.lint:lint-api:提供了 Lint 的 API,包括 Context、Project、Detector、Issue、IssueRegistry 等,后面会做介绍;
com.android.tools.lint:lint-checks:包含了 Lint 支持的200多种规则;
这里需要注意,只能使用 compileOnly 依赖,使用 implementation 将会编译报错
-
因为Lint依赖库内部使用了Kotlin,所以这里也需要添加相关插件,否则编译正常,但是自定义的规则却不会生效;
-
Lint-Registry-v2 注册,将自定义规则注册至Lint;
3.3 API说明
在正式开始实现前,我们先来了解几个Lint API:
Issue:问 题的描述,表示一个 Lint 规则。
Detector:中文是探测器,用于检测并报告代码中的 Issue,每个 Issue 都要指定 Detector。
Scope:翻译过来是表示范围的意思。这是用于声明 Detector 要扫描的代码范围,例如 JAVA_FILE_SCOPE、CLASS_FILE_SCOPE、RESOURCE_FILE_SCOPE、GRADLE_SCOPE 等,一个 Issue 可包含一到多个 Scope。
Scanner:翻译过来就是扫描器的意思。用于扫描并发现代码中的 Issue,每个 Detector 可以实现一到多个 Scanner。
Scanner 类型 | 说明 |
---|---|
UastScanner | 扫描 Java、Kotlin 源文件 |
XmlScanner | 扫描 XML 文件 |
ResourceFolderScanner | 扫描资源文件夹 |
ClassScanner | 扫描 Class 文件 |
BinaryResourceScanner | 扫描二进制资源文件 |
GradleScanner | 扫描Gradle脚本 |
IssueRegistry: Lint 规则加载的入口,提供要检查的 Issue 列表。 |
3.4 规则实现
下面就让我们开始定义XML布局文件内控件id命名检测规则 ViewIdDetector 吧:
- 定义 ViewIdDetector 类继承 Detector ,因为我们需要检查的内容在XML文件内,所以还需要实现 Detector.XmlScanner 接口;
- 指定文件选择
在通过 XmlScanner 和 Scope 限制检查范围后,我们还需要使用 appliesTo 对文件进行进一步地选择,这里我限制只有 ResourceFolderType.LAYOUT 类型的布局文件才会进入最终的检测;
/**
* 自定义检测范围,layout文件
* @param folderType
* @return
*/
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.LAYOUT;
}
- 获取元素,进行元素内容分析
- 重写 getApplicableElements 方法,返回我们所需的空间类型集合
- 重写 visitElement 方法,获取元素 id 属性,与我们所需的命名规则进行匹配;若不满足规则,则通过 report 方法进行上报;
不同的 Scanner 扫描器所对应的 report 方法各有不同,需要根据具体场景实现;
在这里 report有三个参数,第一个参数是Issue,就是第二步中我们定义的规则; 第二个参数是当前节点; 第三个参数location会返回当前的位置信息,便于在报告中显示定位;最后的字符串用来为警告添加解释。
完整类实现如下:
public class ViewIdDetector extends Detector implements Detector.XmlScanner {
/**
* "ViewIdCheck" 是 Lint 规则的 id,必须是唯一的。
* "ViewId命名不规范" 是简述。
* "ViewIdName建议使用 view的缩写加上_xxx,例如tv_xxx, iv_xxx" 是详细解释。
* 5 是优先级系数。必须是1到10之间的某个值。
* ERROR 是严重程度
* Implementation 是Detector间的桥梁,用于发现问题。Scope则用于确认分析范围。在本例中,我们必须处于资源文件层面才能分析前缀问题。
*/
public static Issue ISSUE = Issue.create("ViewIdCheck",
"ViewId命名不规范",
"ViewIdName建议使用 view的缩写加上_xxx,例如tv_xxx, iv_xxx",
Category.CORRECTNESS,
5,
Severity.ERROR,
new Implementation(ViewIdDetector.class, Scope.RESOURCE_FILE_SCOPE));
/**
* 自定义检测范围,layout文件
* @param folderType
* @return
*/
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.LAYOUT;
}
@Nullable
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(SdkConstants.TEXT_VIEW, SdkConstants.IMAGE_VIEW, SdkConstants.BUTTON);
}
@Override
public void visitElement(@NotNull XmlContext context, @NotNull Element element) {
//判断是否设置了 id
if (!element.hasAttributeNS(SdkConstants.ANDROID_URI, SdkConstants.ATTR_ID)) {
return;
}
//获取 id 命名,并进行前缀校验
Attr attr = element.getAttributeNodeNS(SdkConstants.ANDROID_URI, SdkConstants.ATTR_ID);
String value = attr.getValue();
if (value.startsWith(SdkConstants.NEW_ID_PREFIX)) {
String idValue = value.substring(SdkConstants.NEW_ID_PREFIX.length());
boolean matchRule = true;
String expMsg;
switch (element.getTagName()) {
case SdkConstants.TEXT_VIEW:
expMsg = "tv_";
matchRule = idValue.startsWith(expMsg);
break;
case SdkConstants.IMAGE_VIEW:
expMsg = "iv_";
matchRule = idValue.startsWith(expMsg);
break;
case SdkConstants.BUTTON:
expMsg = "btn_";
matchRule = idValue.startsWith(expMsg);
break;
}
if (!matchRule) {
context.report(ISSUE, attr, context.getLocation(attr), "ViewIdName建议使用view的缩写_xxx; ${element.tagName} 建议使用 `${expMsg}_xxx`");
}
}
}
}
- 创建一个 Issue 对象(即一条规则),用于和 ViewIdDetector 进行绑定;
public static Issue ISSUE = Issue.create(
"ViewIdCheck",
"ViewId命名不规范",
"ViewIdName建议使用 view的缩写加上_xxx,例如tv_xxx, iv_xxx",
CustomizeCategory.NAMING_CONVENTION,
5,
Severity.ERROR,
new Implementation(ViewIdDetector.class, Scope.RESOURCE_FILE_SCOPE));
Issue.create方法说明如下
fun create(
id: String,
briefDescription: String,
explanation: String,
category: Category,
priority: Int,
severity: Severity,
implementation: Implementation
)
id:唯一的id,简要表面当前提示的问题;
briefDescription: 简单描述当前问题参数;
explanation:详细解释当前问题和修复建议;
category:问题类别,可自定义;
priority:从1到10,10最重要;
Severity:严重程度,FATAL(奔溃), ERROR(错误), WARNING(警告),INFORMATIONAL(信息性),IGNORE(可忽略);
Implementation:Issue和哪个Detector绑定,以及声明 Scope 检查的范围
Category 也可以自定义类型,如下:
public class CustomizeCategory {
public static final Category NAMING_CONVENTION = Category.create("CustomizeCategory命名规范", 100);
}
- 规则注册
定义 CustomizeIssueRegistry 继承 IssueRegistry 提供 Issue 集合
class CustomizeIssueRegistry : IssueRegistry() {
override val issues: List<Issue>
get() = mutableListOf(ViewIdDetector.ISSUE)
}
gradle 中注册 CustomizeIssueRegistry
jar {
manifest {
attributes("Lint-Registry-v2": "com.jiang.lint.customize.CustomizeIssueRegistry")
}
}
至此,规则定义部分已经完成。
- 使用
- 通过 lintChecks 直接将 Lint module 引入需要检测module后,使用 Lint 命令即可进行检测。
lintChecks project(':lint_customize')
我们可以在代码中直接看到自定义规则的提示:
也可以在检测报告中找到规则的检测结果:
- 为了更方便快捷的集成自定义的规则,我们还可以将其打包为 Jar 然后放入一个 aar 中,以便快速依赖;
4、自定义规则适用场景
我们选择了自定义规则,必是要有用武之地,让我们来看看哪些场景下可以发挥自己的创造力:
1. 编码规则
如上面的例子中,我们就要求开发人员在XML中进行 id 命名时必须带上控件缩写前缀,同理我们还可以检测文件命名、文件大小等;
2. 统一工具库使用
在项目开发中,我们往往会用到很多基础的工具功能,其中大部分我们会沉淀为一个底层工具库,在工具库中会针对各种异常场景进行完善的处理,但并不是所有开发者都知道这个工具库的存在,这里我们就看定义规则进行检测。
如:Log日志、Toast、SharedPrefrence、Glide、Picasso 等,我们可以定义规则检查 Java/Kotlin 文件,限制必须使用统一工具库提供接口;
3. TODO Check
检查代码中是否还有TODO尚未完成。例如开发时可能会在代码中写一些测试数据,最终上线前要确保删除;
4. 个人隐私限制
现在对于用户个人隐私安全越来越重视,很多系统API因涉及隐私政策问题,需要严格控制其使用。在此我们就可以指定规则检测特定API调用,来限制隐私数据的获取;
以上为几个条为本人认为可以继续实践的场景,大家可以发散思维寻找更多有价值的实现!
结语
本文对于Lint的自定义选择了一个小场景进行了实现,但是在项目开发中我们可能会对于Java、Kotlin、字节码等进行规则的定义,因覆盖检测范围的不同需要实现不同的 Lint API。大家在实际尝试中可以多多借鉴 lint-checks 库内已提供的 Detector,帮助理解更多API的正确使用。