iOS代码静态检查方案与实践

2022-04-22

背景

工程代码质量,一个永恒的话题。随着业务开发迭代速度越来越快,完全依赖人工保证工程质量也变得越来越不牢靠。所以,静态分析,这种可以帮助我们在编写代码的阶段就能及时发现代码错误,从而在根儿上保证工程质量的技术,就成为了 iOS 开发者最常用到的一种代码调试技术。Xcode 自带的静态分析工具 Analyze,通过静态语法分析能够找出在代码层面就能发现的内存泄露问题,还可以通过上下文分析出是否存在变量无用等问题。但是,Analyze 的功能还是有限,还是无法帮助我们在编写代码的阶段发现更多的问题。所以,这才诞生出了功能更全、定制化高、效率高的第三方静态检查工具。比如,OCLint、Infer、Clang 静态分析器等。 目前项目是以Objective-C语言编写,使用OCLint工具进行静态检查十分合适。本文主要讲解OCLint在工程中的应用,我会从如下几方面阐述从0到1实现iOS端代码静态检测的实践。

一,OCLint简介

一,什么是OCLint?
OCLint 是基于Clang的前端编译的,把我们的源码编译为了抽象语法树AST和llvm的字节码,并对其进行分析。它可应用于 C,C++,Objective-C 等语言,目的是提高软件质量并且减少代码中存在的潜在问题,是侦测编译器不可见的潜在缺陷的关键技术。 OCLint 旨于分析以下潜在问题:

  1. 可能出现的 bug:if/else/try/catch 等条件语句空的声明。
  2. 未使用的代码: 未使用的局部变量以及参数。
  3. 复杂的代码逻辑:高循环复杂度、NP 复杂度(懵)、 高 NCSS(懵)。
  4. 冗余代码:冗余的条件表达式以及无效的括号。
  5. 代码嗅觉:方法代码行过长或者参数过多。
  6. 不好的代码习惯:颠倒的逻辑和参数的错误分配。
  7. 自定义的规范

OCLint 具有以下先进的代码检验特性:

  1. 依靠源码的抽象语法树来提高分析的精确度以及效率,误报率低。
  2. 动态规则。
  3. 灵活可扩展的配置,确保用户可以自定义分析行为。
  4. 命令行式的调用使持续集成成为可能。

二,安装与使用:
三种安装方式:
1,HomeBrew安装 安装简单快捷,缺点是目前只支持安装0.13版本,无法安装最新0.15版本, 且不支持自定义规则。
2,安装包安装 进入 OCLint 在 Github 中的地址,选择 Release。选择最新版本的安装即可,缺点不支持自定义规则。
3,源码编译安装 和第二种安装方式一样,先下载最新安装包,不同之处在于需要执行./make脚本命令来下载自定义规则所用相关的配置和工具。

三,OCLint 工具的组成:

  • oclint oclint 是 OCLint 工具集最主要的指令,主要作用是规则加载、编译分析选项以及生成分析报告
  • oclint-xcodebuild 用于将 xcodebuild 生成的 log 文件 xcodebuild.log 转换为 JSON Compilation Database format 类型,推荐直接使用xcpretty,直接将编译结果输出为 compile_commands.json。
  • oclint-json-compilation-databases作用是在 JSON Compilation Database format类型的编译文件 compile_commands.json 中提取必要的信息。
    可以使用时序图来概括我们使用这几个指令的场景:

图片

四,主要命令:
1.xcodebuild 命令编译工程

1
2
3
4
5
6
xcodebuild -workspace       OCLintDemo.xcworkspace \
-scheme OCLintDemo \
-configuration Debug \
-sdks iphonesimulator \
COMPILER_INDEX_STORE_ENABLE=NO | xcpretty -r json-compilation-database \
-o compile_commands.json

2.oclint-json-compilation-database 指令来解析 compile_commands.json:

1
2
3
4
5
6
oclint-json-compilation-database -e Pods -- -o=report.html \
-rc LONG_LINE=200 \
-disable-rule ShortVariableName \
-disable-rule ObjCAssignIvarOutsideAccessors \
-disable-rule AssignIvarOutsideAccessors \
-max-priority-1=100000 \

二,如何自定义检测规则

前言:
整个自定义规则是使用c++编写,官方提供了对应接口及脚手架供我们使用,要实现自定义规则,必须实现OCLint提供的RuleBase类或其派生的抽象类。不同的规则有专用不同的抽象级别。

  • 通用规则
    编写通用规则,我们需要实现RuleBase接口。OCLint已经实现了很多通用规则,而且派生的抽象类都继承RuleBase,直接操作抽象类更加灵活方便。因此建议直接操作以下的抽象类来实现自定义规则。
  • 源代码读取器规则
    AbstractSourceCodeReaderRule提供了一种eachLine方法。我们可以获取每行的文本和当前行号。然后,我们可以处理文本。例如,我们可以计算文本的长度,可以理解它是否为注释,可以确定是否存在空格和制表符的混合使用,等等。
  • AST访问者规则
    AbstractASTVisitorRule遵循访客模式。在规则中,我们仅针对感兴趣的节点类型编写方法。OCLint提供了很多对应的visit方法,这些visit方法的返回布尔值用于控制遍历。AST访问者在访问当前节点时返回true时将继续其子节点或同级节点,反之亦然,当前visit方法返回false时它将停止。就是一个递归查询的操作。
  • AST匹配器规则
    AST匹配器规则都从AbstractASTMatcherRule类继承。我们需要添加合适的匹配器,找到匹配项后以当前AST节点为参数回调给我们。

