Objective-C,通常寫作ObjC或OC和較少用的Objective C或Obj-C,是擴充C的面向對象編程語言。它主要使用于Mac OS X和GNUstep這兩個使用OpenStep標準的系統(tǒng),而在NeXTSTEP和OpenStep中它更是基本語言。
GCC與Clang含Objective-C的編譯器,Objective-C可以在GCC以及Clang運作的系統(tǒng)上編譯。
1980年代初布萊德·考克斯(Brad Cox)在其公司Stepstone發(fā)明Objective-C。他對軟件設計和編程里的真實可用度問題十分關心。Objective-C最主要的描述是他1986年出版的書 Object Oriented Programming: An Evolutionary Approach. Addison Wesley. ISBN 0-201-54834-8.
根據約束力強弱,規(guī)約依次分為強制、推薦、參考三大類:
- 【強制】:必須遵守,違反本約定或將會引起嚴重的后果;
- 【推薦】:盡量遵守,長期遵守有助于系統(tǒng)穩(wěn)定性和合作效率的提升;
- 【參考】:充分理解,技術意識的引導,是個人學習、團隊溝通、項目合作的方向。
一. 語言規(guī)約
1.1 命名規(guī)約
1.1.1 【強制】命名約定通用準則:清晰、一致性、不能自我指涉。
- 清晰:命名應該既清晰又剪短,但拒絕為了最求剪短而喪失清晰性,拒絕為了簡潔進行隨意縮寫。
正例:
insertObject:atIndex: 好的命名
removeObjectAtIndex: 好的命名
removeObject: 好的命名
destinationSelection 好的命名
setBackgroundColor 好的命名
反例:
insert:at: 不清晰,插入什么?“at”表示什么?
remove: 不清晰,需要移除什么?
destSel 不清晰,縮寫含義不清
setBkgdColor: 縮寫含義不清
- 一致性:命名含義應該具有前后,全局的一致性,同個功能也應該使用同個名稱。
-(NSInteger)tag 該方法同時定義在 NSView、NSCell、NSControl 這三個類里面
- 不能自我指涉:除掩碼常量、通知常量外,名稱不應該自我指涉(self-reference)。
(通俗的講,自我指涉是指在變量末尾增加了自己類型的一個后綴)
正例:
NSString 規(guī)范的寫法
NSUnderlineByWordMask 掩碼常量,可以使用 Mask 自我指涉
NSTableViewColumnDidMoveNotification 通知常量,可以使用 Notification 自我指涉
反例:
NSStringObject NSString 本身已經是 Object 了,不需要再在名字里顯示指出
1.1.2 【強制】杜絕一切縮寫,除以下已經長期使用形成共識的內容可以使用縮寫。
alloc Allocate
alt Alternate
app Application
calc Calculate
dealloc Deallocate
func Function
horiz Horizontal
info Information
init Initialize
int Integer
max Maximum
min Minimum
msg Message
nib Interface Builder archive
pboard Pasteboard
rect Rectangle
Rep Repressentation
temp Temporary
vert Vertical
1.1.3 【強制】文件名、自定義類、Protocol 禁止以系統(tǒng)已有前綴開頭。
AC,AB,AS,AL,AU,AV,CX,CF,CK,CN,CA,CB,NS,CF,CG,CI,CL,CM,MIDI,CM,CS,CT,CV,EK,EA,GC,GK,HK,HM,AD,IN,GS,LA,MK,MA,MP,MT,MS,MF,MC,NE,NK,NC,AL,PK,PH,CA,QL,RP,SF,SL,SF,Sk,SC,TW,UI,UN,VS,VT,WC,WK
- 一般的,自定義類名前以三位大寫字母表示前綴,例如CYYPerson
1.1.4【參考】文件名、自定義類、Protocol 、常量、枚舉等全局可見內容需要添加三個大寫字符作為前綴,雙字幕前綴為 Apple 的類預留。盡管這個規(guī)范看起來有些古怪,但是這樣做可以減少 Objective-C 沒有命名空間所帶來的問題。
1.1.5 【強制】方法名、參數名、成員變量、局部變量都采用小寫字符開頭,名稱中的單詞首字符要大寫的小駝峰形式。另外,請不要在方法名稱中使用前綴(category 方法除外:category 方法強制使用模塊前綴)。
正例:
normal:
- (BOOL)fileExistsAtPath:(NSString *)path;
category:
- (NSString *)cyy_urlDecode;
1.1.7 【強制】不要使用“do”或者“does”作為名稱的一部分,因為這些輔助性的動詞不能為名稱增加更多的含義。同時,不要在動詞之前使用副詞或者形容詞。
1.1.8 【強制】如果方法返回接收者的某個屬性,則以屬性名稱作為方法名。如果方法沒有間接地返回一個或多個值,不要使用“get”這樣的單詞。
正例:
- (NSSize)cellSize;
反例:
- (NSSize)calcCellSize;
- (NSSize)getCellSize;
1.1.9 【強制】所有參數前面都應使用關鍵字。
正例:
- (void)sendAction:(SEL)aSelector to:(id)anObject forAllCells:(BOOL)flag;
反例:
- (void)sendAction:(SEL)aSelector :(id)anObject :(BOOL)flag;
1.1.10 【強制】如果當前創(chuàng)建的方法比起它所繼承的方法更有針對性,則應該在已有的方法名稱后面添加關鍵字,并將其作為新方法的名稱。
父類:
- (instancetype)initWithFrame:(NSRect)frameRect;
子類:
- (instancetype)initWithFrame:(NSRect)frameRect mode:(int)aMode cellClass:(Class)factoryId numberOfRows:(int)rowsHigh numberOfColumns:(int)colsWide;
1.1.11 【推薦】請不要使用”and“來連接兩個表示接受者屬性的關鍵字。
- 雖然下面的例子使用”and“這個詞感覺還不錯,但是隨著創(chuàng)建的方法所帶有的關鍵字越來越多,這種方式會引起問題
正例:
- (int)runModalForDirectory:(NSString *)path file:(NSString *)name types:(NSArray *)fileTypes;
反例:
- (int)runModalForDirectory:(NSString *)path andFile:(NSString*)name andTypes:(NSArray *)fileTypes;
1.1.12 【推薦】如果方法描述了兩個獨立的動作,請使用”and“把它們連接起來
- (BOOL)openFile:(NSString *)fullPath withApplication:(NSString *)appName andDeactivate:(BOOL)flag;
1.1.13 【強制】您可以使用情態(tài)動詞(在動詞前冠以"can","should","will"等),使得方法的名稱更加明確,但是請不要使用"do"或"does"這樣的情態(tài)動詞。
正例:
- (void)setCanHide:(BOOL)flag;
- (BOOL)canHide;
- (void)setShouldCloseDocument:(BOOL)flag;
- (BOOL)shouldCloseDocument;
反例:
- (void)setDoesAcceptGlyphInfo:(BOOL)flag;
- (BOOL)doesAcceptGlyphInfo;
1.1.14【強制】只有當方法間接地返回對象或者數值,您才需要在方法名稱中使用"get"。這種格式只適用于需要返回多個數據項的方法。像這種接收多個參數的方法應該能夠傳入nil,因為調用者未必對每個參數都感興趣。
- (void)getLineDash:(float *)pattern count:(int *)count phase:(float *)phase;
1.1.15【強制】下面是數條和方法參數命名相關的通用規(guī)則:
- 和方法名稱一樣, 參數的名稱也是以小寫的字符開頭,并且后續(xù)單詞的首字符要大寫。 例如:removeObject:(id)anObject。
- 請不要在參數名稱中使用"pointer"或者"ptr"。您應該使用參數的類型來聲明參數是否是 一個指針。
- 請不要使用一到兩個字符的名稱作為參數名。
- 請不要使用只剩幾個字符的縮寫。
1.1.16【強制】方法名稱的開頭應標識出發(fā)送消息的對象所屬的類:
- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(int)row;
- (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename;
1.1.17【強制】如果調用某個方法是為了通知委托某個事件已經發(fā)生或者即將發(fā)生, 則請在方法名稱 中使用“did”或者“will”這樣的助動詞。
- (void)browserDidScroll:(NSBrowser *)sender;
- (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)window;
1.1.18【強制】【強制】如果調用某個方法是為了要求委托代表其他對象執(zhí)行某件事,當然,您也可以在方法名 稱中使用“did”或者“will”,但我們傾向于使用“should”。
- (BOOL)windowShouldClose:(id)sender;
1.2 常量定義
1.2.1 【強制】請使用NS_ENUM枚舉類型來表示一群相互關聯(lián)的整數值常量。枚舉項以枚舉類型為前綴。
typedef NS_ENUM(NSInteger, NSMatrixMode) {
NSMatrixModeRadio = 0,
NSMatrixModeHighlight = 1,
NSMatrixModeList = 2,
NSMatrixModeTrack = 3
};
1.2.2 【強制】請使用NS_OPTIONS定義一組相互關聯(lián)的位移枚舉常量。位移枚舉常量是可以組合使用的。枚舉項以枚舉類型為前綴。
typedef NS_OPTIONS(NSUInteger, NSMatrixModeMask) {
NSMatrixModeMaskBorderless = 0,
NSMatrixModeMaskTitled = 1 << 0,
NSMatrixModeMaskClosable = 1 << 1,
NSMatrixModeMaskMiniaturizable = 1 << 2,
NSMatrixModeMaskResizable = 1 << 3
};
1.2.3 【強制】請使用 const 來創(chuàng)建浮點值常量。如果某個整數值常量和其他的常量不相關,您也可以使用 const 來創(chuàng)建,否則,則應使用枚舉類型。下面的聲明展示了 const 常量的格式:
const float NSLightGray;
1.2.4 【強制】通常情況下,請不要使用#define 預處器理命令創(chuàng)建常量。對于整數值常量,請使用枚舉類型創(chuàng)建,而對于浮點值常量,請使用const 修飾符創(chuàng)建。
1.2.5 【強制】有些符號,預處理器需要對其進行計算,以便決定是否要對某一代碼塊進行處理,則它們應該使用大寫字符表示。
#ifdef DEBUG
1.2.6 【強制】推薦使用常量來代替字符串字面值和數字。常量應該用 static 聲明為靜態(tài)常量,而不要用 #define,除非它明確的作為一個宏來使用。這樣能夠方便復用,而且可以快速修改而不需要查找和替換
正例:
static NSString * const CYYCacheControllerDidClearCacheNotification = @"CYYCacheControllerDidClearCacheNotification";
static const CGFloat CYYImageThumbnailHeight = 50.0f;
反例:
#define CompanyName @"Apple Inc."
#define magicNumber 42
- 常量應該在頭文件中以這樣的形式暴露給外部:
extern NSString *const CYYCacheControllerDidClearCacheNotification;
并在實現文件中為它賦值。
1.2.7 【強制】異常使用全局的 NSString 對象來標識,其名稱按如下的方式進行組合:異常名稱中的具有唯一性的那部分,其組成詞應該拼寫在一起, 并且每個單詞的首字符要大寫。
[Prefix] + [UniquePartOfName] + Exception
例如:
NSColorListIOException
NSColorListNotEditableException
NSDraggingException
NSFontUnavailableException
NSIllegalSelectorException
1.2.8 【強制】Notification消息使用全局的 NSString 對象進行標識,其名稱按如下的方式組合:
[Name of associated class] + [Did | Will] + [UniquePartOfName] + Notification
例如:
NSApplicationDidBecomeActiveNotification
NSWindowDidMiniaturizeNotification
NSTextViewDidChangeSelectionNotification
NSColorPanelColorDidChangeNotification
1.3 類定義規(guī)約
1.3.1 【強制】要盡可能地使用屬性定義代替無修飾的實例變量。
正例:
@interface Item : NSObject
@property (nonatomic, copy) NSString* name;
@end
反例:
@interface Item : NSObject {
NSString* _name
}
@end
1.3.2 【推薦】如果需要自定義property的getter或setter方法時,請在聲明property時一起聲明掉。
@property (nonatomic, copy, getter=my_name, setter=my_setName:) NSString *name;
1.3.3 【推薦】對外暴露的屬性,盡量定義為readonly。
1.3.4 【推薦】不建議使用@dynamic修飾屬性,除非你真的知道自己在干什么。
1.3.5 【強制】在為某個類添加實例變量時,請記住下面幾個因素:
- 只暴露必須的對外接口或屬性在.h中,其它private保留在.m里。
- 請確保實例變量的名稱能夠扼要地描述它所保存的屬性。
1.3.6 【強制】除非在沒有其他解決方案的情況下,否則不復寫任何 + (void) load方法。所有的load方法的執(zhí)行在Class的裝載階段,會延長App的啟動時間.且如果存在穩(wěn)定性問題,也沒有可以修復的時機。
1.3.7 【強制】+(void)initialize必須判斷class類型或使用dispatch_once防止執(zhí)行多次.由于任何繼承類也會執(zhí)行父類的initilize,所以這里一定要做類型判斷,或使用dispatch_once來保障不會執(zhí)行多次
if (self == [NSFoo class]) {
// the initializing code
}
1.3.8 【強制】不應該顯式地調用 initialize方法。如果需要觸發(fā)初始化行為,則請調用一些無害的方法。
正例:
[NSImage self];
反例:
[NSImage initialize];
1.3.9 【強制】在property的getter方法里不能再顯式或者隱式的調用同一個property的getter方法,會導致死循環(huán)。
反例
@interface ALPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation ALPerson
- (void)setName:(NSString *)name {
self.name = name;//死循環(huán)!
}
@end
1.3.10 【推薦】方法調用,方法調用應盡量保持與方法聲明的格式一致。當格式的風格有多種選擇時,新的代碼要與已有代碼保持一致。
- 調用時所有參數應該在同一行:
[myObject doSomethingWith:arg1 name:arg2 error:arg3];
- 或者每行一個參數,以冒號對齊:
[myObject doSomethingWith:arg1
name:arg2
error:arg3];
- 方法定義與方法聲明一樣,當關鍵字的長度不足以以冒號對齊時,下一行都要以四個空格進行縮進。
[myObj short:arg1
longKeyword:arg2
evenLongerKeyword:arg3];
1.3.11 【推薦】使用nonnull、nullable、__kindof來修飾方入參數、返回值、屬性
@property (nonatomic, strong, nonnull) Sark *sark;
@property (nonatomic, copy, readonly, nullable) NSArray *friends;
+ (nullable NSString *)friendWithName:(nonnull NSString *)name;
與Swift混編的時候,Swift的變量會有,? 與!修飾的變量,前者為變量可以為nil,后者為變量不會為nil。
為了橋接此變量,OC中提供,nullable與nonnull關鍵字修飾,作為對應的橋接。
例:
// OC
- (nullable NSString*)name {
}
- (NSString* __nullable)name {
}
- (NSString* _Nullable)name {
}
// MARK: -
- (int _Nonnull)age {
}
// 橋接Swift
func name() -> String? {
}
func age() -> Int {
}
1.3.12 【強制】禁止從designated initializer 里面調用一個 secondary initializer。如果這樣,調用很可能會調用一個子類重寫的init方法并且陷入無限遞歸之中。
- Objective-C 有指定初始化方法(designated initializer)和間接(secondary initializer)初始化方法的觀念。designated 初始化方法是提供所有的參數,secondary 初始化方法是一個或多個,并且提供一個或者更多的默認參數來調用designated 初始化的初始化方法。
- 一個類應該有且只有一個designated初始化方法,其他的初始化方法應該調用這個designated的初始化方法。
- 在希望提供你自己的初始化函數的時候,應該遵守這三個步驟來保證獲得正確的行為:
(1)定義你的designated initializer,確保調用了直接超類的 designated initializer。
(2)重載直接超類的 designated initializer。調用你的新的 designated initializer。
(3)為新的 designated initializer 寫文檔。可以用編譯器的指令 attribute((objc_designated_initializer))來標記。用編譯器指令attribute((unavailable(Invoke the new designated initializer))讓父類的 designated initializer 失效.
正例:
@interface ZOCNewsViewController : UIViewController
- (instancetype)initWithNews:(ZOCNews *)news __attribute__((objc_designated_initializer));
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil __attribute__((unavailable("Invoke the designated initializer,call initWithNews:")));
- (instancetype)init __attribute__((unavailable("Invoke the designated initializer,call initWithNews:"));
@end
@implementation ZOCNewsViewController
- (id)initWithNews:(ZOCNews *)news
{
//調用直接父類的 designated initializer
self = [super initWithNibName:nil bundle:nil];
if (self) {
_news = news;
}
return self;
}
// 重載直接父類的 designated initializer
// 如果你沒重載 initWithNibName:bundle: ,而且調用者決定用這個方法初始化你的類(這是完全合法的)。 initWithNews: 永遠不會被調用,所以導致了不正確的初始化流程,你的類的特定初始化邏輯沒有被執(zhí)行。
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
// call the new designated initializer
return [self initWithNews:nil];
}
@end
反例:
@implementation ParentObject
//designated initializer
- (instancetype)initWithURL:(NSString*)url title:(NSString*)title {
if (self = [super init]) {
_url = [url copy];
_title = [title copy];
}
return self;
}
//secondary initializer
- (instancetype)initWithURL:(NSString*)url {
return [self initWithURL:url title:nil];
}
@end
@interface ChildObject : ParentObject
@end
@implementation ChildObject
//designated initializer
- (instancetype)initWithURL:(NSString*)url title:(NSString*)title {
//在designated intializer中調用 secondary initializer,錯誤的
if (self = [super initWithURL:url]) {
}
return self;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 這里會死循環(huán)
ChildObject* child = [[ChildObject alloc] initWithURL:@"url" title:@"title"];
}
@end
1.4 注釋規(guī)約
1.4.1 【強制】頭文件中的暴露的方法或者屬性都必須添加注釋
- 注釋建議使用Xcode自帶工具插入默認格式。option+command+/即可自動插入。
1.4.2 【強制】自動生成的代碼注釋中的placeholder要替換掉
1.4.3 【推薦】建議對于復雜難懂邏輯添加注釋
1.5 代碼組織規(guī)約
1.5.1 【推薦】當一個類功能很多時,建議使用Category的方式進行功能劃分,這些Category可以放在同一個文件中。
示例:
@interface UIViewController (UIViewControllerRotation)
+ (void)attemptRotationToDeviceOrientation NS_AVAILABLE_IOS(5_0) __TVOS_PROHIBITED;
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation NS_DEPRECATED_IOS(2_0, 6_0) __TVOS_PROHIBITED;
@end
@interface UIViewController (UILayoutSupport)
@property(nonatomic,readonly,strong) id<UILayoutSupport> topLayoutGuide NS_AVAILABLE_IOS(7_0);
@property(nonatomic,readonly,strong) id<UILayoutSupport> bottomLayoutGuide NS_AVAILABLE_IOS(7_0);
@end
@interface UIViewController (UIKeyCommand)
- (void)addKeyCommand:(UIKeyCommand *)keyCommand NS_AVAILABLE_IOS(9_0);
- (void)removeKeyCommand:(UIKeyCommand *)keyCommand NS_AVAILABLE_IOS(9_0);
@end
1.5.2 【推薦】建議使用#pragma marks -來進行方法分組,提高可讀性,具體樣例如下,建議把生命周期,事件,property方法以及protocol方法進行區(qū)分。
示例:
#pragma mark - Lifecycle
- (instancetype)init {}
- (void)dealloc {}
- (void)viewDidLoad {}
- (void)viewWillAppear:(BOOL)animated {}
- (void)didReceiveMemoryWarning {}
#pragma mark - Custom Accessors
- (void)setCustomProperty:(id)value {}
- (id)customProperty {}
#pragma mark - IBActions
- (IBAction)submitData:(id)sender {}
#pragma mark - Public
- (void)publicMethod {}
#pragma mark - Private
- (void)privateMethod {}
#pragma mark - Protocol conformance
#pragma mark - UITextFieldDelegate
#pragma mark - UITableViewDataSource
#pragma mark - UITableViewDelegate
#pragma mark - NSCopying
- (id)copyWithZone:(NSZone *)zone {}
#pragma mark - NSObject
- (NSString *)description {}
1.5.3 【推薦】建議合理使用group或folder來組織工程結構,而不是全部放在source里,物理group與工程中group要對應
1.5.4 【推薦】過期方法,不要直接刪除,先標記為depcrated。
1.5.5 【推薦】建議類繼承關系不要超過2層,并且抽取公共邏輯到父類,盡量避免父類,子類方法調用跳躍
1.5.6 【參考】盡量減少繼承,可以考慮組合,category,protocol等方式
1.5.7 【推薦】每個文件.m的方法數目不應該超過20個,每個方法的行數不應該超過200行。
- 每個方法應該只做一件事情。當函數過長時,它做的事情通常會不明確,后續(xù)會很難理解與維護。
1.5.8 【強制】函數內嵌套不能太深,一個函數內大括號里嵌套大括號不能超過三層。
- 超過三層已經很難理解一個函數的作用,可以將其中的一些邏輯抽離成一個單獨的函數。
1.5.9 【推薦】建議業(yè)務bundle使用統(tǒng)一的前綴來標識
1.5.10 【推薦】頭文件中只暴露出需要給他人調用的類、方法及屬性,私有類、方法、變量放在.m中
1.5.11 【強制】Release包必須關閉非離線日志(NSLog、print)
1.5.12 【強制】必須清理工程中的所有warning
1.5.13 【推薦】長條件判斷建議使用bool變量來代替
- 太長不容易調試,且不直觀。
正例:
BOOL isConditionSatisfied = (1 == a.x && 3==b.y && 2 == c.x);
if (isConditionSatisfied){
doSomething()
}
反例:
if (a.x = 1 && b.y =3 && c.x = 2){
doSomething()
}
1.5.15 【推薦】條件判斷,推薦加大括號,即使一行,容易導致的錯誤為,當 if 語句里面的一行被注釋掉,下一行就會在不經意間成為了這個 if 語句的一部分。
正例:
if (!error) {
return success;
}
反例:
if (!error)
return success;
//或
if (!error) return success;
1.5.16 【推薦】對三目運算使用時,要注意簡化,x=a?a:b只要寫成x=a?:b 即可;
1.5.17 【推薦】編寫switch語句的時候, 一定要實現default:,防止外部異常調用,內部沒有處理的情況,
1.5.18 【強制】switch里每個case里需要強制有break;
1.5.19 【強制】switch里每個case里都要使用{}所有代碼括起來,就算只有一行。
二. 最佳實踐
2.1.1 【強制】自建線程必須命名。
2.1.2 【強制】多線程訪問同一個對象時,必須注意臨界區(qū)的保護
2.1.3 【強制】單例創(chuàng)建要使用線程安全模式,并且禁止在單例的init方法中使用dispatch_sync來阻塞線程,極易出現死鎖
正例:
+ (instancetype)sharedInstance {
static id sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
2.1.4 【強制】在多線程環(huán)境下使用懶加載方式加載變量,會有crash風險,必須加鎖保護
正例:
//多線程環(huán)境下調用
- (NSCache *)contactCache
{
if (!_contactCache) {
@synchronized(self) {
if (!_contactCache) {
_contactCache = [[NSCache alloc] init];
_contactCache.name = @"contactCache";
}
}
}
return _contactCache;
}
2.1.5 【強制】performSelector:withObject:afterDelay:要在有Runloop的線程里調用,否則調用無法生效。
- 說明:異步線程默認是沒有runloop的,除非手動創(chuàng)建;而主線程是系統(tǒng)會自動創(chuàng)建Runloop的。所以在異步線程調用是請先確保該線程是有Runloop的。
2.1.6 【強制】禁止隨意創(chuàng)建長駐線程,除非是在整個app運行周期內都必須存在且有任務運行的。
2.1.7 【推薦】NSNotificationCenter在iOS 8及更老系統(tǒng)上存在多線程bug,selector執(zhí)行到一半時可能會因為self銷毀而觸發(fā)crash,解決方案是在selector里開始的地方引入下面的宏:
- (void)onMultiThreadNotificationTrigged:(NSNotification *)notify {
__weak typeof(self) weakSelf = self;
__strong typeof(self) strongSelf = weakSelf;
if (! weakSelf) {
return;
}
[strongSelf doSomething];
}
2.1.8 【推薦】在多線程應用中,Notification在哪個線程中post,就在哪個線程中被轉發(fā),而不一定是在注冊觀察者的那個線程中。如果發(fā)送消息的不在主線程,而接受消息的回調里做了UI操作,需要讓其在主線程執(zhí)行。
2.1.9 【推薦】僅當必須保證順序執(zhí)行時才使用dispatch_sync,否則容易出現死鎖,應避免使用,可使用dispatch_async。
正例:
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_block_t block = ^() {
NSLog(@"%@", [NSThread currentThread]);
};
dispatch_async(mainQueue, block); //使用異步操作
}
反例:
// 禁止。出現死鎖,報錯:EXC_BAD_INSTRUCTION。原因:在主隊列中同步的添加一個block到主隊列中
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_block_t block = ^() {
NSLog(@"%@", [NSThread currentThread]);
};
dispatch_sync(mainQueue, block);
}
2.1.10 【參考】使用 performSelector:withObject:afterDelay:和 cancelPreviousPerformRequestsWithTarget組合的時候要小心
- afterDelay會增加receiver的引用計數,cancel則會對應減一
- 如果在receiver的引用計數只剩下1 (僅為delay)時,調用cancel之后會立即銷毀receiver,后續(xù)再調用receiver的方法就會crash
正例:
__weak typeof(self) weakSelf = self;
[NSObject cancelPreviousPerformRequestsWithTarget:self];
if (!weakSelf) {
//NSLog(@"self被銷毀");
return;
}
[self doOther];
2.1.11 【強制】禁止在非主線程中進行UI元素的操作
2.1.12 【強制】在主線程中禁止進行同步網絡資源讀取,使用NSURLSession進行異步獲取
2.1.13 【強制】如果需要進行大文件或者多文件的IO操作,禁止主線程使用,必須進行異步處理
2.1.14 【強制】對剪貼板的讀取必須要放在異步線程處理,最新Mac和iOS里的剪貼板共享功能會導致有可能需要讀取大量的內容,導致讀取線程被長時間阻塞
正例:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
if (pasteboard.string.length > 0) {//這個方法會阻塞線程
NSString *text = [pasteboard.string copy];
[pasteboard setValue:@"" forPasteboardType:UIPasteboardNameGeneral];
if (text == nil || [text isEqualToString:@""]) {
return ;
}
dispatch_async(dispatch_get_main_queue(), ^{
[self processShareCode:text];
});
}
});
2.2 內存管理
2.2.1 【推薦】請慎重使用單例,避免造成產生不必要的常駐內存。
2.2.2 【推薦】單例初始化方法中盡量保證單一職責,尤其不要進行其他單例的調用。極端情況下,兩個單例對象在各自的單例初始化方法中調用,會造成死鎖。
2.2.3 【強制】Delegate需要用weak進行引用。
2.2.4 【強制】使用block時,需要在block訪問外部weak修飾的self,內部在重新strong處理。避免RetainCycle。
2.2.5 【推薦】strong引用 子實例,weak引用parent,基礎類型使用assign,NSString,NSArray,block使用copy
2.2.6 【強制】對類添加屬性時使用 copy方式還是使用retain方式規(guī)約:
- 對實現 NSCopying協(xié)議的對象使用copy方式。通常情況下,諸如NSString、NSURL, block,NSArray 這樣的對象應該能被copy;
- 像UIView的對象則應該可以被保持。strong引用 子實例,weak引用parent.
- 基礎類型使用assign。
2.2.7 【強制】在dealloc中要記得要remove observer, callback=null
2.2.8 【強制】會循環(huán)使用的Timer(指定了repeat參數為YES),必須要在合適的時機調用invalidate方法,否則會出現內存泄漏,在使用類的析構函數中調用Timer的invalidate方法為時已晚,因為timer會對其傳遞的目標object增加引用計數,若不調用invalidate,使用類根本得不到析構。
- 對于指定了repeat參數為NO的Timer,則可以不調用invalidate方法。
2.2.9 【強制】在init 和dealloc中不允許使用self訪問屬性(父類屬性除外),只允許通過"_變量名"直接訪問。
- 容易出現重復創(chuàng)建對象,甚至crash問題。
- 在init和dealloc階段,self是一個不完整的對象。
- 由于accessor方法是可以被子類重寫的,在調用父類init初始化的時候,使用self訪問屬性會調到子類重寫的(如果有)getter或setter,這就出現了先于子類init訪問其屬性或調用子類方法的情況,如果子類getter或setter中有一些特殊的處理邏輯,在某些極端情況下就可能出現行為不一致的問題。 由于在init函數返回前,對象結構和結構是不穩(wěn)定的,在init函數內對任何方法的調用(尤其是public方法)都應該慎之又慎。dealloc同理。
2.2.10 【推薦】在非init和dealloc方法中訪問屬性推薦通過getter方法獲取,不推薦直接使用“_變量名”。
2.2.11 【推薦】在init中不需要直接使用的Property,建議使用lazyloading的方法創(chuàng)建。
2.2.12 【強制】在創(chuàng)建大量臨時的UIImage,或者 Model 之類的對象的時,用@autoreleasepool使autorelease 對象在結束時間釋放,緩解內存的壓力。比如:
正例:
NSMutableArray *dataList = [NSMutableArray new];
NSMutableArray *imageList = [NSMutableArray new];
[dataList enumerateObjectsUsingBlock:^(NSDictionary *dict, NSUInteger idx, BOOL *stop) {
@autoreleasepool {
NSData *data = dataList[idx];
UIImage *image = [[UIImage alloc] initWithData:data];
//可能對 image 進行一些處理,裁剪之類的
[imageList addObject:image];
}
}];
2.2.13 【強制】在使用到 UIScrollView,UITableView,UICollectionView的 Class 中,需要在dealloc方法里手動的把對應的 delegate, dataSouce置為 nil
- 防止在scrollView滑動時頁面退出,delegate釋放,出現crash問題
- 蘋果在iOS9上已經將以上類的delegate及datasource由assign改為了weak,如果只支持9.0以上,則不需要手動置nil
2.2.14 【推薦】在dealloc中,避免將self作為參數傳遞。如果被retain住,到下個runloop周期再次釋放,則會造成多次釋放crash。
-(void)dealloc{
[self unsafeMethod:self];
//因為當前已經在self所指向對象的銷毀階段,如果在unsafeMethod:中將self放到了autorelease pool中,那么self會被retain住,計劃下個runloop周期再進行銷毀;但是dealloc運行結束后,self對象的內存空間就直接被回收了,self變成了野指針
//當到了下個runloop周期,self指向的對象實際上已經被銷毀,會因為非法訪問造成crash問題
}
2.2.15 【推薦】除非是非法參數等提前判斷提前return的可以寫在最前面。其他的return建議有效返回值盡量只剩最后一個。提前return時,要注意是否有對象沒有被釋放(常見的有CF對象),是否有鎖沒有釋放等配對問題。
2.2.16 【強制】禁止一次性申請超過10MB的內存。
- 內存過高將會導致app被kill,并且沒有crash堆棧。而申請大內存將會增加內存峰值,更容易出現內存過高而crash。
2.3 集合
- 包括,但不限于 NSMutableDictionay,NSMutableArray,NSMutableSet
2.3.1 【強制】插入對象需要做判空處理。
2.3.2 【強制】注意線程安全問題,必要時加鎖,保障線程安全
2.3.3 【強制】先copy,再枚舉操作,禁止對非臨時變量的可變集合進行枚舉操作,多線程情況下有可能因為可變集合在進行枚舉時發(fā)生改變進而crash。
正例:
- (void)checkAllValidItems{
[_arrayLock lock];
NSArray *array = [oldArray copy];
[_arrayLock unlock];
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
//do something using obj
}];
}
反例:
-(void)checkAllValidItems{
[self.allItems enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
//do something using obj
//如果在enumerate過程中其它線程對self.allItems進行了變更操作,這里就會引發(fā)crash
}];
}
2.3.4 【推薦】大部分情況下都不使用可變集合作為成員變量,如果確實需要進行集合的增刪改操作,使用臨時可變集合變量處理,之后再進行賦值操作。
2.3.5 【強制】禁止返回mutable對象,禁止mutable對象作為入參傳遞。
2.3.6 【推薦】如果使用NSMutableDictionary作為緩存,推薦使用NSCache代替
2.3.7 【推薦】容器類使用泛型來指定對象的類型
正例:
@property (readonly) NSArray<NSURL *> *imageURLs;
NSDictionary<NSString *, NSNumber *> *mapping = @{@"a": @1, @"b": @2};
反例:
@property (readonly) NSArray *imageURLs;
NSDictionary<NSString *, NSNumber *> *mapping = @{@"a": @1, @"b": @2};
2.4 字符串
2.4.1 【推薦】當使用keypaths:@"xx"時候,盡量使用NSStringFromSelector(@selector(xx))方式,防止某個key被刪除后沒有編譯感知
2.4.2 【強制】取substring的時候要考慮emoji字符的問題,防止截到中間crash
- (NSString *)dt_substringToIndex:(NSUInteger)index {
//... 越界判斷
NSRange wRange = [self rangeOfComposedCharacterSequencesForRange:NSMakeRange(0, index)];
return [self substringWithRange:wRange];
}
2.5 鎖
2.5.1 【推薦】專鎖專用,一個lock對象只負責一個任務。這樣可以在邏輯上進行區(qū)分,也可以避免潛在的死鎖問題
2.5.2 【推薦】不同鎖的使用場景:
- 性能最好的屬pthread_mutex、dispatch_semaphore,另外dispatch_semaphore在等待的時候會釋放CPU資源,所以適合用在等待耗時較長的場景;
- @synchronized是最簡單易用的遞歸鎖,不會有忘記unlock的情況,但性能也是最低的,適合用在對性能要求不高的場景;
- 其他的還有NSLock,性能介于上面二者之間,也有對應的條件鎖NSConditionLock和遞歸鎖NSRecursiveLock,因為是Objective-C對象,適合用在偏Objective-C編程的場景,比如需要把鎖存放在NSDictionary中的場景。
2.5.3 【強制】在使用鎖的過程中如果要return,切記要先進行unlock; 如果可能有exception發(fā)生,那么需要在@finally中進行鎖的釋放
正例:
- (void) exclusiveMethod1{
[self.lock lock];
if (condition == true){
//這里要記得unlcok,否則下次在進入這個方法就會發(fā)生線程被死鎖的問題
[self.lock unlock];
return;
}
[self.lock unlock];
}
- (void) exclusiveMethod2{
[self.lock lock];
@try{
//異常發(fā)生
}@catch(NSException* ex){
}@finally{
//此處需要進行鎖的回收
[self.lock unlock];
}
}
2.6 IO
2.6.1 【參考】盡量減少使用NSUserDefault
2.6.2 【推薦】[[NSUserDefaults standardUserDefaults] synchronize]會block當前線程直到所有UserDefault里的內容寫回存儲;如果內容過多,重復調用的話會嚴重影響性能。建議只有在合適的時候(比如退到后臺)再進行持久化操作(此方法即將deprecated,可以不再調用)
2.6.3 【推薦】一些經常被讀取的本地文件建議做好內存緩存,減少IO開銷
2.6.4 【推薦】文件存儲路徑請遵循以下規(guī)則:
- Documents目錄:您應該將所有的應用程序數據文件寫入到這個目錄下。這個目錄用于存儲用戶數據。該路徑可通過配置實現iTunes共享文件。可被iTunes備份。
- AppName.app 目錄:這是應用程序的程序包目錄,包含應用程序的本身。由于應用程序必須經過簽名,所以您在運行時不能對這個目錄中的內容進行修改,否則可能會使應用程序無法啟動。
- Library目錄:這個目錄下有兩個子目錄:
** Preferences 目錄:包含應用程序的偏好設置文件。您不應該直接創(chuàng)建偏好設置文件,而是應該使用NSUserDefaults類來取得和設置應用程序的偏好.
** Caches 目錄:用于存放應用程序專用的支持文件,保存應用程序再次啟動過程中需要的信息。 可創(chuàng)建子文件夾。可以用來放置您希望被備份但不希望被用戶看到的數據。該路徑下的文件夾,除Caches以外,都會被iTunes備份。 - tmp 目錄:這個目錄用于存放臨時文件,保存應用程序再次啟動過程中不需要的信息。該路徑下的文件不會被iTunes備份。
2.7 UI
2.7.1 【推薦】不要在除了viewDidLoad方法之外調用ViewController的self.view來進行view操作,特別是在一些系統(tǒng)通知之類的回調中,有可能造成self.view創(chuàng)建出來之后沒有被加入到當前層級,導致子view的詭異問題。
- (void)didReceiveMemoryWarning{
[super didReceiveMemoryWarning];
[self.view doSomething]; //如果當VC已經被創(chuàng)建,但是view還沒有加入到view層級中時(比如Tabbar初始化之后的非選中VC),此時接收到了內存警告,那么self.view會被直接創(chuàng)建,沒有加入到層級,導致其子view可能處于異常的狀態(tài)
}
2.7.2 【推薦】如果想要獲取app的window,不要view.window來獲取,可以使用[[UIApplication sharedApplication] keyWindow]來獲取。
- 如果view不在展示時,獲取window會是nil,而不是真正的app所在的window.
2.7.3 【強制】UI對象只允許在主線程訪問。(避免在異步線程里釋放,這樣可以避免在dealloc時訪問view結構導致問題)
2.7.4 【強制】禁止在ViewController的dealloc方法中訪問self.view,會導致已經釋放的view被再次重建,可能會造成各種不可預知的問題
2.7.5 【強制】顯示帶textfield的alert之前,一定要確保鍵盤不在顯示狀態(tài),否則會crash
- 可以直接: [[[UIApplication sharedApplication].delegate window] endEditing:YES];
2.7.6 【強制】禁止使用drawViewHierarchyInRect截屏
- 原因:截屏會消耗大內存和耗性能,不建議使用該技術方案.
- 推薦使用 snapshotViewAfterScreenUpdates
2.7.7 【推薦】不建議將UIView類的對象加入到NSDictionary, NSSet,如有需要可以添加到NSMapTable 和 NSHashTable。
- NSDictionary,NSSet會對加入的對象做strong引用,而NSMapTable、NSHashTable會對加入的對象做weak引用。
2.8 Category
2.8.1 【強制】category方法加自定義前綴。防止與其它人沖突。
正例:
@interface NSString(CYYEncode)
- (NSString *)cyy_urlEncode;
@end
反例:
@interface NSString(Encode)
- (NSString*)encode;
@end
2.8.2 【強制】禁止category方法覆蓋系統(tǒng)方法,防止出現方法調用的不確定性
2.8.3 【推薦】對于一些提供category的工具庫,建議根據不同類型功能拆分成不同的子bundle,方便引用方按需引用,控制App體積
2.8.4 【強制】Category的源文件名稱必須是“類名+擴展名.{h,m}”
正例:
NSString+CYYEncode.h
反例:
NSStringCYYEncode.h
NSString_CYYEncode.h
2.9 異常
2.9.1 【強制】不要在@finally塊中使用return或者@throw等導致方法執(zhí)行中斷的語句,會導致@try內的return失效
2.10 其它
2.10.1 【推薦】使用Method swizzle之前考慮是否有其他方法可以代替,禁止隨意swizzle其他基礎庫及三方庫的方法
2.10.2 【強制】NSNotification接口,userInfo和object的使用要規(guī)范。
- object通常是指發(fā)出notification的對象,如果在發(fā)送notification的同時要傳遞一些信息,請使用userInfo,而不是object.
2.10.3 【強制】網絡返回數據在客戶端需要轉為 NSString 類型,在作為參數返回時轉為接口需要的指定類型。避免應用內模塊之間傳遞數據時不必要的類型轉換。
2.10.4 【推薦】在使用固定格式的dateFormatter時候,需要設置setLocale為"en_US_POSIX",防止一些不同日歷下格式異常。
示例:
NSDate* now = [NSDate date];
NSDateFormatter* fmt = [[NSDateFormatter alloc] init];
fmt.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
NSString* string = "1996-12-19T16:39:57-08:00";
NSDate* date = fmt.dateFromString(string);
2.10.5 【推薦】在使用CTTelephonyNetworkInfo的時候,務必使用全局的單例實例,這個類本身存在bug,如果有多實例會存在會導致小概率的crash。
2.10.6 【強制】調用block時務必判斷block是否為nil
2.10.7 【推薦】調用delegate的optional方法時,判斷delegate能否響應該方法,避免crash
2.10.8 【強制】禁止訪問對象的結構體變量(使用->)
2.10.9 【強制】需要使用磁盤緩存的業(yè)務,務必提供清理緩存的能力
2.10.10 【強制】對于不確定對象類型的比較,可以使用isEqual:方法,其會對類型進行判斷;對于確定對象類型的比較,比如NSString,可以使用isEqualToString:,其不對類型進行判斷,但相比前者性能更好
三. 工程規(guī)約
3.1 版本管理規(guī)約
3.1.1 【建議】遵循語義化版本號規(guī)范,版本格式:主版本號.次版本號.修訂號,版本號遞增規(guī)則如下:
- 主版本號:當你做了不兼容的 API 修改
- 次版本號:當你做了向下兼容的功能性新增,
- 修訂號:當你做了向下兼容的問題修正。
- 先行版本號及版本編譯信息可以加到“主版本號.次版本號.修訂號”的后面,作為延伸。
3.1.2 【建議】App灰度使用四位版本號
3.1.3【建議】業(yè)務方維護自己業(yè)務SDK的版本號,不要使用主App的版本號來做業(yè)務邏輯判斷,如果有需要可以使用業(yè)務SDK的版本號來判斷
3.2 分支管理
3.2.1 【建議】主分支Master
- 代碼庫應該有一個、且僅有一個主分支。所有提供給用戶使用的正式版本,都在這個主分支上發(fā)布。
3.2.2 【【建議】開發(fā)分支Develop
- 日常開發(fā)分支在Develop,如果想正式對外發(fā)布,就在Master分支上,對Develop分支進行"合并"(merge)。
3.2.3 【建議】臨時性分支,按不同的需求,開啟相應的臨時分支,使用完以后,應該刪除
- 功能(feature)分支
- 預發(fā)布(release)分支
- 修補bug(fixbug)分支
- 建議使用GitFlow進行代碼管理。
3.2.4 【強制】每次版本發(fā)布之后,都應該在代碼倉庫中對應的節(jié)點添加tag,保證版本的可回溯
3.2.5 【參考】在 Git 提交時可以使用 [添加],[修改],[刪除],[修復],[更新]等前綴詞語來表明當前的Commit 信息。
3.3 包管理
3.3.1 【強制】使用CocoaPods作為包管理工具
3.3.2 【參考】推薦使用 source 'https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git' 源
3.3.3 【強制】外部 pod 倉庫引用務必使用固定版本號,僅在必要時更新指定版本號
3.3.4 【強制】檢查podspec的resource選項,不要把Podfile、podspec、InfoPlist.strings、Info.plist或者源文件等導出到使用方的工程中
3.3.5 【強制】模塊引用使用自上往下方式,下層模塊禁止引用上層模塊,基礎模塊禁止引用其他模塊。如果在進行模塊開發(fā)更新過程中發(fā)現需要違背此原則,則需要思考是否有需要新增必要的模塊。
3.3.6 【推薦】使用carthage進行包管理工具
3.3.7 【推薦】純Swift工程時,可以使用SwiftPackage
四. 示例
五. 參考
- 《Coding Guidelines for Cocoa》
- 《Zen and the Art of the Objective-C Craftsmanship》
- 《iOS Good Practices》
- 《Wonderful Objective-C style guide》
- 《Google Objective-C Style Guide》
- 《The official raywenderlich.com Objective-C style guide》
- 《紐約時報移動團隊 Objective-C 規(guī)范指南》
- 《Effective Objective-C 2.0》
- Effective Objective-C 2.0 無廢話精簡篇