前言
隨著團隊人員的增多,開發(fā)人員的編碼風格不一致帶來的開發(fā)效率的低下,以及編寫代碼中容易犯錯的一些問題,盡管在人工code review 的能夠發(fā)現(xiàn)并且解決,但是效率將會大大降低,而且依靠人工來保證項目代碼質(zhì)量本身就不牢靠。我們必須在編碼階段,打包交付測試前就發(fā)現(xiàn)編寫代碼的潛在問題,并且解決這些問題來提高工程代碼質(zhì)量。
本文就是筆者在實際項目中運用OCLint對整個項目進行的一次實踐,記錄在實踐過程中的思路和所遇到的坑點,分享給大家。
筆者的所使用的開發(fā)工具和和開發(fā)環(huán)境如下:
Mac系統(tǒng)版本:macOS Mojave 10.14.5
ruby版本:2.6.3p62
gem版本:3.0.3
Xcode版本: 10.2.1
OCLint版本: 0.13
xcpretty版本: 0.3.0 # 這個是在項目文件夾下使用gem配置了的本地ruby環(huán)境安裝的,后面會說明為啥不安裝在系統(tǒng)默認的ruby環(huán)境下
筆者使用OCLint所做的codereview是結(jié)合Xcode在編碼開發(fā)階段進行的,ocline結(jié)合xcode的配置教程,這么做的優(yōu)點是:能夠在在開發(fā)階段發(fā)現(xiàn)編寫代碼的潛在問題,把問題提前暴露出來。缺點就是會:延長開發(fā)編譯時長,降低開發(fā)效率。
再講如何安裝OCLint和使用之前,我先講一下衡量代碼質(zhì)量的幾個指標:
代碼質(zhì)量衡量指標
-
Cyclomatic Complexity:循環(huán)復雜度(又叫圈復雜度),用來表示程序的復雜度,圈復雜度越高,代碼就越難復雜難維護。OCLint給的默認閾值是10。
復雜度計算:M = E ? N + 2P,其中 E 為圖中邊的個數(shù),N 為圖中節(jié)點的個數(shù),P 為連接組件的個數(shù)。
簡單程序的控制流圖。此程序由紅色的節(jié)點開始運行,然后進入循環(huán)(紅色節(jié)點下由三個節(jié)點組成),離開循環(huán)后有條件分支,最后運行藍色節(jié)點后結(jié)束,此控制流圖中,E = 9, N = 8, P = 1,因此其循環(huán)復雜度為 9 - 8 + (2*1) = 3。
具體可見維基百科介紹 和 這篇文章的計算方法。
下面我用例子介紹一個簡單的計算方式:如下
例子:下面這段代碼的 M = 2(if) + 1(for) + 1(for) + 2(if) + 2(if) + 2(if) + 1 = 11.
- (UIBezierPath *)pathForRect:(CGRect)rect {
UIBezierPath *path = [[UIBezierPath alloc] init];
if (self.width < 1.0) {
self.width = self.segments.firstObject?self.segments.firstObject.width:1;
}
NSArray *segments = [_segments copy];
xxxxxSegment *lastSegment = nil;
xxxxxPoint *lastPoint = nil;
for (xxxxxSegment *segment in segments) {
xxxxxPoint *firstPoint = nil;
for (xxxxxPoint *point in segment.points) {
if (!firstPoint) {
firstPoint = point;
if (!lastSegment) {
[path moveToPoint:CGPoint(point)];
[path addLineToPoint:CGPoint(point)];
} else {
xxxxxPoint *lastPoint = lastSegment.points.lastObject;
if (![lastPoint isEqual:firstPoint]) {
[path addLineToPoint:CGPoint(lastPoint)];
[path moveToPoint:CGPoint(firstPoint)];
} else {
[path addQuadCurveToPoint:MID_CGPoint(lastPoint, firstPoint) controlPoint:CGPoint(lastPoint)];
}
}
} else {
NSAssert(lastPoint, @"last point should not be nil");
[path addQuadCurveToPoint:MID_CGPoint(lastPoint, point) controlPoint:CGPoint(lastPoint)];
}
lastPoint = point;
}
lastSegment = segment;
}
if (lastPoint) {
[path addLineToPoint:CGPoint(lastPoint)];
}
return path;
}
-
NPath Complexity:NPath復雜度,用來表示一個方法所有可能執(zhí)行路徑的總和。OCLint給的默認閾值是200。NPath復雜度越高,代碼越難以被理解。
例子:下面的例子NPath復雜度為4。
void example()
{
if (xx) // 1
{
}
else // 2
{
}
if (xx) // 1
{
}
else // 2
{
}
}
-
Non Commenting Source Statements:除去空語句,注釋代碼之后的源代碼行數(shù)。OCLint給方法的默認閾值是30,給一個類文件的默認閾值是1000。當NCSS過高,代碼的維護成本就會提高,此時就得考慮方法和類的瘦身,進行拆分和重構(gòu)。
例子:
void example() // 1
{
if (1) // 2
{
}
else // 3
{
}
}
-
Statement Depth:語句嵌套深度。OCLint給方法的默認閾值是5。
例子:
if (1)
{ // 1
{ // 2
{ // 3
}
}
}
為什么選擇則OCLint
iOS靜態(tài)代碼分析工具對比:
-
Xcode自帶的Analyzer,使用方式超級簡單Product > Analyze或者快捷鍵shift + command + B。
能夠進行以下的問題檢測,支持的語言包括C, C++ 和 Objective-C,不可進行自定義;
?邏輯缺陷,例如訪問未初始化的變量和解除引用空指針;
?內(nèi)存管理缺陷,例如內(nèi)存泄漏;
?未使用的變量;
?由于不遵循項目使用的框架和庫所需的策略而導致的API使用缺陷。 -
Facebook開源的Infer,使用方式用command line,可以持續(xù)集成
能夠檢查的bug類型比Analyzer豐富,具體見官方文檔。 -
OCLint, 優(yōu)點是可檢查的規(guī)則最多,并且具有高可定制性,缺點是集成到Xcode中進行檢測效率低。官方定義的支持的檢查規(guī)則有71條,詳見官方規(guī)則。
以上三個工具的底層原理都類似,都需要用到Clang進行碼編譯后的產(chǎn)物,然后進行分析。
綜上比較,因OCLint的可定制化最高,并且可以和Xcode無縫結(jié)合,所以我們團隊選擇使用OCLint可以非常方便和統(tǒng)一的進行項目工程代碼質(zhì)量檢測并且修改。
OCLint安裝
官方文檔上提供了三種安裝方式了,分別是:Homebrew、下載安裝包安裝、源代碼編譯安裝;
如果需要自定義檢測規(guī)則,則必須使用第三種安裝方式:源代碼編譯安裝。不過筆者尚未嘗試過,到后期根據(jù)團隊的項目實際需要,如果需自定義則會嘗試使用。
筆者使用的是最簡單的Homebrew安裝方式:
brew tap oclint/formulae
brew install oclint
有以下信息則表示安裝成功
$ oclint
oclint: Not enough positional command line arguments specified!
Must specify at least 1 positional arguments: See: oclint -help
xcpretty安裝
由于筆者使用方式是直接在xcode的Build Pahses添加的Run Script腳本,在使用gem install xcpretty的安裝方式在系統(tǒng)的ruby環(huán)境安裝會在編譯的時候會報錯,xcpretty command not found,原因就是xcode和teminal的環(huán)境不一樣,盡管在terminal上能很好工作但是在xcode就會報找不到xcprrety的錯誤。當時找到的一種解決方式直接在sh腳本中將xcpretty寫為絕對路徑(找到安裝的絕對路徑就是which xcpretty),但是這種方式的弊端就是組內(nèi)成員沒法協(xié)同開發(fā),畢竟比無法保證其他伙伴安裝的xcpretty的絕對路徑和你的保持一致,座椅最終放棄這種方式。最后在詢問團隊其他人員的協(xié)助下找到了另一個解決方案:使用bundler在工程目錄維護一個管理ruby gem,詳細介紹見官方文檔。
首先安裝bundler,在終端執(zhí)行如下命令:
gem install bundler
在xcode工程根目錄寫一個Gemfile,內(nèi)容如下
source 'https://rubygems.org'
gem 'xcpretty', '0.3.0'
然后執(zhí)行
bundle install
執(zhí)行完成后,會有一個ruby文件夾的生成,這個就是本地的ruby環(huán)境,xcpretty就安裝完成。

