如何通过静态分析提高iOS代码质量


 随着项目的扩大,依靠人工codereview来保证项目的质量,越来越不现实,这时就有必要借助于一种自动化的代码审查工具:程序静态分析。

程序静态分析(Program Static Analysis)是指在不运行代码的方式下,通过词法分析、语法分析、控制流、数据流分析等技术对程序代码进行扫描,验证代码是否满足规范性、安全性、可靠性、可维护性等指标的一种代码分析技术。(来自百度百科)

词法分析,语法分析等工作是由编译器进行的,所以对iOS项目为了完成静态分析,我们需要借助于编译器。对于OC语言的静态分析可以完全通过Clang,对于Swift的静态分析除了Clange还需要借助于SourceKit。

Swift语言对应的静态分析工具是SwiftLint,OC语言对应的静态分析工具有Infer和OCLitn。以下会是对各个静态分析工具的安装和使用做一个介绍。

SwiftLint

 

对于Swift项目的静态分析可以使用SwiftLint。SwiftLint 是一个用于强制检查 Swift 代码风格和规定的一个工具。它的实现是 Hook 了 Clang 和 SourceKit 从而能够使用 AST 来表示源代码文件的更多精确结果。Clange我们了解了,那SourceKit是干什么用的?

SourceKit包含在Swift项目的主仓库,它是一套工具集,支持Swift的大多数源代码操作特性:源代码解析、语法突出显示、排版、自动完成、跨语言头生成等工作。

安装

安装有两种方式,任选其一: 方式一:通过Homebrew

pod 'SwiftLint', :configurations => ['Debug']

这种方式相当于把SwiftLint作为一个三方库集成进了项目,因为它只是调试工具,所以我们应该将其指定为仅Debug环境下生效。

集成进Xcode

我们需要在项目中的Build Phases,添加一个Run Script Phase。如果是通过homebrew安装的,你的脚本应该是这样的。

"${PODS_ROOT}/SwiftLint/swiftlint"

 

运行SwiftLint

键入CMD + B编译项目,在编译完后会运行我们刚才加入的脚本,之后我们就能看到项目中大片的警告信息。有时候build信息并不能填入项目代码中,我们可以在编译的log日志里查看。

 

定制

SwiftLint规则太多了,如果我们不想执行某一规则,或者想要滤掉对Pods库的分析,我们可以对SwfitLint进行配置。

在项目根目录新建一个.swiftlint.yml文件,然后填入如下内容:

! Function Body Length Violation: Function body should span 40 lines or less excluding comments and whitespace: currently spans 43 lines (function_body_length)

disabled_rules下填入我们不想遵循的规则。

excluded设置我们想跳过检查的目录,Carthage、Pod、SubModule这些一般可以过滤掉。

其他的一些像是文件长度(file_length),类型名长度(type_name),我们可以通过设置具体的数值来调节。

另外SwiftLint也支持自定义规则,我们可以根据自己的需求,定义自己的rule

生成报告

如果我们想将此次分析生成一份报告,也是可以的(该命令是通过homebrew安装的swiftlint):

# 不带pod的项目,target名为TargetName,在Debug下,指定模拟器sdk环境进行编译
xcodebuild -target TargetName -configuration Debug -sdk iphonesimulator
# 带pod的项目,workspace名为TargetName.xcworkspace,在Release下,scheme为TargetName,指定真机环境进行编译。不指定模拟器环境会验证证书
xcodebuild -workspace WorkspaceName.xcworkspace -scheme SchemeName Release
# 清楚项目的编译产物
xcodebuild -workspace WorkspaceName.xcworkspace -scheme SchemeName Release clean

之后对xcodebuild命令的使用都需要将这些参数替换为自己项目的参数。

Infer

 

Infer是Facebook开发的针对C、OC、Java语言的静态分析工具,它同时支持对iOS和Android应用的分析。对于Facebook内部的应用像是 Messenger、Instagram 和其他一些应用均是有它进行静态分析的。它主要检测隐含的问题,主要包括以下几条:

  • 资源泄露,内存泄露
  • 变量和参数的非空检测
  • 循环引用
  • 过早的nil操作

暂不支持自定义规则。

安装及使用

$ cd projectDir
# 跳过对Pods的分析
$ infer run --skip-analysis-in-path Pods -- xcodebuild -workspace "Project.xcworkspace" -scheme "Scheme" -configuration Debug -sdk iphonesimulator

我们会得到一个infer-out的文件夹,里面是各种代码分析的文件,有txt,json等文件格式,当这样不方便查看,我们可以将其转成html格式:

$ xcodebuild -workspace "Project.xcworkspace" -scheme "Scheme" -configuration Debug -sdk iphonesimulator clean

再次运行Infer去编译。

$ brew tap oclint/formulae   
$ brew install oclint

通过Hombrew安装的版本为0.13。

1 error generated
1 error generated
...
oclint: error: cannot open report output file ..../onlintReport.html

我并不清楚原因,如果你想试试0.13能否使用的话,直接跳到安装xcpretty。如果你也遇到了这个问题,可以回来安装oclint0.15版本。

