随着公司业务的发展, 完全依赖人工保证工程质量也变得越来越不牢靠. 所以, 静态分析, 这种可以帮助我们在编写代码的阶段就能及时发现代码错误, 从而在根儿上保证工程质量的技术, 就成为了 iOS 开发者最常用的代码调试技术.
Infer 的介绍
下面, 我就跟大家介绍一款在 iOS 开发中常用的静态分析工具 - Infer:
Infer 是 Facebook 开源的, 使用 OCaml(https://ocaml.org/)写成的, 可以对 Java,C 和 Objective-C 程序进行分析.
Infer 最早部署在 Facebook 内部, 用于发布移动应用之前对每一行代码进行分析, 目前 Facebook 使用此工具分析所开发的 Android,iOS 应用, 包括 Facebook Messenger,Instagram 和其他一些应用. Infer 不仅仅用于移动应用程序的分析, 还可以分析 C,Java 等不是 Android 系统的代码. 目前 Infer 着重于发现一些诸如空指针的访问, 资源和内存的泄露等导致手机程序崩溃或性能严重下降的问题.
Infer 的安装:
Infer 的安装分为两种方式:
如果电脑没有安装 homebrew 的话先安装一下, Homebrew 是一款 Mac OS 平台下的软件包管理工具, 拥有安装, 卸载, 更新, 查看, 搜索等很多实用的功能. Homebrew 的安装如下命令:
/usr/bin/Ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
为 MacOSX 安装 Infer 需要的依赖, 这些工具在后面编译时会用到, 命令行指令如下:
- brew install autoconf automake cmake opam pkg-config SQLite gmp mpfr
- brew cask install java
从源码安装
- # Checkout Infer
- Git clone https://github.com/facebook/infer.git
- cd infer
- # 编译 Infer
- ./build-infer.sh clang
- # 配置环境变量
- # 使用下面这句, 这句话是要在你下载的 infer 目录下执行
- echo "export PATH=\"\$PATH:`pwd`/infer/bin\"" \>> ~/.bash_profile
- # 下面句表示立即生效
- source ~/.bash_profile
- # 检查看有没有配置好:
- echo $PATH
直接安装 binary releases
brew install infer
infer 就安装好了.
Infer 的使用
接下来通过事例, 给大家分享下如何使用 Infer:
首先, 使用 Xcode 创建一个 OC 项目比如: 名字叫 InferTest.
分析单个文件
项目中创建一个类 InferTest 并且编写一段代码, 代码如下:
.h 文件:
- #import <Foundation/Foundation.h>
- @interface InferTest : NSObject
- @end
.m 文件:
- #import "InferTest.h"
- @interface InferTest ()
- @property NSMutableDictionary *dict;
- @end
- @implementation InferTest
- - (instancetype)init
- {
- self = [super init];
- if (self) {
- NSString *nsmeStr = nil;
- [self.dict setObject:nsmeStr forKey:@"name"];
- }
- return self;
- }
- @end
在终端输入:
cd InferTest.m 所在的目录
infer -- clang -c InferTest.m
结果如下:
- Found 3 issues
- InferTest.m:21: error: NULL_DEREFERENCE
- pointer `nsmeStr` last assigned on line 20 could be null and is dereferenced by call to `setObject:forKey:` at line 21, column 9.
- 19. if (self) {
- 20. NSString *nsmeStr = nil;
- 21.> [self.dict setObject:nsmeStr forKey:@"name"];
- 22. }
- 23. return self;
- InferTest.m:11: warning: ASSIGN_POINTER_WARNING
- Property `dict` is a pointer type marked with the `assign` attribute at line 11, column 1. Use a different attribute like `strong` or `weak`.
- 9. #import "InferTest.h"
- 10. @interface InferTest ()
- 11.> @property NSMutableDictionary *dict;
- 12. @end
- 13.
- InferTest.m:11: warning: ASSIGN_POINTER_WARNING
- Property `dict` is a pointer type marked with the `assign` attribute at line 11, column 1. Use a different attribute like `strong` or `weak`.
- 9. #import "InferTest.h"
- 10. @interface InferTest ()
- 11.> @property NSMutableDictionary *dict;
- 12. @end
- 13.
- Summary of the reports
- ASSIGN_POINTER_WARNING: 2
- NULL_DEREFERENCE: 1
一个错误两个警告:
一个错误是:
- pointer `nsmeStr` last assigned on line 20 could be null and is dereferenced by call to `setObject:forKey:` at line 21, column 9.
- 19. if (self) {
- 20. NSString *nsmeStr = nil;
- 21.> [self.dict setObject:nsmeStr forKey:@"name"];
- 22. }
- 23. return self;
意思就是第 20 行的 nsmeStr 为空, 被第 21 行 setObject: forKey: 所引用.
我们都知道可变字典 setObject: forKey: 赋值, object 不能为 nil, 否则程序会崩溃错误.
然后我们给 nameStr 赋上非空值:
NSString *nsmeStr = @"张三";
重新运行分析编译命令:
infer -- clang -c InferTest.m
发现就只有两个警告了, 如下:
- Found 2 issues
- InferTest.m:11: warning: ASSIGN_POINTER_WARNING
- Property `dict` is a pointer type marked with the `assign` attribute at line 11, column 1. Use a different attribute like `strong` or `weak`.
- 9. #import "InferTest.h"
- 10. @interface InferTest ()
- 11.> @property NSMutableDictionary *dict;
- 12. @end
- 13.
- InferTest.m:11: warning: ASSIGN_POINTER_WARNING
- Property `dict` is a pointer type marked with the `assign` attribute at line 11, column 1. Use a different attribute like `strong` or `weak`.
- 9. #import "InferTest.h"
- 10. @interface InferTest ()
- 11.> @property NSMutableDictionary *dict;
- 12. @end
- 13.
- Summary of the reports
- ASSIGN_POINTER_WARNING: 2
意思就是属性 dict 应该用 strong 或者 weak 来修饰. 接下来我们把 dict 的修饰属性加上:
@property (nonatomic,strong)NSMutableDictionary *dict;
重新运行分析编译命令:
infer -- clang -c InferTest.m
结果发现就没有问题了:
No issues found
分析整个项目
命令行如下:
cd 到项目所在的目录
infer -- xcodebuild -target InferTest -configuration Debug -sdk iphonesimulator
分析结果如下:
- Found 1 issue
- InferTest/InferTest.m:21: error: NULL_DEREFERENCE
- pointer `nsmeStr` last assigned on line 20 could be null and is dereferenced by call to `setObject:forKey:` at line 21, column 9.
- 19. if (self) {
- 20. NSString *nsmeStr = nil;
- 21.> [self.dict setObject:nsmeStr forKey:@"name"];
- 22. }
- 23. return self;
- Summary of the reports
- NULL_DEREFERENCE: 1
发现了空值的问题, 但是没有报告可变字典属性修饰的警告, 跟单个文件编译略有差异.
分析带 CocoaPods 的项目
命令行如下:
cd 到项目所在目录
- //*** 代表项目名或者 scheme 名
- infer -- xcodebuild -workspace ./***.xcworkspace -configuration Debug -scheme ***
分析结果和正常项目一样, 分析结果在: 项目目录 ->infer-out->bugs.txt
- Found 1 issue
- InferTest/InferTest.m:21: error: NULL_DEREFERENCE
- pointer `nsmeStr` last assigned on line 20 could be null and is dereferenced by call to `setObject:forKey:` at line 21, column 9.
- 19. if (self) {
- 20. NSString *nsmeStr = nil;
- 21.> [self.dict setObject:nsmeStr forKey:@"name"];
- 22. }
- 23. return self;
- Summary of the reports
- NULL_DEREFERENCE: 1
过滤掉我们引入的第三库等不想做分析的文件
方法一:
而当我们在项目中使用了很多第三方的时候, 其实我们只想让 Infer 分析我们的代码, 而不想分析第三方的代码, 不然分析报告中会有很多第三方的 issue, 看着混乱, 这时我们可以用命令行过滤掉关于不想分析的文件. 比如用到 CocoaPods 的项目, 我们想过滤掉 Pods 引入的第三方库, 命令行如下:
INFER_ARGS="--skip-clang-analysis-in-path^[\"Pods\"]" infer -- xcodebuild -workspace ./***.xcworkspace -configuration Debug -scheme ***
in-path 后面跟的是一个忽略文件或者文件夹的路径数组.
方法二:
在项目的根目录创建一个. inferconfig 类型的配置文件(我以这种方式过滤 Pods 引入的第三方库没有成功, 如果有简友过滤成功还望不吝告知一下)
- How to include project folders or exclude dependency folders during analysis of iOS project? I
- Yes, there is a way to exclude certain directories. I'll illustrate it by example let me know if it works for you.
- If your use case is different/more complex let me know as well.
- Let's say I have a project with following directory structure.
- <ROOT>
- - <Lib> // libraries that your project depends on.
- - <ExternalDeps> // you want to skip analyzing those
- - <InternalLibs> // you want to analyze those
- - <Src> // implementation of your project
- - <Headers> // headers of your project
- - <Docs> // irrelevant
- We have a way of providing extra information to guide the analyzer by .inferconfig file (it's in JSON format)
- Step 1:
- Create .inferconfig file (name is important) in <ROOT> directory
- Step 2:
- Add entry for skip-clang-analysis-in-path with list of all paths relative to <ROOT>.
- .inferconfig file should look like this:
- {
- "skip-clang-analysis-in-path": [
- "Lib/ExternalDeps"
- ]
- }
- Step 3:
- Run infer from <ROOT> directory (it's important for infer to find .inferconfig file there). If you have to run infer from different directory, you have pass --project-root flag to your infer invocation. And that's all.
Infer 工作流程
Infer 工作的流程:
第一个阶段是转化阶段, 将源代码转成 Infer 内部的中间语言. 类 C 语言使用 Clang 进行编译, Java 语言使用 javac 进行编译, 编译的同时转成中间语言, 输出到 infer-out 目录.
第二个阶段是分析阶段, 分析 infer-out 目录下的文件, 分析每个方法, 如果出现错误的话会继续分析下一个方法, 不会被中断, 但是会记录下出错的位置, 最后将所有出错的地方进行汇总输出. 默认情况下, 每次运行 infer 命令都会删除之前的 infer-out 文件, 你可以通过 --incremental 参数使用增量模式. 增量模式下, 运行 infer 命令不会删除 infer-out 文件, 但是会利用这个文件夹进行 diff, 减少分析量.
Infer 的工作流程图如下:
Infer 的工作流程图
Infer 使用过程中遇到的一些问题:
分析某一个文件报错(fatal error: 'UIKit/UIKit.h' file not found)
解决方案:
- // 在命令中加 -isysroot /Applications/Xcode.App/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk
- infer -- clang -c -isysroot /Applications/Xcode.App/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk ViewController.m
分析带 ARC 特有的修饰 (如 weak 或者__weak) 或者其他的情况:
- // 命令行中加 - fobjc-arc
- infer -- clang -c -fobjc-arc InferTest.m
如果 infer 分析编译报错, 忽略错误继续分析
infer --keep-going
有时候发现编译分析结果不全, 此时可以先清理一下, 然后再编译:
- // 工程清理命令
- xcodebuild -target InferTest -configuration Debug -sdk iphonesimulator clean
- // 或者带 CocoaPods 的项目清理命令
- xcodebuild -workspace ./InferTest.xcworkspace -configuration Debug -scheme InferTest clean
然后再运行 Infer 分析的命令
遇到更多的问题请到 GitHub 上 Facebook Infer 的仓库 issues 中去找, 绝大多数的问题都能找到你想要的答案, 传送门: https://github.com/facebook/infer/issues
参考文献:
戴铭的 Clang,Infer 和 OCLint , 我们应该使用谁来做静态分析
来源: http://www.jianshu.com/p/53ad11e6f0c5