准备工作:
首先我们要知道自定义规则是使用C++来写的,并最终以动态连结库来存储。要编写自定义规则还需要了解了一下clang AST的知识。
本质是OCLint调用clang 的API把一个个源文件生成一个一个AST,然后遍历树中的每个节点传入各个规则的一个过程。
我们可以使用如下命令查看某个文件的 AST 结构:

1
clang -Xclang -ast-dump -fsyntax-only ./testOCLint/XXXTest.m

解析后的AST终端输出:

图片

获取到AST语法树后,需要根据目标找到关键节点进行自定义规则编码。刚开始我们可以参考官方已有的源码或OCLint 自定义规则。

生成源码文件:
进入如下目录找到脚手架工程:

图片

我们通过他传入要生成的规则名,级别,类型,脚本就会在目录oclint-rules/rules/custom/自动帮我们生成一个模板代码,并且加入编译路径中,执行脚本:

1
./scaffoldRule xxxRule -t ASTVisitor

这里我们自定义的规则继承自ASTVisitor,他会帮我们自动生成两个文件:

1
2
3
4
#CMakeLists.txt 是对规则XXTestRule的编译描述,由make程序在编译时使用。xxxRule.cpp就是我们要写自定义规则代码的地方
├── custom
│ ├── CMakeLists.txt
│ └── xxxRule.cpp

一个简单的自定义规则代码如下:

图片

前面已经讲到OCLint的Rule都是以动态库的形式存储的,官方提供了,脚手架工程及命令工具方便我们生成动态库。也提供了测试机制。由于篇幅原因且接入较复杂且坑很多,这里就不再详细阐述。
在我们生成了动态库后,只需把动态库拷贝到OCLint的Rule文件下即可生效。自己生成的规则,同样支持-disable-rule 来禁止规则生效。

三,增量检测方案

由于OCLint本身不支持增量检查。为了实现静态检测接入CI发版流程。需要把发版和检测同步串行执行。但现有针对组件全文件检测,耗时严重影响发版进度。为了解决耗时瓶颈,提出增量检测方案。 oclint-json-compilation-database,提供了 -i 参数,可以传入要检测文件的路径,我们就从这里作为突破口。 方案如下:
比较最近两次或多次发版差异,提取修改的文件路径。只对修改的文件就行检测。可大幅度减少检测耗时,几乎做到对发版无影响。
大概git命令如下:
1.获取提交tag

1
git tag -l --sort=-version:refname "8.5*" > tag.txt 

按倒序获取8.5分支(分支可指定)下的所有tag写入文件中。通过文件生成tags数组。
2.获取diff文件路径

1
git diff --name-only ${tags[0]} ${tags[1]} > full_diff.txt

比较最近两个或多个tag差异,提取出修改的文件路径并写入文件。通过文件生成paths数组,如果paths为空,兜底全量检测。
3.运用OCLint支持分析指定文件路径下源码。把得到的文件路径传入即可。

四,OCLint自动化部署

由于最初的oclint不管是环境安装还是自定义规则编写及发布,都没有规范化。尤其是环境配置还依赖于本地环境,导致服务地址变更都需要重新配置,浪费人力。为了统一及规范化管理iOS静态代码检测,在基础平台的支持下实现了oclint环境自动化,脚本一键安装再无本地概念。OCLint自定义规则流程自动化,通过脚本拉取规则,本地编写规则,脚本编译及发布规则,最终生成二进制安装包。用时只需下载解压即可。
OCLint自定义规则发布自动化
1.克隆托管到我们仓库的oclint源码
2.checkout最新分支,在oclint/oclint-rules/rules/custom路径下 修改编写.cpp源码及CMakeLists文件,提交相应MR同步到远端仓库。
3.执行编译脚本,根据源码自动生成动态库

1
./build.sh

4.执行脚本上传最新规则包

1
./publish.sh 

会生成最新的环境安装包:oclint-release-xxxxxx.tar.xz
OCLint环境配置自动化
通过规则自动化我们已经生成了环境安装包,接下来只需下载解压即可脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
echo "下载并解压oclint环境"
curl -o oclint.tar.xz xxxxx/oclint/oclint-release-xxx.tar.xz
tar xvJf oclint.tar.xz
if [[ -d oclint-release ]]; then
echo "oclint环境解压成功"
cd oclint-release
pwd
OCLint_PATH=$(pwd)
echo "OCLint_PATH: ${OCLint_PATH}"
PATH=${OCLint_PATH}/bin:$PATH //修改环境变量
which oclint //查看环境变量是否修改成功
else
echo "oclint环境解压失败"
exit 64
fi

需要用到oclint的地方只需调用上面脚本即可完成环境配置。

检测报告如下:

图片

已经支持的自定义规则:

图片
图片

写到最后

目前安卓与iOS双端检测方案不同,Infer 是 Facebook 开源的、使用 OCaml 语言编写的静态分析工具,可以对 C、Java 和 Objective-C代码进行静态分析。支持增量分析,但可定制性不强,未来可考虑尝试双端统一使用Infer,但需要考量双端实践难易及成本。