本文是<<iOS开发高手课>> 第七篇学习笔记.
随着业务开发迭代速度越来越快,完全依赖人工保证工程质量也变得越来越不牢靠。所以,静态分析,这种可以帮助我们在编写代码的阶段就能及时发现代码错误,从而在根儿上保证工程质量的技术,成为了 iOS 开发者最常用到的一种代码调试技术。
Analyze
Xcode 自带的静态分析工具 Analyze,通过静态语法分析能够找出在代码层面就能发现的内存泄露问题,还可以通过上下文分析出是否存在变量无用等问题。
但是,Analyze 的功能还是有限,还是无法帮助我们在编写代码的阶段发现更多的问题。所以,这才诞生出了功能更全、定制化高、效率高的第三方静态检查工具。比如,OCLint、Infer、Clang 静态分析器等。
什么是优秀的静态分析器
一款优秀的静态分析器,能够帮助我们更加全面的发现人工测试中的盲点,提高检查问题的效率,寻找潜在的可用性问题
比如空指针访问、资源和内存泄露等等。同时,静态分析器还可以检查代码规范和代码可维护性的问题,根据一些指标就能够找出哪些代码需要优化和重构。
这里有三个常用的复杂度指标,可以帮助我们度量是否需要优化和重构代码。
- 圈复杂度高。圈复杂度,指的是遍历一个模块时的复杂度,这个复杂度是由分支语句比如 if、case、while、for,还有运算符比如 &&、||,以及决策点,共同确定的。一般来说,圈复杂度在以 4 以内是低复杂度,5 到 7 是中复杂度,8 到 10 是高复杂度,11 以上时复杂度就非常高了,这时需要考虑重构,不然就会因为测试用例的数量过高而难以维护。
而这个圈复杂度的值,是很难通过人工分析出来的。而静态分析器就可以根据圈复杂度规则,来监控圈复杂度,及时发现代码是否过于复杂,发现问题后及早解决,以免造成代码过于复杂难以维护。 - NPath 复杂度高。NPath 度量是指一个方法所有可能执行的路径数量。一般高于 200 就需要考虑降低复杂度了。
- NCSS 度量高。NCSS 度量是指不包含注释的源码行数,方法和类过大会导致代码维护时阅读困难,大的 NCSS 值表示方法或类做的事情太多,应该拆分或重构。一般方法行数不过百,类的行数不过千。
但是,使用静态分析技术来保证工程质量,也并不尽如人意,还有如下两大缺陷:
- 需要耗费更长的时间。相比于编译过程,使用静态分析技术发现深层次程序错误时,会对当前分析的方法、参数、变量去和整个工程关联代码一起做分析。所以,随着工程代码量的增加,每一步分析所依赖的影响面都会增大,所需耗时就更长。
虽然在设计静态分析器时,就已经对其速度做了很多优化,但还是达不到程序编译的速度。因为静态分析本身就包含了编译最耗时的 IO 和语法分析阶段,而且静态分析的内容多于编译,所以再怎么优化,即使是最好的情况也会比编译过程来得要慢。 - 静态分析器只能检查出那些专门设计好的、可查找的错误。对于特定类型的错误分析,还需要开发者靠自己的能力写一些插件并添加进去。
OCLint
OCLint 是基于 Clang Tooling 开发的静态分析工具,主要用来发现编译器检查不到的那些潜在的关键技术问题。主要包括语法上的基础规则、Cocoa 库相关规则、一些约定俗成的规则、各种空语句检查、是否按新语法改写的检查、命名上长变量名短变量名检查、无用的语句变量和参数的检查。
除此之外,还包括了和代码量大小是否合理相关的一些规则,比如过大的类、类里方法是否太多、参数是否过多、Block 嵌套是否太深、方法里代码是否过多、圈复杂度的检查等。
这些规则可以在运行时被动态地加载到系统中,规则配置灵活、可扩展性好、方便自定义。
你可以在官方规则索引中,查看完整的规则说明。这些规则可以在运行时被动态地加载到系统中,规则配置灵活、可扩展性好、方便自定义。
OCLint是一个通过检查C,C++或Objective-C代码来提高代码质量、降低错误率的静态代码分析工具,代码通过OCLint检测后,可以发现一些潜在的问题,如:
* 可能的bug:if/else/try/catch/finally 空语句空变量
* 代码无用:并未使用的本地变量和参数
* 代码过于复杂:高复杂度的循环、判断
* 代码冗余:冗余的if判断和多余的括号
* 代码异味:长的方法和长参数列表
* 不好的尝试:反向逻辑、参数重复赋值
静态代码分析是一个很重要的技术发现编译器中那些不可视的缺点,OCLint自动完成这些检测需要依赖以下特点:
* 依赖源代码的抽象语法树来保证精准度和效率,尽可能减少误报,避免有用的结果被跳过;
* 动态加载规则到系统中(甚至是运行期间加载规则);
* 灵活可扩展的配置保证用户可以定制化静态代码检查工具;
* 为了技术问题尽早的被修复,降低维护成本,使用命令行运行命令,在代码开发过程中,对代码进行持续集成和检测;
OCLint的安装
有三种方式安装,分别为 Homebrew、下载安装包安装、源代码编译安装。 建议先使用Homebrew方式安装,更简单方便些。它们的区别为:
* 如果需要自定义 Lint 规则,则需要下载源码编译安装
* 如果仅仅是使用自带的规则来 Lint,那么以上3种安装方式都可以
Homebrew 的方式。
Homebrew 是 macOS 下专门用来进行软件包管理的一个工具,使用起来很方便,让你无需关心一些依赖和路径配置。使用 Homebrew 的方式安装时,我们首先安装第三方依赖库-oclint/formulae,然后安装 OCLint。安装方法是在终端输入:
brew tap oclint/formulae
brew install oclint
下载安装包安装
- 进入 OCLint 在 Github 中的地址,选择 Release。选择最新版本的安装包。
- 解压下载文件。将文件存放到一个合适的位置。(比如我选择将这些需要的源代码存放到 Document 目录下)
- 配置环境变量,将 bin 目录添加到 PATH 下,编辑
.bashrc
或.bash_profile
OCLint_PATH=/Users/zjh48/Documents/oclint/build/oclint-release
export PATH=$OCLint_PATH/bin:$PATH
将配置文件 source 一下。
source .bash_profile
安装xcpretty
需要使用OCLint对日志信息进行分析运行命令,安装xcpretty,使用xcpretty命令分析日志信息。xcpretty是用来格式化xcodebuild输出的工具,使用ruby开发。安装:
gem install xcpretty
OCLint的使用
进入指定项目
cd /Users/geneqiao/Desktop/iOS_Collection; clear;
查看项目基本信息
xcodebuild -list
打印输出
Command line invocation:
/Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild -list
Information about project "iOS_Collection":
Targets:
iOS_Collection
Build Configurations:
Debug
Release
If no build configuration is specified and -scheme is not passed then "Release" is used.
Schemes:
iOS_Collection
编译项目
先command+shift+k
clean项目,然后再Debug 编译项目了;最后通过xcpretty
,使用-r json-compilation-database
可以生成指定格式的数据。编译成功后,会在项目的文件夹下出现 compile_commands.json
文件,compile_commands.json文件则是记录了自定义规则代码不匹配的信息,里面是一个个字典,字典有三个键值对,command,file,directionary
xcodebuild -scheme iOS_Collection -workspace iOS_Collection.xcworkspace clean && xcodebuild -scheme iOS_Collection -workspace iOS_Collection.xcworkspace -configuration Debug | xcpretty -r json-compilation-database -o compile_commands.json
注意项
- 如果项目使用了 Cocopod,则需要指定 -workspace xxx.workspace
- 每次编译之前需要 clean
生成 html 报表
- 使用 oclint-json-compilation-database 命令对上一步生成的json数据进行分析,对项目代码进行分析,最终生成report.html文件。OCLint目前支持输出html,json,xml,pmd,Xcode格式文件
oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html
- 看到有报错,但是报错信息太多了,不好定位,利用下面的脚本则可以将报错信息写入 log 文件,方便查看
oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html 2>&1 | tee 1.log
- 如果项目工程太大,整个 lint 会比较耗时,oclint 支持针对某个代码文件夹进行 lint
oclint-json-compilation-database -i 需要静态分析的文件夹或文件 -- -report-type html -o oclintReport.html 其他的参数
如有错误可根据下一小节内容进行修改,或查找其他资料解决。执行成功后,查看 html 文件可以具体定位哪个代码文件,哪一行哪一列有什么问题,方便修改
可能遇到的问题
oclint: error: one compiler command contains multiple jobs
https://github.com/oclint/oclint/issues/462
-将 Project 和 Targets 中 Building Settings 下的 COMPILER_INDEX_STORE_ENABLE 设置为 NO
-在 podfile 中 target 'xx' do 前面添加下面的脚本
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['COMPILER_INDEX_STORE_ENABLE'] = "NO"
end
end
end
oclint: error: violations exceed threshold
警告数量超过默认的限制,则 lint 失败。事实上 lint 后可以跟参数,所以我们修改脚本如下
oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html -rc LONG_LINE=9999 -max-priority-1=9999 -max-priority-2=9999 -max-priority-3=9999
OCLint的规则
可以通过 -e 参数忽略指定的文件,比如忽略Pods文件夹:
oclint-json-compilation-database -e Pods -- -o=report.html
通过-rc改变检查规则的默认值
比如有一条默认规则:long line [size|P3] Line with 137 characters exceeds limit of 100
,这表示一个方法里的代码行数不能超过100,可以通过-rc改变默认100行的限制比如改成200行
oclint-json-compilation-database -- -rc=LONG_LINE=200 -o=report.html
具体可以操作哪些规则,可以去官网查询
通过 -disable-rule可以禁止某一规则,比如禁止LongLine长方法检查:
oclint-json-compilation-database -disable-rule=LongLine
且这些命令是可以组合使用
oclint-json-compilation-database -e Pods -rc=LONG_LINE=200-- -o=report.html
如果需要更改的规则比较多,可以通过.oclint 文件配置规则
Xcode脚本使用
创建Aggregate项目
OClint 可以和 Xcode IDE 结合,把错误直接在 IDE 中显示出来。
- 项目中创建一个新的 target,然后选择 Aggregate 作为模板。这里命名为 FPLint。
注意我们可以建立多个 target,然后分别分析代码的不同方面。
添加 Run Script 脚本
选择创建的 TARGET 。然后在 Build Phases 选项卡中选择 Add Run Script。
关于脚本的编写我们仍然选择最简单的方式,即 xcodebuild + xcpretty + oclint-json-compilation-database 的方式来做 oclint。脚本如下:
cd ${SRCROOT}
xcodebuild clean
xcodebuild | tee xcodebuild.log | xcpretty -r json-compilation-database -o compile_commands.json
oclint-json-compilation-database -e Pods oclints_args -- -report-type xcode
运行Aggregate项目
然后开始执行分析,因为 report-type 是 xcode,oclint 发现的错误会直接在 IDE 中标示出来,方便我们对代码进行改进。当然这只是一种参考,不一定要采纳 oclint 给的提示。
Clang 静态分析器
Clang 静态分析器(Clang Static Analyzer)是一个用 C++ 开发的,用来分析 C、C++ 和 Objective-C 的开源工具,是 Clang 项目的一部分,构建在 Clang 和 LLVM 之上。Clang 静态分析器的分析引擎用的就是 Clang 的库。
Clang 静态分析器专门为速度做过优化,可以在保证查出错误的前提下,使用更聪明的算法减少检查的工作量。
Clang 静态分析器
在 Clang 静态分析器中,常用的就是 scan-build 和 scan-view 这两个工具。
.
├── bin
│ ├── scan-build
│ ├── scan-view
scan-build 是用来运行分析器的命令行工具;scan-view 包含了 scan-build 工具,会在 scan-build 执行完后将结果可视化。
scan-build 的原理是,将编译器构建改成另一个“假的”编译器来构建,这个“假的”编译器会执行 Clang 来编译,然后执行静态分析器分析你的代码。
scan-build 的使用
make
我们需要在项目下生成makefile
文件,需要先安装 automake
brew install automake
生成makefile文件:https://blog.csdn.net/qq_19004627/article/details/79061457
之后使用如下命令
\yourpath\scan-build -k -V make
xcodebuild
\yourpath\scan-build xcodebuild
Infer
Infer 是 Facebook 开源的、使用 OCaml 语言编写的静态分析工具,可以对 C、Java 和 Objective-C 代码进行静态分析,可以检查出空指针访问、资源泄露以及内存泄露
Infer 的安装,
有源码安装和直接安装 binary releases 两种方式。
源码安装
如果想在 macOS 上编译源码进行安装的话,你需要预先安装一些工具,这些工具在后面编译时会用到,指令如下:
brew install autoconf automake cmake opam pkg-config sqlite gmp mpfr
clone源码
# Checkout Infer
git clone https://github.com/facebook/infer.git
cd infer
# Compile Infer
./build-infer.sh clang
# install Infer system-wide...
sudo make install
# ...or, alternatively, install Infer into your PATH
export PATH=`pwd`/infer/bin:$PATH
binary releases
使用源码安装所需的时间会比较长,因为会编译一个特定的 Clang 版本,而 Clang 是个庞大的工程,特别是第一次编译的耗时会比较长。我在第一次编译时,就大概花了一个多小时。所以,直接安装 binary releases 会更快些,在终端输入:
brew install infer
infer的使用
分析单个文件
infer -- clang -c main.m
分析项目
infer run -- xcodebuild -project XXX.xcodeproj -target XXX -sdk iphoneos14.3
-sdk
版本可以通过 xcodebuild -showsdks
查看
生成了infer-out文件,保存了静态分析的结果,
有各种格式的结果文件,JSON
更便于其他系统集成,txt
便于我们自己人工查看
infer也支持直接解析compile_commands.json文件,其原理与OCLint一致,都是基于Clang。
infer --compilation-database compile_commands.json
Infer 的工作原理
- 第一个阶段是转化阶段,将源代码转成 Infer 内部的中间语言。类 C 语言使用 Clang 进行编译,Java 语言使用 javac 进行编译,编译的同时转成中间语言,输出到 infer-out 目录。
- 第二个阶段是分析阶段,分析 infer-out 目录下的文件。分析每个方法,如果出现错误的话会继续分析下一个方法,不会被中断,但是会记录下出错的位置,最后将所有出错的地方进行汇总输出。
默认情况下,每次运行 infer 命令都会删除之前的 infer-out 文件夹。你可以通过 --incremental 参数使用增量模式。增量模式下,运行 infer 命令不会删除 infer-out 文件夹,但是会利用这个文件夹进行 diff,减少分析量。
一般进行全新一轮分析时直接使用默认的非增量模式,而对于只想分析修改部分情况时,就使用增量模式。
Infer 检查的结果,在 infer-out 目录下,是 JSON 格式的,名字叫做 report.json 。生成 JSON 格式的结果,通用性会更强,集成到其他系统时会更方便。
总结
Clang 静态分析器、Infer 和 OCLint 这三个 iOS 静态分析工具都是基于 Clang 库开发的。
其中 Clang 静态分析器和 Xcode 的集成度高,也支持命令行。不过,它们检查的规则少,基本都是只能检查出较大的问题,比如类型转换问题,而对内存泄露问题检查的侧重点则在于可用性。OCLint 检查规则多、定制性强,能够发现很多潜在问题。但缺点也是检查规则太多,反而容易找不到重点;可定制度过高,导致易用性变差。
Infer 的效率高,支持增量分析,可小范围分析。可定制性不算最强,属于中等。综合来看,Infer 在准确性、性能效率、规则、扩展性、易用性整体度上的把握是做得最好的。
参考链接:
OCLint在Xcode中的使用