這篇文檔包含了一系列在 iOS 代碼中推薦的指導(dǎo)方針、編碼慣例和編寫代碼的最佳實(shí)踐,主要是為了提高代碼的可讀性。我們應(yīng)該知道,永遠(yuǎn)是讀代碼的時(shí)間比寫代碼的時(shí)間多,我們超過80%的時(shí)間都是在維護(hù)代碼。
有一點(diǎn)是需要注意的,編寫本篇規(guī)范不是為了說服所有人下面所寫的每一條都是編碼最好的方式,而是為了推薦大家一系列的準(zhǔn)則來保證我們的代碼風(fēng)格更加一致、可理解并且遠(yuǎn)離晦澀的取向。
一、編碼的格式及慣例
空格、空行
- 縮排使用4個(gè)空格(一般 tab 就是4個(gè)空格)
- 確保 Xcode 配置中
Automatically trim trailing whitespace和Including whitespace-only lines已經(jīng)勾選(譯者注:我覺得包括空行這項(xiàng)值得商榷)
方法聲明
我們應(yīng)該延續(xù) Xcode 中使用的方法聲明模板:-后面使用一個(gè)空格,除了參數(shù)之間不應(yīng)該有其他的地方存在空格。
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
...
}
流程控制語句(條件判斷/循環(huán)等等)
括號(hào)應(yīng)該同條件在同一行:
if (foobar) {
...
} else {
...
}
for (foo; foo; foo) {
...
}
單行流程控制語句依然應(yīng)該使用括號(hào)(不要省略)。
命名
這里需要大家看一下 Apple's Coding Guidelines 特別是方法命名指南這一塊,舉例:
使用 - (NSSize)cellSize 而不是- (NSSize)getCellSize。
指針聲明
對(duì)于指針來說,應(yīng)該將星號(hào)放置于變量名的前面,例如:NSString *foo。
使用字面量
盡可能使用 Objc 字面量來定義字典、數(shù)組和 number,例如:使用@42而不是[NSNumber numberWithInt:42]或者:
NSDictionary *dictionary = @{
@”k1” : @”v1”,
@”k2” : @”v2”,
};
而不是
[NSDictionary dictionaryWithObjectsAndKeys:@”v1”, @”k1”, @”v2”, @”k2”, nil]
只在用@property 聲明的屬性上使用點(diǎn)語法
只在聲明了的屬性或者結(jié)構(gòu)上使用點(diǎn)語法,不要這樣用:foobar.release或者foobar.count
原因是因?yàn)椋狐c(diǎn)語法是圍繞 getter/setter 調(diào)用的語法糖,只要用在屬性上才是符合慣例的。
ivar 實(shí)例命名使用下劃線 _ 前綴
ivar 實(shí)例命名應(yīng)該使用下劃線前綴,例如:NSString *_foobar
默認(rèn) property 生成的 ivar 就是以下劃線作為前綴,我們應(yīng)該保持風(fēng)格統(tǒng)一。
聲明屬性時(shí)限定語要明確
對(duì)于屬性的限定語是 nonatomic/atomic、strong/weak 要明確。
不要使用共有變量(public ivar)
外部調(diào)用應(yīng)該無法直接修改變量,如有需要,提供 getter/setter 方法給外部調(diào)用;
私有變量放到@implementation 代碼塊中
例如:
@implementation
{
NSString *_foobar;
}
對(duì)于#import 引用應(yīng)該進(jìn)行排序
對(duì)于引用的頭文件按照字符次序進(jìn)行排序,并且本地文件的引用放在前面(對(duì)于 .m 文件,對(duì)于自己的頭文件的引用放在第一位),全局文件的引用放在后面,例如對(duì)于 DBFoo.m:
#import "DBFoo.h"
#import "DBFoo+Protected.h"
#import "DBAnotherFile.h"
#import "DBSomeOtherFile.h"
#import <CoolFramework/Header.h>
#import <FoobarFramework/Foo.h>
使用#pragma mark 來根據(jù)功能標(biāo)記一組代碼
使用#pragma mark來標(biāo)記一組方法的邏輯含義,例如實(shí)現(xiàn)的一個(gè)特定協(xié)議的方法,或者對(duì)象的 setter/getter 方法。在一個(gè)區(qū)域的子區(qū)域中,可以使用#pragma mark -來創(chuàng)建 Xcode 的導(dǎo)航分隔標(biāo)記。例如,在一個(gè) tableViewController 中我們可以這樣使用:
#pragma mark - Lifecycle // for init and dealloc methods
#pragma mark - Appearance
#pragma mark view initializations
#pragma mark view utilities
#pragma mark - UIViewController overrides
#pragma mark - UITableViewDataSource
#pragma mark - UITableViewDelegate
為了使代碼易讀、易查找,方法應(yīng)該按照功能進(jìn)行劃分,而不是根據(jù)方法的可訪問范圍。例如一個(gè)私有的類方法可以放在兩個(gè)共有的實(shí)例方法之間。
審視 switch 代碼,避免包羅萬象的現(xiàn)象(catch-alls)
對(duì)于復(fù)雜的 switch-case 代碼塊,將每個(gè) case 代碼使用括號(hào)標(biāo)明代碼塊,例如:
switch (foo) {
case 0: {
...
}
break;
...
}
如果可以的話,盡量避免使用 default 代碼塊來代表“其他情況”,應(yīng)該使用default 代碼塊代表異常的情況。
為私有方法和分類方法添加前綴
例如:- (void)db_flushQueue
原因:這樣做可以很容易的識(shí)別出這個(gè)方法是否是一個(gè)私有方法,并也可以盡量減少子類無意中重寫父類私有方法的可能性。
如果私有方法不依賴實(shí)例狀態(tài)的話,將其改寫為類方法
如果一個(gè)私有方法沒有使用成員變量,將其改寫為類方法。
TODO/FIXME
使用姓名、時(shí)間來標(biāo)記 TODO 和 FIXME:
TODO:(rich) 2013-09-13 finish the style guide!
使用TODO:(<name>) <date>的格式來保證風(fēng)格一致、便于查找。
使用 FIXME 來標(biāo)記在 push 之前急需修改的內(nèi)容。
對(duì)于常量使用 static const
使用 static const 來代替#define 語句:
static const CGFloat kFooBar = 123.0;
原因:這樣編寫可以代表常量的作用域,是類型安全的,并且這樣編碼常量是通過變量符號(hào)裝載進(jìn)內(nèi)存,在調(diào)試模式下便于訪問。
使用 NSUIntegers/NSIntegers 代替 int(CGFloat 代替 float)
除非是特定的 API 庫指定類型為 int/float,我們應(yīng)該使用 NSUIntegers/NSIntegers/CGFloat。
原因:第一,大多數(shù) cocoa 庫偏向使用 NSUIntegers/NSIntegers/CGFloat,我們?cè)谑褂?cocoa 的時(shí)候使用NSUIntegers/NSIntegers/CGFloat 更加便捷;第二,int 類型占用的位數(shù)會(huì)根據(jù)系統(tǒng)架構(gòu)不同而變化,使用 NSUIntegers/NSIntegers/CGFloat 更加可靠。
枚舉聲明
就像2012年 WWDC 中介紹的那樣,我們應(yīng)該使用NS_ENUM來聲明枚舉:
typedef NS_ENUM(NSInteger, UITableViewCellStyle) {
UITableViewCellStyleDefault,
UITableViewCellStyleValue1,
UITableViewCellStyleValue2,
UITableViewCellStyleSubtitle
};
原因:
NS_ENUM有更好的類型校驗(yàn)和更好的代碼補(bǔ)全。
位移常量
與NS_ENUM 相似,在 iOS6 以后使用NS_OPTIONS:
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
UIViewAutoresizingNone = 0,
UIViewAutoresizingFlexibleLeftMargin = 1 << 0,
UIViewAutoresizingFlexibleWidth = 1 << 1,
UIViewAutoresizingFlexibleRightMargin = 1 << 2,
UIViewAutoresizingFlexibleTopMargin = 1 << 3,
UIViewAutoresizingFlexibleHeight = 1 << 4,
UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};
類名前面添加前綴
例如:自定義的視圖控制器命名為:DBFoobarViewController。
原因:OC 不支持命名空間。
對(duì)比使用下標(biāo),偏向使用 enumerator 進(jìn)行遍歷
使用enumerateObjectsUsingBlock或者for-in 遍歷代替for-index 遍歷。
二、最佳實(shí)踐,遠(yuǎn)離一些坑
使用 NSCAssert 進(jìn)行斷言
我們應(yīng)該使用NSCAssert而不是assert 或者NSAssert 來進(jìn)行斷言。
原因:
NSAssert是一個(gè)宏,實(shí)際上,它內(nèi)部對(duì)self有一個(gè)引用,也就是說,如果在 block 中使用的斷言的話,可能會(huì)在不注意的情況下對(duì)self產(chǎn)生一個(gè)引用,我們可以在無所謂引不引用self的地方使用NSAssert。但是因?yàn)?code>NSAssert 和NSCAssert作用大致相同(都包含文件名、方法名和行數(shù),但是NSCAssert不打印self的實(shí)際類型),我們可以直接簡(jiǎn)單的使用NSCAssert。
存儲(chǔ) block 的時(shí)候使用 copy
當(dāng)在一個(gè)方法的外部你希望引用一個(gè) block 變量的時(shí)候(例如通過把它存儲(chǔ)為一個(gè) ivar),應(yīng)該對(duì)其進(jìn)行 copy。
原因:block 開始被創(chuàng)建在棧內(nèi)存上,如果在超出作用域?qū)?block 進(jìn)行引用,應(yīng)該將其 copy 到堆上。
對(duì) NSString 使用 copy
原因:對(duì)于外部訪問者傳入的 NSString 使用 copy 是為了防止傳入 NSMutableString。
永遠(yuǎn)不要將整型數(shù)據(jù)強(qiáng)制轉(zhuǎn)換為 BOOL 類型
例如常見的將一個(gè)數(shù)組的長(zhǎng)度賦給一個(gè) BOOL 類型的變量。
原因:即使整型值大于0,當(dāng)其二進(jìn)制后8位全都是0的時(shí)候,BOOL 值依然為0。
對(duì)于私有變量不要使用@property 聲明屬性
在.m文件中不要使用@property,使用 ivar 成員變量就好。
有一種例外情況:少有的需要使用atomic 屬性的時(shí)候,不需遵循以上原則。
在 dealloc 方法中,清除指向?qū)ο蟮?delegate 或者 observer
如果一個(gè)對(duì)象被設(shè)置為另一些對(duì)象的代理或者觀察者(包括通知對(duì)象),在dealloc 方法中應(yīng)該將其設(shè)置為nil。
注意:即使在ARC 環(huán)境下,delegate 屬性被設(shè)置為 weak,delegate 對(duì)象的指針被設(shè)置為 nil 依然很重要,原因是因?yàn)槔锩嬗泻芏嗟那闆r依然是按照 assign 的規(guī)則編譯,如果出現(xiàn)這種情況,再訪問的時(shí)候就會(huì)出現(xiàn)崩潰。
對(duì)局部變量進(jìn)行初始化
ivar 會(huì)被自動(dòng)的初始化為 nil 或者0,所以你不需要顯式的將其聲明為 nil,但是對(duì)于局部變量則不然,你需要手動(dòng)的將其初始化。
視圖控制器中,在viewDidLoad之前不要碰 view
調(diào)用self.view會(huì)導(dǎo)致 view 的加載,通常在viewDidLoad之前并不需要這么做。
相比 target-selector 代理模式更偏向使用 block 來完成回調(diào)
對(duì)于簡(jiǎn)單的回調(diào),我們應(yīng)該傾向使用 block 來代替 target-selector代理模式來完成回調(diào)。
原因:這樣做能夠降低代碼的分離、碎片性,比如想想對(duì)于一個(gè)簡(jiǎn)單的回調(diào),相比定義代理協(xié)議、遵循協(xié)議、實(shí)現(xiàn)協(xié)議,如下使用 block 會(huì)顯得更加清晰:
[op startWIthCompletionHandler:^() {
// ... stuff to be done after the op finishes ...
}];
對(duì)于支持多線程操作的類、方法、變量應(yīng)該添加更加詳細(xì)的注釋文檔
通常情況下我們會(huì)假定所有的方法僅僅只會(huì)在主線程上執(zhí)行,如果一個(gè)方法是預(yù)期在后臺(tái)線程上執(zhí)行的話(例如:某些蘋果的通知,像ALAssetsLibraryChangedNotification),請(qǐng)確保添加相對(duì)應(yīng)的注釋并且如果可能的話添加斷言(例如:DBAssertMainThread)。
同樣的,我們通常會(huì)假定所有的實(shí)例變量只會(huì)從主線程訪問,如果某個(gè)變量需要在線程間共享使用(或者僅僅只在一個(gè)特定的線程、隊(duì)列中使用),請(qǐng)對(duì)變量確保添加了相應(yīng)的注釋。
對(duì)于類來說,如果是被設(shè)計(jì)成含有線程安全的接口,需要添加對(duì)應(yīng)的注釋來說明其工作的情況。
如無必要,避免使用原子屬性
默認(rèn)情況下,所有的屬性都是原子屬性的,需要你顯式的將其標(biāo)明為非原子屬性。
原因:設(shè)置原子屬性會(huì)對(duì) setter/getter 方法加鎖,這樣會(huì)嚴(yán)重影響性能。
使用 GCD 來代替performSelectorInBackground
通常來說,永遠(yuǎn)也不要使用performSelectorInBackground。
原因:使用 GCD 會(huì)讓后臺(tái)線程執(zhí)行的代碼與執(zhí)行代碼放到一起更加有利于理解和閱讀。
避免頭文件中不必要的引用
如果可以的話,使用前向聲明來代替頭文件的引用,例如:@class DBFoo。
原因:減少編譯時(shí)間和不必要的依賴。
對(duì)指定構(gòu)造方法進(jìn)行文檔注釋
對(duì)于指定的構(gòu)造方法要標(biāo)注清楚,只有這樣,子類對(duì)構(gòu)造方法進(jìn)行重寫的時(shí)候才知道重寫哪些構(gòu)造方法能夠確保子類的構(gòu)造方法被調(diào)用。
子類應(yīng)該對(duì)父類的指定構(gòu)造方法進(jìn)行重寫
如果方法調(diào)用方不應(yīng)該再調(diào)用這個(gè)構(gòu)造方法時(shí),應(yīng)該使用拋出斷言進(jìn)行警告。
通知中心的觀察者方法應(yīng)該帶有NSNotification參數(shù)
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(db_updateStuffWithNotification:)
name:@"SomeNotificationName"
object:theSender];
原因:即使并不需要參數(shù)的情況下(例如:觀察者的方法不需要參數(shù)),這樣做可以使代碼閱讀者更加明顯的發(fā)現(xiàn)這個(gè)方法是被通知中心所調(diào)用。
Block 及循環(huán)引用
Block 中訪問self或者使用變量間接引用了self的情況下,Block 會(huì)隱式的創(chuàng)建self的強(qiáng)引用。
換句話說,在使用 Block 的時(shí)候可能會(huì)引起循環(huán)引用。一個(gè)通常的解決方案是:舉個(gè)例子來說,比如你使用了一個(gè)BlockAlertView在回調(diào)用的 block 里面引用了self,并且self又強(qiáng)引用了這個(gè)BlockAlertView,這樣就會(huì)引起一個(gè)self -> alert view -> block -> self的引用循環(huán),為了避免這種情況的發(fā)生,我們可以使用一種通常被稱為strong-weak dance的方法:
__weak DBFoo *weakSelf = self;
[_someRandomViewIOwn setObserverBlock:^{
DBFoo *strongSelf = weakSelf;
// NOTE: strongSelf may point to nil if weakSelf is already nil'ed out prior to the block being called!
[strongSelf doSomeStuff];
}];
原因:通過使用
weakSelf,block 不再強(qiáng)持有self,但是還是有可能出現(xiàn)在 block 調(diào)用之前weakSelf變成nil 的情況,更糟的是,如果 block 是在多個(gè)線程中被調(diào)用的,weakSelf還有可能是在 block 調(diào)用過程中被置為 nil。
為了避免在 block 執(zhí)行過程中出現(xiàn)self突然消失的情況,我們?nèi)藶榈慕oweakSelf添加一個(gè)強(qiáng)引用strongSelf,雖然strongSelf仍然有可能為 nil,但是起碼在 block 執(zhí)行過程中會(huì)保持始終如一,不必再擔(dān)心會(huì)隨意的置為 nil。
但是,你也不應(yīng)該濫用這種處理方式,在絕大多是情況下,你想使用延遲執(zhí)行的 block,(例如:動(dòng)畫、dispatch_after),不應(yīng)該使用這種方式。