1. 程式人生 > IOS開發 >如何通過靜態分析提高iOS程式碼質量

如何通過靜態分析提高iOS程式碼質量

cover
隨著專案的擴大,依靠人工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

$ brew install swiftlint
複製程式碼

這種是全域性安裝,各個應用都可以使用。 方式二:通過CocoaPods

pod 'SwiftLint',:configurations => ['Debug']
複製程式碼

這種方式相當於把SwiftLint作為一個三方庫整合進了專案,因為它只是除錯工具,所以我們應該將其指定為僅Debug環境下生效。

整合進Xcode

我們需要在專案中的Build Phases,新增一個Run Script Phase。如果是通過homebrew安裝的,你的指令碼應該是這樣的。

if which swiftlint >/dev/null; then
  swiftlint
else
  echo "warning: SwiftLint not installed,download from https://github.com/realm/SwiftLint"
fi
複製程式碼

如果是通過cocoapods安裝的,你得指令碼應該是這樣的:

"${PODS_ROOT}/SwiftLint/swiftlint"
複製程式碼

執行SwiftLint

鍵入CMD + B編譯專案,在編譯完後會執行我們剛才加入的指令碼,之後我們就能看到專案中大片的警告資訊。有時候build資訊並不能填入專案程式碼中,我們可以在編譯的log日誌裡檢視。

定製

SwiftLint規則太多了,如果我們不想執行某一規則,或者想要濾掉對Pods庫的分析,我們可以對SwfitLint進行配置。

在專案根目錄新建一個.swiftlint.yml檔案,然後填入如下內容:

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)
複製程式碼

一條rules提示如下,其對應的rules名就是function_body_length

! 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):

# reporter type (xcode,markdown)
$ swiftlint lint --reporter html > swiftlint.html
複製程式碼

xcodebuild

xcodebuild是xcode內建的編譯命令,我們可以用它來編譯打包我們的iOS專案,接下來介紹的Infer和OCLint都是基於xcodebuild的編譯產物進行分析的,所以有必要簡單介紹一下它。

一般編譯一個專案,我們需要指定專案名,configuration,scheme,sdk等資訊以下是幾個簡單的命令及說明。

# 不帶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操作

暫不支援自定義規則。

安裝及使用

$ brew install infer
複製程式碼

執行infer

$ 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格式:

$ infer explore --html
複製程式碼

點選trace,我們會看到該問題程式碼的上下文。

因為Infer預設是增量編譯,只會分析變動的程式碼,如果我們想整體編譯的話,需要clean一下專案:

$ xcodebuild -workspace "Project.xcworkspace" -scheme "Scheme" -configuration Debug -sdk iphonesimulator clean
複製程式碼

再次執行Infer去編譯。

$ infer run --skip-analysis-in-path Pods -- xcodebuild -workspace "Project.xcworkspace" -scheme "Scheme" -configuration Debug -sdk iphonesimulator
複製程式碼

Infer的大致原理

Infer的靜態分析主要分兩個階段:

1、捕獲階段

Infer 捕獲編譯命令,將檔案翻譯成 Infer 內部的中間語言。

這種翻譯和編譯類似,Infer 從編譯過程獲取資訊,並進行翻譯。這就是我們呼叫 Infer 時帶上一個編譯命令的原因了,比如: infer -- clang -c file.c,infer -- javac File.java。結果就是檔案照常編譯,同時被 Infer 翻譯成中間語言,留作第二階段處理。特別注意的就是,如果沒有檔案被編譯,那麼也沒有任何檔案會被分析。

Infer 把中間檔案儲存在結果資料夾中,一般來說,這個資料夾會在執行 infer 的目錄下建立,命名是 infer-out/

2、分析階段

在分析階段,Infer 分析 infer-out/ 下的所有檔案。分析時,會單獨分析每個方法和函式。

在分析一個函式的時候,如果發現錯誤,將會停止分析,但這不影響其他函式的繼續分析。

所以你在檢查問題的時候,修復輸出的錯誤之後,需要繼續執行 Infer 進行檢查,知道確認所有問題都已經修復。

錯誤除了會顯示在標準輸出之外,還會輸出到檔案 infer-out/bug.txt 中,我們過濾這些問題,僅顯示最有可能存在的。

在結果資料夾中(infer-out),同時還有一個 csv 檔案 report.csv,這裡包含了所有 Infer 產生的資訊,包括:錯誤,警告和資訊。

OCLint

OCLint是基於Clange Tooling編寫的庫,它支援擴充套件,檢測的範圍比Infer要大。不光是隱藏bug,一些程式碼規範性的問題,例如命名和函式複雜度也均在檢測範圍之內。

安裝OCLint

OCLint一般通過Homebrew安裝

$ brew tap oclint/formulae   
$ brew install oclint
複製程式碼

通過Hombrew安裝的版本為0.13。

$ 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).
複製程式碼

我分別用Xcode11在兩個專案上執行過OCLint,一個例項專案可以正常執行,另一個複雜的專案卻執行失敗,報如下錯誤:

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、安裝CMakeNinja這兩個編譯工具

$ brew install cmake ninja
複製程式碼

2、clone OCLint專案

$ git clone https://github.com/oclint/oclint
複製程式碼

3、進入oclint-scripts目錄,執行make命令

$ ./make
複製程式碼

成功之後會出現build資料夾,裡面有個oclint-release就是編譯成功的oclint工具。

設定oclint工具的環境變數

設定環境變數的目的是為了我們能夠快捷訪問。然後我們需要配置PATH環境變數,注意OCLint_PATH的路徑為你存放oclint-release的路徑。將其新增到.zshrc,或者.bash_profile檔案末尾:

OCLint_PATH=/Users/zhangferry/oclint/build/oclint-release
export PATH=$OCLint_PATH/bin:$PATH
複製程式碼

執行source .zshrc,重新整理環境變數,然後驗證oclint是否安裝成功:

$ oclint --version
OCLint (http://oclint.org/):
OCLint version 0.15.
Built May 19 2020 (11:48:49).
複製程式碼

出現這個介紹就說明我們已經完成了安裝。

安裝xcpretty

xcpretty是一個格式化xcodebuild輸出內容的指令碼工具,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的編譯配置也改為此選項
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
複製程式碼

使用方式

1、進入專案根目錄,執行如下指令碼:

$ 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檔案的分析,為了防止行數上限,我們加上行數的限制:

$ 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
複製程式碼

最終會產生一個oclintReport.html檔案。

OCLint支援自定義規則,因為其本身規則已經很豐富了,自定義規則的需求應該很小,也就沒有嘗試。

封裝指令碼

OCLint跟Infer一樣都是通過執行幾個指令碼語言進行執行的,我們可以將這幾個命令封裝成一個指令碼檔案,以OCLint為例,Infer也類似:

#!/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

../_images/xcode_screenshot_1.png

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資訊:

../_images/xcode_screenshot_8.png

總結

以下是對這幾種靜態分析方案的對比,我們可以根據需求選擇適合自己的靜態分析方案。

SwiftLint Infer OCLint
支援語言 Swift C、C++、OC、Java C、C++、OC
易用性 簡單 較簡單 較簡單
能否整合進Xcode 可以 不能整合進xcode 可以
自帶規則豐富度 較多,包含程式碼規範 相對較少,主要檢測潛在問題 較多,包含程式碼規範
規則擴充套件性 可以 不可以 可以

參考

OCLint 實現 Code Review - 給你的程式碼提提質量

Using OCLint in Xcode

Infer 的工作機制

LLVM & Clang 入門