爲何項目維護困難、BUG 反覆?實際上不少時候就是代碼質量的問題。代碼架構就像是建築的鋼筋結構,代碼細節就像是建築的內部裝修,建築的抗震等級、簡裝或豪裝徹底取決於團隊開發人員的水平。前端
本文是筆者對於一些代碼質量技巧的小總結,編寫高質量代碼的思路在任何技術棧都是基本相通的,文章內容僅表明筆者的我的見解,拋磚引玉,不喜勿噴😁。算法
看這段 C 代碼:編程
int i = 100;
i++;
++i;
複製代碼
對於基本類型,沒法直接窺探內部實現,可是能夠經過彙編代碼來直接觀察實現邏輯,經過 clang 編譯器轉換的彙編代碼大體以下( i++ 和 ++i 彙編指令是相同的):swift
0x100000fa4 <+20>: movl $0x64, -0x14(%rbp)
0x100000fab <+27>: movl -0x14(%rbp), %edi
0x100000fae <+30>: addl $0x1, %edi
0x100000fb1 <+33>: movl %edi, -0x14(%rbp)
複製代碼
大體邏輯:將0x64
當即數寫入rbp
寄存器;將rbp
的值寫入edi
寄存器;edi
的值加一;將edi
的值寫回rbp
。設計模式
固然,clang 編譯器對++i
和i++
作了優化,它們的彙編代碼看起來是相同的,可是這不能說明全部的編譯器都對++i
和i++
一視同仁,並且也查看不了是否編譯器作了優化工做。因此在使用基本類型的時候,對於自增的單步操做,寫++i
是個好習慣(C++ STL 庫中有體現)。在平常開發中,for 循環能夠如此寫:緩存
for (int i = 0; i < 10; ++i) {}
複製代碼
好比 swift 中的 Int,它是一個結構體,經過寫一個擴展來定義++
運算符(實際開發中 swift 不建議這麼作,這裏只是舉個例子):安全
extension Int {
static prefix func ++ (i: inout Int) -> Int {
i += 1
return i
}
static postfix func ++ (i: inout Int) -> Int {
let tmp = i
i += 1
return tmp
}
}
複製代碼
這是編程語言中自定義++
前綴和後綴運算符經常使用的邏輯,後綴++
比前綴++
多了一個tmp
臨時變量。感謝百度工程師微博名稱 @提拉拉拉就是技術宅 指出問題,實際上 swift 中後綴++
運算符有更高效的實現:bash
...
defer {
i += 1
}
return i
...
複製代碼
無論是何種自定義的實現,後綴++
老是有更多的邏輯表達。因此,理論上對於自定義數據類型的單步自增操做,使用前綴++
能略微的提升效率。網絡
這一段描述引發了不少朋友的爭議,筆者簡單說明一下。數據結構
對於 C 中 Int 等自帶類型或者用戶自定義類型的前綴++
和後綴++
運算符,當單獨使用時 (++i; i++;
),無論它們上層代碼如何實現,最終均可能經過源碼級優化器和目標代碼優化器優化成相同的彙編代碼,可是它們畢竟是經過編譯器的優化轉化的,優化意味着時鐘週期的開銷,可能會加長編譯的時間。
既然咱們能經過代碼讓編譯器免去優化的過程,何樂不爲之?
固然,這有些吹毛求疵了,當作各位看官茶餘飯後的一點樂子吧 😁。
位運算效率很高,並且有不少巧妙的用法,這裏提出一個需求:
typedef enum : NSUInteger {
TestEnumA = 1,
TestEnumB = 1 << 1,
TestEnumC = 1 << 2,
TestEnumD = 1 << 3
} TestEnum;
複製代碼
對於該多選枚舉,如何判斷該枚舉類型的變量是不是複合項?
若是按照常規的思路,就須要逐項判斷是否包含,時間複雜度最差爲O(n)。而使用位運算能夠這麼寫:
TestEnum test = ...;
if (test == (test & (-test))) {
//不是複合項
}
複製代碼
實際上就是經過負數二進制的一個特性來判斷,看以下分析便一目瞭然:
test 0000 0100
反碼 1111 1011
補碼 1111 1100
test & (-test) 0000 0100
複製代碼
不明白有些工程師爲何排斥組合運算符,他們喜歡這麼寫:
bool is = ...;
if (is) a = 1;
else a = 2;
複製代碼
使用三目運算符:
bool is = ...;
a = is ? 1 : 2;
複製代碼
其餘組合運算符好比 ?:
%=
等,靈活的使用它們可讓代碼更加的簡潔清晰。
static
可讓變量進入靜態區,提升變量生命週期至程序結束。值得注意的是,文件中最外層(#include下)的變量自己就是在靜態區的,而這種狀況使用static
是爲了變量的私有化。
const 修飾的變量在常量區不可變,是在編譯階段處理;宏是在預編譯階段執行宏替換。因此頻繁使用 const 修飾的變量不會產生額外的內存,而全部使用宏的地方均可能開闢內存,何況,預編譯階段的大量宏替換會帶來必定的時間消耗。
因此筆者的建議是,能用常量的不用宏,好比一個網絡請求的 url:
.h 接口文件
extern NSString * const BaseServer;
.m 實現文件
NSString * const BaseServer = @"https://...";
複製代碼
值得注意的是,const 是修飾右邊內存,因此這裏是想要BaseServer
字符串指針指向的內容不可變,而不是*BaseServer
內容不可變。
在不少場景中,能夠犧牲必定的空間來下降時間複雜度,爲了程序的高效運行,工程師能夠自行判斷是否值得,下面舉一個代碼例子,判斷字符串是否有效:
BOOL notEmpty(NSString *str) {
if (!str) return NO;
static NSSet *emptySet;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
emptySet = [NSSet setWithObjects:@"", @"(null)", @"null", @"<null>", @"NULL", nil];
});
if ([emptySet containsObject:str]) return NO;
if ([str isKindOfClass:NSNull.class]) return NO;
return YES;
}
複製代碼
使用一個 hash 來提升匹配效率,這在數據較少時可能體現不出優點,甚至會讓效率變低,可是在數據量稍大的時候優點就明顯了,並且這樣寫能夠避免大量的if-elseif
等判斷,邏輯更清晰。
值得注意的是,此處使用static
來提高局部變量emptySet
的生命週期,而不是將這句代碼寫在方法體外面。在變量聲明時,必定要明確它的使用範圍,限定合適的做用域。
好比在 C++ 中,若不須要鍵值對的 hash ,就使用set
而不是map
;若不須要排序的集合就使用unordered_set
而不是set
。
歸根結底也是對時間複雜度的考慮,選擇容器類型時,必定要選擇「恰好」能知足需求的,能用更「簡單」效率更高的容器就不用「複雜」效率更低的容器。
對於變量的使用,儘可能在類或結構體初始化方法中對其賦初值,而不要依賴於編譯器。由於在可見的將來,無論是編譯器的更新或是代碼跨平臺移植,這些變量的初始值都不會受編譯器影響。
這是一個老生常談的東西了,多分支結構儘可能使用 switch 而不是大量的 if - else if 語句,若非要用 if - else if 來寫,則出現頻率高的分支優先判斷,能夠從總體上最大限度的減小判斷次數,從而下降 jump 指令的使用頻率。
不要小看這些少許的效率提高,放大到整個項目也是有不小的收益。
若想更優雅的處理分支結構,可使用策略模式,將多種分支狀況視做多種策略。
常常會有一些需求,對一系列的數據有不少額外的操做,好比選擇、刪除、篩選、搜索等。代碼設計時,要儘可能將全部的操做狀態都緩存到同一個數據模型中,而不是使用多個容器數據結構來處理,咱們應該儘可能避免數據同步防止出錯。
常常會看到這種代碼:
doSomething(city.school.class.jack.name,
city.school.class.jack.age,
city.school.class.jack.sex);
複製代碼
當同一個變量的調用過深且使用頻繁時,可使用一個局部指針來處理:
Person *jack = city.school.class.jack;
doSomething(jack.name,
jack.age,
jack.sex);
複製代碼
相對於指針變量所佔用的空間來講,代碼的簡潔和美觀度稍顯重要一點。
單例做爲一種設計模式應用很是普遍,在移動端開發中,有些開發者利用它來實現非緩存傳值,筆者認爲這是一個錯誤的作法,使用單例傳值的時候你須要管理單例中的數據什麼時候釋放與更新,可能會引起數據錯亂。
單例存在的意義應該是持久化數據,而非傳值,切勿爲了方便濫用單例。
繼承自己和解耦思想有些衝突,代碼設計中要儘可能避免過深的繼承關係,由於子類與父類的耦合將沒法真正剝離。過深的繼承關係會增長調試的困難程度,而且若繼承關係設計有缺陷,修改越深的類影響面將會越廣,可能帶來災難性的後果。
可使用分類的方式(裝飾模式)作一些通用配置,而後在具體類中簡潔的調用一次方法;也可使用 AOP 思想,hook 住生命週期方法無侵入配置(好比簡單埋點)。
好比 iOS 開發中,可能會有開發者喜歡寫一套基類,實際上只是基於系統的類作了小量的配置,好比BaseViewController
、BaseView
、BaseModel
、BaseViewModel
,甚至是BaseTableViewCell
。控制器基類能夠對棧和導航欄作一些配置,仍是有一點使用意義,至於其它的筆者感受就是過分設計,其實很大意義上BaseViewController
也沒有存在的必要。
記住:過多的基類並非代碼規範,那是你囚禁其餘開發者的牢籠。
提取方法應該遵照單一職責原則,但若功能自己就是不多的一兩句代碼可能就不必額外提取了。在保證代碼清晰的狀況下,不少時候提取邏輯也是須要酌情考慮的。
有見過開發者使用一套所謂的簡潔配置 UI 的框架,不過就是將 UI 控件的屬性封裝成鏈式語法之類的,用起來有種快一些的錯覺,卻不知這就是過分封裝的典範。
封裝的意義在於簡潔的解決一類問題,而非少敲那幾個字母,過分封裝只會增長其餘開發者閱讀你代碼的成本。
好比業界知名的 Masonry,使用它時比原生的 layout 快了不止 10 倍,並且代碼很簡潔易懂,極大的提升了開發效率。
當代碼中出現大量的 if - else 嵌套、閉包嵌套時,會讓代碼難以閱讀。出現這種狀況能夠從如下幾個方面處理:
寫某塊代碼中,要時刻注意空值和越界的處理,好比給NSDictionary
插入空值會崩潰,從NSArray
越界取值會崩潰,這些狀況要時刻考慮到。
固然,可能有人會說有方法能夠全局避免崩潰。實際上筆者不是很贊同這種作法,這可能會讓新手開發者永遠發現不了本身代碼的漏洞。
當你寫一塊代碼時,須要習慣性的思考兩個問題:這塊代碼的共有變量會被多線程訪問從而存在安全問題麼?這塊代碼可能會在一個 RunLoop 循環中調用很頻繁麼?
對於第一個問題,可能須要使用「鎖」來保證線程安全,而鎖的選擇有一些技巧,好比整形使用原子自增保證線程安全:OSAtomicIncrement32()
;調用耗時短的代碼使用dispatch_semaphore_t
更高效;可能存在重複獲取鎖時使用遞歸鎖處理......
對於第二個問題,只須要在合適的地方加入自動釋放池 (autoreleasepool) 避免內存峯值太高就好了。
對於大前端來講,界面是項目中重要的組成部分,而有時候設計師給的圖中,不一樣界面有不少相同的元素,看起來如出一轍,因此不少工程師偷懶直接複用界面了。
在這裏,筆者建議儘可能少的複用界面,寧願選擇複製一份。
試想,目前版本兩個界面相同,你複用了它,當下個版本其中一個界面要調整一下,這時你繼續偷懶,加入一些判斷來區分邏輯,下一次迭代又增長了差別,你又偷懶加入判斷邏輯...... 最終你會發現,這個界面裏面已經邏輯爆炸了,拆分紅兩個界面將變得異常困難。
而對於功能代碼,筆者是提倡多提取,多複用,切記命名規範和適當的註釋。
在封裝一些小組件時,必定要造成習慣,不想暴露給使用者的屬性和方法不要寫在接口文件中,甚至於某些延續父類的方法不想使用者使用,能夠以下處理:
- (instancetype)init UNAVAILABLE_ATTRIBUTE;
複製代碼
固然,不用擔憂組件內部如何獲取父類特性,能夠經過[super init]
來處理。
OC 開發中,可使用獨立的延展文件來作「知識隔離」,由於獨立的延展文件和當前類是一體的,會在編譯期決議。在須要的地方導入這個延展文件就能正常使用,而對未導入的文件進行「隔離」。
無論是任何技術棧的緩存機制設計,都須要一套緩存淘汰算法,使用最普遍的淘汰算法就是 LRU,便是最近最少使用淘汰算法,開發者須要嚴格的控制磁盤緩存和內存緩存的空間佔用。
在 iOS 開發中,可使用 YYCache 來處理緩存機制,該框架的源碼剖析可見筆者博客:YYCache 源碼剖析:一覽亮點
還有一點須要提出的是磁盤緩存的位置問題。iOS 設備沙盒中有 Documents、Caches、Preferences、tmp 等文件夾,其中 Documents 和 Preferences 會被 iCloud 同步。
Documents 適合存儲比較重要的數據;Caches 適合存儲大量且不那麼重要的數據,好比圖片緩存、網絡數據緩存啥的;tmp 存儲臨時文件,重啓手機或者內存告急時會被清理;Preferences 是偏好設置,適合存儲比較個性化的數據。
值得注意的是,NSUserDefaults
是存儲在 Preferences 下的文件,發現有不少開發者爲了偷懶頻繁的使用NSUserDefaults
作任意數據的磁盤緩存,這是一個很不合理的作法,用處不大且大量的數據通常緩存在 Caches 中,就算是從技術角度考慮,NSUserDefaults
是以 .plist 形式存儲的,不適合大數據存儲。
軟件工程師應該清楚本身編寫的代碼是運行在 32 位仍是 64 位的系統上,而且瞭解編程語言對於各類數字類型的定義。
在 iOS 領域,CGFloat
在 32 位系統中爲 float 單精度,64 位系統中爲 double 雙精度,當將一個NSNumber
轉換爲數字類型時,爲了兼容,須要以下寫:
NSNumber *number = ...;
CGFloat result = 0;
#if CGFLOAT_IS_DOUBLE
result = number.doubleValue;
#else
result = number.floatValue;
#endif
複製代碼
在使用不一樣數字類型時,須要考慮數字類型的表示範圍,好比能用short
處理的就不要用long int
。
同時,數字類型的精度問題每每困擾着新手開發者。無論是單精度 (float) 仍是雙精度 (double) 它們都是基於浮點計數實現的,包含了符號域、指數域、尾數域,而在計算機的理解裏數字就是二進制,因此浮點數基於二進制的科學計數法形如:1.0101 * 2^n ,這可不像十進制那樣方便的表示十進制小數,好比在十進制中使用 10^-1 輕鬆的表示十進制的 0.1 ,而二進制方式卻沒法實現(試想 2 的幾回方等於十進制的 0.1 ?),因此浮點數只能用最大限度的近似值表示這些沒法精確表示的小數。
好比寫一句代碼 float f = 0.1;
打一個斷點能夠看到它實際的值是:f = 0.100000001
。
和浮點計數相對的是定點計數,定點計數比較直觀,好比:10.0101 ,它的弊端就是對於有效位數過多的數字,須要大量的空間來存儲。因此爲了存儲空間的高效利用,使用最普遍的仍然是「不夠精確」的基於浮點計數的單精度和雙精度類型。
然而,在一些特定場景下,定點計數仍然能發揮它的優點,好比金錢計算。
對於金錢計算的處理,每每都是要求絕對準確的,因此在不少語言中都有基於定點計數的數據類型,好比 Java 中的 BigDecimal
、Objective-C 中的 NSDecimalNumber
,犧牲一些空間和時間來達到精確的計算。
使用裝飾模式時,一般狀況下不該該修改當前類的算法。
好比 OC 中的分類,它爲功能的添加提供了優雅的實現方式,可是開發者應該注意,不該該在分類裏面重寫當前類已經有了的方法。
由於在運行期,OC 分類中的方法是會自動插入類的方法列表,消息調用機制會找到最靠前的方法而忽略掉該類本有的方法,這可能會出現不少異常狀況且不易排查。
因此在使用裝飾模式時,要儘可能不作可能影響其餘業務的邏輯,好比 iOS 中「時髦」的 hook 技術,應該儘可能少用。
爲了防止在寫分類時一不當心重載了已有方法(多是其它分類的方法),應該爲分類方法都加上一個有辨識度的前綴,好比-()mj_ 、-()custom_
。
咱們應該儘可能避免用上帝視角去寫代碼。
常常會有一些需求,好比某段動畫能夠選擇是否執行,能夠以下處理:
void (^animationsBlock)(void) = ^{
...
};
void (^completionBlock)(BOOL) = ^(BOOL x){
...
};
if (duration <= 0) {
animationsBlock();
completionBlock(YES);
} else {
[UIView animateWithDuration:duration animations:animationsBlock completion:completionBlock];
}
複製代碼
建立兩個棧區的 Block,若須要動畫就傳入 -animateWithDuration:
系列方法,若不須要動畫 Block 就不用被拷貝到堆區,而是直接調用。這樣處理還有一個好處就是不用重複寫兩個 Block 中的業務邏輯了,避免格外的方法封裝。
代碼技巧都是實踐加思考總結出來的,在代碼編寫過程當中,開發者須要時刻明白本身的代碼是幹什麼的,不要隨意的複製代碼。同時,開發者須要有算法思惟和工程思惟,力求使用高效率和高可維護的代碼來實現業務。
筆者最後總結幾點提升代碼質量的途徑: