如何通过静态分析提高iOS代码质量?
<h3 id="前言">前言:</h3><p>随着项目的扩大,依靠人工codereview来保证项目的质量,越来越不现实,这时就有必要借助于一种自动化的代码审查工具:<strong>程序静态分析</strong>。</p>
<p>程序静态分析(Program Static Analysis)是指在不运行代码的方式下,通过词法分析、语法分析、控制流、数据流分析等技术对程序代码进行扫描,验证代码是否满足规范性、安全性、可靠性、可维护性等指标的一种代码分析技术。(来自百度百科)</p>
<p>词法分析,语法分析等工作是由编译器进行的,所以对iOS项目为了完成静态分析,我们需要借助于编译器。对于OC语言的静态分析可以完全通过Clang,对于Swift的静态分析除了Clange还需要借助于SourceKit。</p>
<p>Swift语言对应的静态分析工具是SwiftLint,OC语言对应的静态分析工具有Infer和OCLitn。以下会是对各个静态分析工具的安装和使用做一个介绍。</p>
<h2 id="swiftlint">SwiftLint</h2>
<p>对于Swift项目的静态分析可以使用SwiftLint。SwiftLint 是一个用于强制检查 Swift 代码风格和规定的一个工具。它的实现是 Hook 了 Clang 和 SourceKit 从而能够使用 AST 来表示源代码文件的更多精确结果。Clange我们了解了,那SourceKit是干什么用的?</p>
<p>SourceKit包含在Swift项目的主仓库,它是一套工具集,支持Swift的大多数源代码操作特性:源代码解析、语法突出显示、排版、自动完成、跨语言头生成等工作。</p>
<blockquote>
<p><strong>作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:413038000,不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!</strong></p>
</blockquote>
<h4 id="推荐阅读">推荐阅读</h4>
<h4 id="ios开发最新-bat面试题合集持续更新中">iOS开发——最新 BAT面试题合集(持续更新中)</h4>
<h3 id="安装">安装</h3>
<p>安装有两种方式,任选其一: <strong>方式一:通过Homebrew</strong></p>
<pre><code>$ brew install swiftlint
复制代码
</code></pre>
<p>这种是全局安装,各个应用都可以使用。 <strong>方式二:通过CocoaPods</strong></p>
<pre><code>pod 'SwiftLint', :configurations => ['Debug']
复制代码
</code></pre>
<p>这种方式相当于把SwiftLint作为一个三方库集成进了项目,因为它只是调试工具,所以我们应该将其指定为仅Debug环境下生效。</p>
<h3 id="集成进xcode">集成进Xcode</h3>
<p>我们需要在项目中的<code>Build Phases</code>,添加一个<code>Run Script Phase</code>。如果是通过homebrew安装的,你的脚本应该是这样的。</p>
<pre><code>if which swiftlint >/dev/null; then
swiftlint
else
echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
fi
复制代码
</code></pre>
<p>如果是通过cocoapods安装的,你得脚本应该是这样的:</p>
<pre><code>"${PODS_ROOT}/SwiftLint/swiftlint"
复制代码
</code></pre>
<p><img src="https://upload-images.jianshu.io/upload_images/12311242-8ee6e2889bc33219.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"></p>
<h3 id="运行swiftlint">运行SwiftLint</h3>
<p>键入<code>CMD + B</code>编译项目,在编译完后会运行我们刚才加入的脚本,之后我们就能看到项目中大片的警告信息。有时候build信息并不能填入项目代码中,我们可以在编译的log日志里查看。</p>
<p><img src="https://upload-images.jianshu.io/upload_images/12311242-42b3808816c41930.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"></p>
<h3 id="定制">定制</h3>
<p>SwiftLint规则太多了,如果我们不想执行某一规则,或者想要滤掉对Pods库的分析,我们可以对SwfitLint进行配置。</p>
<p>在项目根目录新建一个<code>.swiftlint.yml</code>文件,然后填入如下内容:</p>
<pre><code>disabled_rules: # rule identifiers to exclude from running
- colon
- trailing_whitespace
- vertical_whitespace
- function_body_length
opt_in_rules: # some rules are only opt-in
- empty_count
# Find all the available rules by running:
# swiftlint rules
included: # paths to include during linting. `--path` is ignored if present.
- Source
excluded: # paths to ignore during linting. Takes precedence over `included`.
- Carthage
- Pods
- Source/ExcludedFolder
- Source/ExcludedFile.swift
- Source/*/ExcludedFile.swift # Exclude files with a wildcard
analyzer_rules: # Rules run by `swiftlint analyze` (experimental)
- explicit_self
# configurable rules can be customized from this configuration file
# binary rules can set their severity level
force_cast: warning # implicitly
force_try:
severity: warning # explicitly
# rules that have both warning and error levels, can set just the warning level
# implicitly
line_length: 110
# they can set both implicitly with an array
type_body_length:
- 300 # warning
- 400 # error
# or they can set both explicitly
file_length:
warning: 500
error: 1200
# naming rules can set warnings/errors for min_length and max_length
# additionally they can set excluded names
type_name:
min_length: 4 # only warning
max_length: # warning and error
warning: 40
error: 50
excluded: iPhone # excluded via string
allowed_symbols: ["_"] # these are allowed in type names
identifier_name:
min_length: # only min_length
error: 4 # only error
excluded: # excluded via string array
- id
- URL
- GlobalAPIKey
reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit, html, emoji, sonarqube, markdown)
复制代码
</code></pre>
<p>一条rules提示如下,其对应的rules名就是<code>function_body_length</code>。</p>
<pre><code>! Function Body Length Violation: Function body should span 40 lines or less excluding comments and whitespace: currently spans 43 lines (function_body_length)
复制代码
</code></pre>
<p><code>disabled_rules</code>下填入我们不想遵循的规则。</p>
<p><code>excluded</code>设置我们想跳过检查的目录,Carthage、Pod、SubModule这些一般可以过滤掉。</p>
<p>其他的一些像是文件长度(file_length),类型名长度(type_name),我们可以通过设置具体的数值来调节。</p>
<p>另外SwiftLint也支持自定义规则,我们可以根据自己的需求,定义自己的<code>rule</code>。</p>
<h3 id="生成报告">生成报告</h3>
<p>如果我们想将此次分析生成一份报告,也是可以的(该命令是通过homebrew安装的swiftlint):</p>
<pre><code># reporter type (xcode, json, csv, checkstyle, junit, html, emoji, sonarqube, markdown)
$ swiftlint lint --reporter html > swiftlint.html
复制代码
</code></pre>
<p><img src="https://upload-images.jianshu.io/upload_images/12311242-73d9a85d03faa83a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"></p>
<h2 id="xcodebuild">xcodebuild</h2>
<p>xcodebuild是xcode内置的编译命令,我们可以用它来编译打包我们的iOS项目,接下来介绍的Infer和OCLint都是基于xcodebuild的编译产物进行分析的,所以有必要简单介绍一下它。</p>
<p>一般编译一个项目,我们需要指定项目名,configuration,scheme,sdk等信息以下是几个简单的命令及说明。</p>
<pre><code># 不带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
复制代码
</code></pre>
<p><strong>之后对xcodebuild命令的使用都需要将这些参数替换为自己项目的参数。</strong></p>
<h2 id="infer">Infer</h2>
<p><img src="https://upload-images.jianshu.io/upload_images/12311242-d53667cb774ca837.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"></p>
<p>Infer是Facebook开发的针对C、OC、Java语言的静态分析工具,它同时支持对iOS和Android应用的分析。对于Facebook内部的应用像是 Messenger、Instagram 和其他一些应用均是有它进行静态分析的。它主要检测隐含的问题,主要包括以下几条:</p>
<ul>
<li>资源泄露,内存泄露</li>
<li>变量和参数的非空检测</li>
<li>循环引用</li>
<li>过早的nil操作</li>
</ul>
<p>暂不支持自定义规则。</p>
<h3 id="安装及使用">安装及使用</h3>
<pre><code>$ brew install infer
复制代码
</code></pre>
<p>运行infer</p>
<pre><code>$ cd projectDir
# 跳过对Pods的分析
$ infer run --skip-analysis-in-path Pods -- xcodebuild -workspace "Project.xcworkspace" -scheme "Scheme" -configuration Debug -sdk iphonesimulator
复制代码
</code></pre>
<p>我们会得到一个<code>infer-out</code>的文件夹,里面是各种代码分析的文件,有txt,json等文件格式,当这样不方便查看,我们可以将其转成html格式:</p>
<pre><code>$ infer explore --html
复制代码
</code></pre>
<p><img src="https://upload-images.jianshu.io/upload_images/12311242-d76ebc05a70f048b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"></p>
<p>点击trace,我们会看到该问题代码的上下文。</p>
<p>因为Infer默认是增量编译,只会分析变动的代码,如果我们想整体编译的话,需要clean一下项目:</p>
<pre><code>$ xcodebuild -workspace "Project.xcworkspace" -scheme "Scheme" -configuration Debug -sdk iphonesimulator clean
复制代码
</code></pre>
<p>再次运行Infer去编译。</p>
<pre><code>$ infer run --skip-analysis-in-path Pods -- xcodebuild -workspace "Project.xcworkspace" -scheme "Scheme" -configuration Debug -sdk iphonesimulator
复制代码
</code></pre>
<h3 id="infer的大致原理">Infer的大致原理</h3>
<p>Infer的静态分析主要分两个阶段:</p>
<p><strong>1、捕获阶段</strong></p>
<p>Infer 捕获编译命令,将文件翻译成 Infer 内部的中间语言。</p>
<p>这种翻译和编译类似,Infer 从编译过程获取信息,并进行翻译。这就是我们调用 Infer 时带上一个编译命令的原因了,比如: <code>infer -- clang -c file.c</code>, <code>infer -- javac File.java</code>。结果就是文件照常编译,同时被 Infer 翻译成中间语言,留作第二阶段处理。特别注意的就是,如果没有文件被编译,那么也没有任何文件会被分析。</p>
<p>Infer 把中间文件存储在结果文件夹中,一般来说,这个文件夹会在运行 <code>infer</code> 的目录下创建,命名是 <code>infer-out/</code>。</p>
<p><strong>2、分析阶段</strong></p>
<p>在分析阶段,Infer 分析 <code>infer-out/</code> 下的所有文件。分析时,会单独分析每个方法和函数。</p>
<p>在分析一个函数的时候,如果发现错误,将会停止分析,但这不影响其他函数的继续分析。</p>
<p>所以你在检查问题的时候,修复输出的错误之后,需要继续运行 Infer 进行检查,知道确认所有问题都已经修复。</p>
<p>错误除了会显示在标准输出之外,还会输出到文件 <code>infer-out/bug.txt</code> 中,我们过滤这些问题,仅显示最有可能存在的。</p>
<p>在结果文件夹中(<code>infer-out</code>),同时还有一个 csv 文件 <code>report.csv</code>,这里包含了所有 Infer 产生的信息,包括:错误,警告和信息。</p>
<h2 id="oclint">OCLint</h2>
<p>OCLint是基于Clange Tooling编写的库,它支持扩展,检测的范围比Infer要大。不光是隐藏bug,一些代码规范性的问题,例如命名和函数复杂度也均在检测范围之内。</p>
<h4 id="安装oclint">安装OCLint</h4>
<p>OCLint一般通过Homebrew安装</p>
<pre><code>$ brew tap oclint/formulae
$ brew install oclint
复制代码
</code></pre>
<p>通过Hombrew安装的版本为0.13。</p>
<pre><code>$ oclint --version
LLVM (http://llvm.org/):
LLVM version 5.0.0svn-r313528
Optimized build.
Default target: x86_64-apple-darwin19.0.0
Host CPU: skylake
OCLint (http://oclint.org/):
OCLint version 0.13.
Built Sep 18 2017 (08:58:40).
复制代码
</code></pre>
<p>我分别用Xcode11在两个项目上运行过OCLint,一个实例项目可以正常运行,另一个复杂的项目却运行失败,报如下错误:</p>
<pre><code>1 error generated
1 error generated
...
oclint: error: cannot open report output file ..../onlintReport.html
复制代码
</code></pre>
<p>我并不清楚原因,如果你想试试0.13能否使用的话,直接跳到安装xcpretty。如果你也遇到了这个问题,可以回来安装oclint0.15版本。</p>
<h4 id="oclint015">OCLint0.15</h4>
<p>我在oclint issuse #547这里找到了这个问题和对应的解决方案。</p>
<p>我们需要更新oclint至0.15版本。brew上的最新版本是0.13,github上的最新版本是0.15。我下载github上的release0.15版本,但是这个包并不是编译过的,不清楚是不是官方自己搞错了,只能手动编译了。因为编译要下载llvm和clange,这两个包较大,所以我将编译过后的包直接传到了这里CodeChecker。</p>
<p>如果不关心编译过程,可以下载编译好的包,跳到设置环境变量那一步。</p>
<p><strong>编译OCLint</strong></p>
<p>1、安装CMake和Ninja这两个编译工具</p>
<pre><code>$ brew install cmake ninja
复制代码
</code></pre>
<p>2、clone OCLint项目</p>
<pre><code>$ git clone https://github.com/oclint/oclint
复制代码
</code></pre>
<p>3、进入oclint-scripts目录,执行make命令</p>
<pre><code>$ ./make
复制代码
</code></pre>
<p>成功之后会出现build文件夹,里面有个oclint-release就是编译成功的oclint工具。</p>
<p><strong>设置oclint工具的环境变量</strong></p>
<p>设置环境变量的目的是为了我们能够快捷访问。然后我们需要配置PATH环境变量,注意OCLint_PATH的路径为你存放oclint-release的路径。将其添加到<code>.zshrc</code>,或者<code>.bash_profile</code>文件末尾:</p>
<pre><code>OCLint_PATH=/Users/zhangferry/oclint/build/oclint-release
export PATH=$OCLint_PATH/bin:$PATH
复制代码
</code></pre>
<p>执行<code>source .zshrc</code>,刷新环境变量,然后验证oclint是否安装成功:</p>
<pre><code>$ oclint --version
OCLint (http://oclint.org/):
OCLint version 0.15.
Built May 19 2020 (11:48:49).
复制代码
</code></pre>
<p>出现这个介绍就说明我们已经完成了安装。</p>
<h3 id="安装xcpretty">安装xcpretty</h3>
<p>xcpretty是一个格式化xcodebuild输出内容的脚本工具,oclint的解析依赖于它的输出。它的安装方式为:</p>
<pre><code>$ gem install xcpretty
复制代码
</code></pre>
<h3 id="oclint的使用">OCLint的使用</h3>
<p>在使用OCLint之前还需要一些准备工作,需要将编译项<code>COMPILER_INDEX_STORE_ENABLE</code>设置为NO。</p>
<ul>
<li>将 Project 和 Targets 中 Building Settings 下的 <code>COMPILER_INDEX_STORE_ENABLE</code> 设置为 <strong>NO</strong></li>
<li>在 podfile 中 <strong>target 'target' do 前面</strong>添加下面的脚本,将各个pod的编译配置也改为此选项</li>
</ul>
<pre><code>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
复制代码
</code></pre>
<h4 id="使用方式">使用方式</h4>
<p>1、进入项目根目录,运行如下脚本:</p>
<pre><code>$ xcodebuild -workspace ProjectName.xcworkspace -scheme ProjectScheme -configuration Debug -sdk iphonesimulator | xcpretty -r json-compilation-database -o compile_commands.json
复制代码
</code></pre>
<p>会将xcodebuild编译过程中的一些信息记录成一个文件<code>compile_commands.json</code>,如果我们在项目根目录看到了该文件,且里面是有内容的,证明我们完成了第一步。</p>
<p>2、我们将这个json文件转成方便查看的html,过滤掉对Pods文件的分析,为了防止行数上限,我们加上行数的限制:</p>
<pre><code>$ 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
复制代码
</code></pre>
<p>最终会产生一个<code>oclintReport.html</code>文件。</p>
<p><img src="https://upload-images.jianshu.io/upload_images/12311242-741a118888b20b14.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"></p>
<p>OCLint支持自定义规则,因为其本身规则已经很丰富了,自定义规则的需求应该很小,也就没有尝试。</p>
<p><strong>封装脚本</strong></p>
<p>OCLint跟Infer一样都是通过运行几个脚本语言进行执行的,我们可以将这几个命令封装成一个脚本文件,以OCLint为例,Infer也类似:</p>
<pre><code>#!/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
复制代码
</code></pre>
<p><code>oclint-json-compilation-database</code>命令的几个参数说明:</p>
<p><code>-e</code> 需要忽略分析的文件,这些文件的警告不会出现在报告中</p>
<p><code>-rc</code> 需要覆盖的规则的阀值,这里可以自定义项目的阀值,默认阀值</p>
<p><code>-enable-rule</code> 支持的规则,默认是oclint提供的都支持,可以组合-disable-rule来过滤掉一些规则 规则列表</p>
<p><code>-disable-rule</code> 需要忽略的规则,根据项目需求设置</p>
<h4 id="在xcode中使用oclint">在Xcode中使用OCLint</h4>
<p>因为OCLint提供了xcode格式的输出样式,所以我们可以将它作为一个脚本放在Xcode中。</p>
<p>1、在项目的 TARGETS 下面,点击下方的 "+" ,选择 cross-platform 下面的 Aggregate。输入名字,这里命名为 OCLint</p>
<p><img src="https://upload-images.jianshu.io/upload_images/12311242-b34fa34a105cbedc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"></p>
<p>2、选中该Target,进入Build Phases,添加Run Script,写入下面脚本:</p>
<pre><code># 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
复制代码
</code></pre>
<p>可以看出该脚本跟上面的脚本一样,只不过 将<code>oclint-json-compilation-database</code>命令的<code>-report-type</code>由<code>html</code>改为了<code>xcode</code>。而OCLint作为一个target本身就运行在特定的环境下,所以xcodebuild可以省去配置参数。</p>
<p>3、通过<code>CMD + B</code>我们编译一下项目,执行脚本任务,会得到能够定位到代码的warning信息:</p>
<p><img src="https://upload-images.jianshu.io/upload_images/12311242-a04218ef76574989.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"></p>
<h2 id="总结">总结</h2>
<p>以下是对这几种静态分析方案的对比,我们可以根据需求选择适合自己的静态分析方案。</p>
<table>
<thead>
<tr>
<th></th>
<th>SwiftLint</th>
<th>Infer</th>
<th>OCLint</th>
</tr>
</thead>
<tbody>
<tr>
<td>支持语言</td>
<td>Swift</td>
<td>C、C++、OC、Java</td>
<td>C、C++、OC</td>
</tr>
<tr>
<td>易用性</td>
<td>简单</td>
<td>较简单</td>
<td>较简单</td>
</tr>
<tr>
<td>能否集成进Xcode</td>
<td>可以</td>
<td>不能集成进xcode</td>
<td>可以</td>
</tr>
<tr>
<td>自带规则丰富度</td>
<td>较多,包含代码规范</td>
<td>相对较少,主要检测潜在问题</td>
<td>较多,包含代码规范</td>
</tr>
<tr>
<td>规则扩展性</td>
<td>可以</td>
<td>不可以</td>
<td>可以</td>
</tr>
</tbody>
</table>
<h2 id="参考">参考</h2>
<p>OCLint 实现 Code Review - 给你的代码提提质量</p>
<p>Using OCLint in Xcode</p>
<p>Infer 的工作机制</p>
<p>LLVM & Clang 入门</p>
<p>作者:zhangferry<br>
链接:https://juejin.im/post/5ec5de72e51d4578702f3e6f<br>
来源:掘金</p>
<blockquote>
<p><strong>作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:413038000,不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!</strong></p>
</blockquote>
<h4 id="推荐阅读-1">推荐阅读</h4>
<h4 id="ios开发最新-bat面试题合集持续更新中-1">iOS开发——最新 BAT面试题合集(持续更新中)</h4><br><br>
来源:https://www.cnblogs.com/iOSer1122/p/12957634.html
頁:
[1]