OCLint0.15

我在oclint issuse #547这里找到了这个问题和对应的解决方案。

我们需要更新oclint至0.15版本。brew上的最新版本是0.13,github上的最新版本是0.15。我下载github上的release0.15版本,但是这个包并不是编译过的,不清楚是不是官方自己搞错了,只能手动编译了。因为编译要下载llvm和clange,这两个包较大,所以我将编译过后的包直接传到了这里CodeChecker。

如果不关心编译过程,可以下载编译好的包,跳到设置环境变量那一步。

编译OCLint

1、安装CMake和Ninja这两个编译工具

$ git clone https://github.com/oclint/oclint

3、进入oclint-scripts目录,执行make命令

OCLint_PATH=/Users/zhangferry/oclint/build/oclint-release
export PATH=$OCLint_PATH/bin:$PATH

执行source .zshrc,刷新环境变量,然后验证oclint是否安装成功:

$ gem install xcpretty

OCLint的使用

在使用OCLint之前还需要一些准备工作,需要将编译项COMPILER_INDEX_STORE_ENABLE设置为NO。

  • 将 Project 和 Targets 中 Building Settings 下的 COMPILER_INDEX_STORE_ENABLE 设置为 NO
  • 在 podfile 中 target 'target' do 前面添加下面的脚本,将各个pod的编译配置也改为此选项
$ xcodebuild -workspace ProjectName.xcworkspace -scheme ProjectScheme -configuration Debug -sdk iphonesimulator | xcpretty -r json-compilation-database -o compile_commands.json

会将xcodebuild编译过程中的一些信息记录成一个文件compile_commands.json,如果我们在项目根目录看到了该文件,且里面是有内容的,证明我们完成了第一步。

2、我们将这个json文件转成方便查看的html,过滤掉对Pods文件的分析,为了防止行数上限,我们加上行数的限制:

#!/bin/bash
# mark sure you had install the oclint and xcpretty

# You need to replace these values with your own project configuration
workspace_name="WorkSpaceName.xcworkspace"
scheme_name="SchemeName"

# remove history
rm compile_commands.json
rm oclint_result.xml
# clean project
# -sdk iphonesimulator means run simulator
xcodebuild -workspace $workspace_name -scheme $scheme_name -configuration Debug -sdk iphonesimulator clean || (echo "command failed"; exit 1);

# export compile_commands.json
xcodebuild -workspace $workspace_name -scheme $scheme_name -configuration Debug -sdk iphonesimulator \
| xcpretty -r json-compilation-database -o compile_commands.json \
|| (echo "command failed"; exit 1);

# export report html
# you can run `oclint -help` to see all USAGE
oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html \
-disable-rule ShortVariableName \
-rc LONG_LINE=1000 \
|| (echo "command failed"; exit 1);

open -a "/Applications/Safari.app" oclintReport.html

oclint-json-compilation-database命令的几个参数说明:

-e 需要忽略分析的文件,这些文件的警告不会出现在报告中

-rc 需要覆盖的规则的阀值,这里可以自定义项目的阀值,默认阀值

-enable-rule 支持的规则,默认是oclint提供的都支持,可以组合-disable-rule来过滤掉一些规则 规则列表

-disable-rule 需要忽略的规则,根据项目需求设置

在Xcode中使用OCLint

因为OCLint提供了xcode格式的输出样式,所以我们可以将它作为一个脚本放在Xcode中。

1、在项目的 TARGETS 下面,点击下方的 "+" ,选择 cross-platform 下面的 Aggregate。输入名字,这里命名为 OCLint

 

2、选中该Target,进入Build Phases,添加Run Script,写入下面脚本:

# Type a script or drag a script file from your workspace to insert its path.
# 内置变量
cd ${SRCROOT}
xcodebuild clean 
xcodebuild | xcpretty -r json-compilation-database
oclint-json-compilation-database -e Pods -- -report-type xcode

可以看出该脚本跟上面的脚本一样,只不过 将oclint-json-compilation-database命令的-report-typehtml改为了xcode。而OCLint作为一个target本身就运行在特定的环境下,所以xcodebuild可以省去配置参数。

3、通过CMD + B我们编译一下项目,执行脚本任务,会得到能够定位到代码的warning信息:

 

总结

以下是对这几种静态分析方案的对比,我们可以根据需求选择适合自己的静态分析方案。

 SwiftLintInferOCLint
支持语言 Swift C、C++、OC、Java C、C++、OC
易用性 简单 较简单 较简单
能否集成进Xcode 可以 不能集成进xcode 可以
自带规则丰富度 较多,包含代码规范 相对较少,主要检测潜在问题 较多,包含代码规范
规则扩展性 可以 不可以 可以

参考

OCLint 实现 Code Review - 给你的代码提提质量

Using OCLint in Xcode

Infer 的工作机制

LLVM & Clang 入门

推荐??:

如果你想一起进阶,不妨添加一下交流群1012951431

面试题资料或者相关学习资料都在群文件中 进群即可下载!