.oclint 規(guī)則配置文件編寫
官方可配置的71條規(guī)則
在項目根目錄編寫一個.oclint文件,筆者的項目使用的規(guī)則內(nèi)容如下:
rule-configurations:
- key: CYCLOMATIC_COMPLEXITY # Cyclomatic complexity of a method 10
value: 30
- key: LONG_LINE
value: 110
- key: NCSS_METHOD # Number of non-commenting source statements of a method 30
value: 50
- key: LONG_VARIABLE_NAME
value: 40
- key: NESTED_BLOCK_DEPTH
value: 6
- key: MINIMUM_CASES_IN_SWITCH
value: 2
- key: SHORT_VARIABLE_NAME
value: 1
- key: TOO_MANY_METHODS
value: 50
- key: LONG_METHOD
value: 100
disable-rules:
- RedundantLocalVariable
- SHORT_VARIABLE_NAME
- LongVariableName
- UnnecessaryElseStatement
- RedundantNilCheck
- RedundantIfStatement
- InvertedLogic
- AssignIvarOutsideAccessors
- UseObjectSubscripting
- BitwiseOperatorInConditional
- PreferEarlyExit
- UnusedMethodParameter
max-priority-1: 1000
max-priority-2: 1000
max-priority-3: 1000
enable-clang-static-analyzer: false
sh腳本編寫
# Type a script or drag a script file from your workspace to insert its path.
export LC_CTYPE=en_US.UTF-8
set -euo pipefail # 腳本只要發(fā)生錯誤,就終止執(zhí)行
# 刪除DerivedData的build文件
#echo $(dirname ${BUILD_DIR})
rm -rf $(dirname ${BUILD_DIR})
# 1. 環(huán)境配置,判斷是否安裝oclint,沒有則安裝
if which oclint 2>/dev/null; then
echo 'oclint already installed'
else # install oclint
brew tap oclint/formulae
brew install oclint
fi
# 2.0 使用xcodebuild構(gòu)建項目,并且使用xcprretty將便于產(chǎn)物轉(zhuǎn)換為json
projectDir=${PROJECT_DIR}
prettyPath="${projectDir}/ruby/2.6.0/gems/xcpretty-0.3.0/bin/xcpretty" # 替換為你安裝的本地路徑
#echo ${prettyPath}
projectName="xxxxxxx" # 替換為你的project name
xcodebuild -scheme ${projectName} -workspace ${projectName}.xcworkspace clean && xcodebuild clean && xcodebuild -scheme ${projectName} -workspace ${projectName}.xcworkspace -configuration Debug -sdk iphonesimulator COMPILER_INDEX_STORE_ENABLE=NO | ${prettyPath} -r json-compilation-database -o compile_commands.json
# 3.0 判斷json是否
if [ -f ./compile_commands.json ]; then echo "compile_commands.json 文件存在";
else echo "-----compile_commands.json文件不存在-----"; fi
# 4.0 oclint分析json
oclint-json-compilation-database -e Pods -- -report-type xcode

