iOS 静态分析

本文是<<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 值表示方法或类做的事情太多,应该拆分或重构。一般方法行数不过百,类的行数不过千。

但是,使用静态分析技术来保证工程质量,也并不尽如人意,还有如下两大缺陷:

  1. 需要耗费更长的时间。相比于编译过程,使用静态分析技术发现深层次程序错误时,会对当前分析的方法、参数、变量去和整个工程关联代码一起做分析。所以,随着工程代码量的增加,每一步分析所依赖的影响面都会增大,所需耗时就更长。
    虽然在设计静态分析器时,就已经对其速度做了很多优化,但还是达不到程序编译的速度。因为静态分析本身就包含了编译最耗时的 IO 和语法分析阶段,而且静态分析的内容多于编译,所以再怎么优化,即使是最好的情况也会比编译过程来得要慢。
  2. 静态分析器只能检查出那些专门设计好的、可查找的错误。对于特定类型的错误分析,还需要开发者靠自己的能力写一些插件并添加进去。

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+kclean项目,然后再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 文件配置规则
image.png

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 格式的结果,通用性会更强,集成到其他系统时会更方便。

image.png

总结

Clang 静态分析器、Infer 和 OCLint 这三个 iOS 静态分析工具都是基于 Clang 库开发的。

其中 Clang 静态分析器和 Xcode 的集成度高,也支持命令行。不过,它们检查的规则少,基本都是只能检查出较大的问题,比如类型转换问题,而对内存泄露问题检查的侧重点则在于可用性。OCLint 检查规则多、定制性强,能够发现很多潜在问题。但缺点也是检查规则太多,反而容易找不到重点;可定制度过高,导致易用性变差。

Infer 的效率高,支持增量分析,可小范围分析。可定制性不算最强,属于中等。综合来看,Infer 在准确性、性能效率、规则、扩展性、易用性整体度上的把握是做得最好的。

参考链接:
OCLint在Xcode中的使用

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容