然后command + B之后,去喝泡一杯咖啡,回來你就能夠看到工程代碼的warning然后修改代碼。
Clang-format對團隊代碼進行風格統(tǒng)一
筆者使用的.clangformat的配置如下,具體見gist:
---
# Language: ObjC
BasedOnStyle: Google
AccessModifierOffset: 0
ConstructorInitializerIndentWidth: 4
SortIncludes: false
# 連續(xù)賦值時,對齊所有等號
# AlignConsecutiveAssignments: true
AlignAfterOpenBracket: true
AlignEscapedNewlinesLeft: true
AlignOperands: false
AlignTrailingComments: true
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortBlocksOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: false
AllowShortIfStatementsOnASingleLine: false
# AllowShortFunctionsOnASingleLine: All
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: false
AlwaysBreakTemplateDeclarations: false
AlwaysBreakBeforeMultilineStrings: false
BreakBeforeBinaryOperators: None
BreakBeforeTernaryOperators: false
BreakConstructorInitializersBeforeComma: false
BinPackArguments: true
BinPackParameters: true
ColumnLimit: 110
ConstructorInitializerAllOnOneLineOrOnePerLine: true
DerivePointerAlignment: false
ExperimentalAutoDetectBinPacking: false
IndentCaseLabels: true
IndentWrappedFunctionNames: false
IndentFunctionDeclarationAfterType: false
MaxEmptyLinesToKeep: 1 # 連續(xù)的空行保留幾行
KeepEmptyLinesAtTheStartOfBlocks: false
NamespaceIndentation: Inner
ObjCBlockIndentWidth: 4
ObjCSpaceAfterProperty: true
ObjCSpaceBeforeProtocolList: true
PenaltyBreakBeforeFirstCallParameter: 10000
PenaltyBreakComment: 300
PenaltyBreakString: 1000
PenaltyBreakFirstLessLess: 120
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 200
PointerAlignment: Right
SpacesBeforeTrailingComments: 1
Cpp11BracedListStyle: true
Standard: Auto
IndentWidth: 4
TabWidth: 4
UseTab: Never
BreakBeforeBraces: Custom
BraceWrapping:
AfterClass: true
AfterControlStatement: false
AfterEnum: false
AfterFunction: false
AfterNamespace: true
AfterObjCDeclaration: false # ObjC定義后面是否換行
AfterStruct: false
AfterUnion: false
BeforeCatch: false
BeforeElse: false
IndentBraces: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
SpacesInAngles: false
SpaceInEmptyParentheses: false
SpacesInCStyleCastParentheses: false
SpaceAfterCStyleCast: false
SpacesInContainerLiterals: true
SpaceBeforeAssignmentOperators: true
ContinuationIndentWidth: 4
CommentPragmas: '^ IWYU pragma:'
ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ]
SpaceBeforeParens: ControlStatements
DisableFormat: false
...
使用命令clang-format -i [files]