本文來自:https://github.com/oa414/objc-zen-book-cn#%E9%94%99%E8%AF%AF%E5%A4%84%E7%90%86
錯誤處理
當方法返回一個錯誤參數的引用的時候,檢查返回值,而不是錯誤的變量。
推薦:
<span style="font-size:14px;">NSError *error = nil;
if (![self trySomethingWithError:&error]) {
// Handle Error
}</span>
此外,一些蘋果的 API 在成功的情況下會對 error 參數(如果它非
NULL)寫入垃圾值(garbage values),所以如果檢查 error 的值可能導致錯誤 (甚至崩潰)。常量
常量應該用 static
聲明,不要使用 #define
,除非它就是明確作爲一個宏來用的。
常量應該使用駝峯命名法,並且爲了清楚,應該用相關的類名作爲前綴。
推薦
<span style="font-size:14px;">static const NSTimeInterval ZOCSignInViewControllerFadeOutAnimationDuration = 0.4;
static NSString * const ZOCCacheControllerDidClearCacheNotification = @"ZOCCacheControllerDidClearCacheNotification";
static const CGFloat ZOCImageThumbnailHeight = 50.0f;</span></span>
常量應該在
interface 文件中這樣被聲明:
<span style="font-size:14px;">extern NSString *const ZOCCacheControllerDidClearCacheNotification;</span>
並且應該在實現文件中實現它的定義。
字面值
NSString
, NSDictionary
, NSArray
,
和 NSNumber
字面值應該用在任何創建不可變的實例對象。特別小心不要把 nil
放進 NSArray
和 NSDictionary
裏,這會導致崩潰
例子:
<span style="font-size:14px;">NSArray *names = @[@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul"];
NSDictionary *productManagers = @{@"iPhone" : @"Kate", @"iPad" : @"Kamal", @"Mobile Web" : @"Bill"};
NSNumber *shouldUseLiterals = @YES;
NSNumber *buildingZIPCode = @10018;</span>
不要這樣:
<span style="font-size:14px;">NSArray *names = [NSArray arrayWithObjects:@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul", nil];
NSDictionary *productManagers = [NSDictionary dictionaryWithObjectsAndKeys: @"Kate", @"iPhone", @"Kamal", @"iPad", @"Bill", @"Mobile Web", nil];
NSNumber *shouldUseLiterals = [NSNumber numberWithBool:YES];
NSNumber *buildingZIPCode = [NSNumber numberWithInteger:10018];</span>
對於那些可變的副本,我們推薦使用明確的如 NSMutableArray
, NSMutableString
這些類。Initializer和dealloc
init
方法應該是這樣的結構:
<span style="font-size:14px;">- (instancetype)init
{
self = [super init]; // call the designated initializer
if (self) {
// Custom initialization
}
return self;
}</span>
爲什麼設置 self
爲 [super
init]
的返回值,以及中間發生了什麼呢?這是一個十分有趣的話題。
讓我們後退一步:我們一直寫類似 [[NSObject alloc] init]
的表達式,而淡化了 alloc
和 init
的區別
。一個 Objective-C 的特性叫 兩步創建 。 這意味着申請分配內存和初始化是兩個分離的操作。
alloc
表示對象分配內存,這個過程涉及分配足夠的可用內存來保存對象,寫入isa
指針,初始化 retain 的計數,並且初始化所有實例變量。init
是表示初始化對象,這意味着把對象轉換到了個可用的狀態。這通常是指把可用的值賦給了對象的實例變量。
alloc
方法會返回一個合法的沒有初始化的實例對象。每一個發送到實例的消息會被翻譯爲objc_msgSend()
函數的調用,它的參數是指向 alloc
返回的對象的、名爲 self
的指針的。這樣之後 self
已經可以執行所有方法了。
爲了完成兩步創建,第一個發送給新創建的實例的方法應該是約定俗成的 init
方法。注意在 NSObject
的 init
實現中,僅僅是返回了 self
。
關於 init
有一個另外的重要的約定:這個方法可以(並且應該)在不能成功完成初始化的時候返回nil
;初始化可能因爲各種原因失敗,比如一個輸入的格式錯誤,或者未能成功初始化一個需要的對象。
這樣我們就理解了爲什麼需要總是調用 self = [super init]
。如果你的超類沒有成功初始化它自己,你必須假設你在一個矛盾的狀態,並且在你的實現中不要處理你自己的初始化邏輯,同時返回nil
。如果你不是這樣做,你看你會得到一個不能用的對象,並且它的行爲是不可預測的,最終可能會導致你的
App 發生 crash。
重新給 self
賦值同樣可以被 init
利用爲在被調用的時候返回不同的實例。一個例子是 類簇 或者其他的返回相同的(不可變的)實例對象的
Cocoa 類。
Designated和Secondary Initializers
初始化模式
類簇(class cluster)
an architecture that groups a number of private, concrete subclasses under a public, abstract superclass. (一個在共有的抽象超類下設置一組私有子類的架構)
class
cluster 的想法很簡單,你經常有一個抽象類在初始化期間處理信息,經常作爲一個構造器裏面的參數或者環境中讀取,來完成特定的邏輯並且實例化子類。這個"public facing" 應該知曉它的子類而且返回適合的私有子類。
Class clusters 在 Apple 的Framework 中廣泛使用:一些明顯的例子比如 NSNumber
可以返回不同類型給你的子類,取決於
數字類型如何提供 (Integer, Float, etc...) 或者 NSArray
返回不同的最優存儲策略的子類。
一個經典的例子是如果你有爲
iPad 和 iPhone 寫的一樣的 UIViewController 子類,但是在不同的設備上有不同的行爲。
<span style="font-size:14px;">@implementation ZOCKintsugiPhotoViewController
- (id)initWithPhotos:(NSArray *)photos
{
if ([self isMemberOfClass:ZOCKintsugiPhotoViewController.class]) {
self = nil;
if ([UIDevice isPad]) {
self = [[ZOCKintsugiPhotoViewController_iPad alloc] initWithPhotos:photos];
}
else {
self = [[ZOCKintsugiPhotoViewController_iPhone alloc] initWithPhotos:photos];
}
return self;
}
return [super initWithNibName:nil bundle:nil];
}
@end</span></span>
上面的代碼的例子展示瞭如何創建一個類簇。
單例
如果可能,請儘量避免使用單例而是依賴注入。
然而,如果一定要用,請使用一個線程安全的模式來創建共享的實例。對於 GCD,用 dispatch_once()
函數就可以咯。
<span style="font-size:14px;">+ (instancetype)sharedInstance
{
static id sharedInstance = nil;
static dispatch_once_t onceToken = 0;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}</span>
使用 dispatch_once(),來控制代碼同步,取代了原來的約定俗成的用法。
dispatch_once()
的優點是,它更快,而且語法上更乾淨,因爲dispatch_once()的意思就是 “把一些東西執行一次”,就像我們做的一樣。 這樣同時可以避免 possible
and sometimes prolific crashes.
經典的使用單例對象的例子是一個設備的 GPS 以及動作傳感器。即使單例對象可以被子類化,這個情況也可以十分有用。這個接口應該證明給出的類是趨向於使用單例的。然而,通常使用一個單獨的公開的 sharedInstance
類方法就夠了,並且不可寫的屬性也應該被暴露。
把單例作爲一個對象的容器來在代碼或者應用層面上共享是糟糕和醜陋的,這是一個不好的設計。
屬性你總應該用 getter 和 setter ,因爲:
- 使用 setter 會遵守定義的內存管理語義(
strong
,weak
,copy
etc...) ,這個在 ARC 之前就是相關的內容。舉個例子,copy
屬性定義了每個時候你用 setter 並且傳送數據的時候,它會複製數據而不用額外的操作。 - KVO 通知(
willChangeValueForKey
,didChangeValueForKey
) 會被自動執行。 - 更容易debug:你可以設置一個斷點在屬性聲明上並且斷點會在每次 getter / setter 方法調用的時候執行,或者你可以在自己的自定義 setter/getter 設置斷點。
- 允許在一個單獨的地方爲設置值添加額外的邏輯。
你應該傾向於用 getter:
- 它是對未來的變化有擴展能力的(比如,屬性是自動生成的)。
- 它允許子類化。
- 更簡單的debug(比如,允許拿出一個斷點在 getter 方法裏面,並且看誰訪問了特別的 getter
- 它讓意圖更加清晰和明確:通過訪問 ivar
_anIvar
你可以明確的訪問self->_anIvar
.這可能導致問題。在 block 裏面訪問 ivar (你捕捉並且 retain 了 self,即使你沒有明確的看到 self 關鍵詞)。 - 它自動產生KVO 通知。
- 在消息發送的時候增加的開銷是微不足道的。更多關於新年問題的介紹你可以看 Should I Use a Property or an Instance Variable?。
屬性定義
屬性可以存儲一個代碼塊。爲了讓它存活到定義的塊的結束,必須使用 copy
(block 最早在棧裏面創建,使用 copy
讓
block 拷貝到堆裏面去)
爲了完成一個共有的 getter 和一個私有的 setter,你應該聲明公開的屬性爲 readonly
並且在類擴展總重新定義通用的屬性爲 readwrite
的。
相等性
一個完整的
isEqual 方法應該是這樣的:
<span style="font-size:14px;">- (BOOL)isEqual:(id)object {
if (self == object) {
return YES;
}
if (![object isKindOfClass:[ZOCPerson class]]) {
return NO;
}
return [self isEqualToPerson:(ZOCPerson *)object];
}
- (BOOL)isEqualToPerson:(Person *)person {
if (!person) {
return NO;
}
BOOL namesMatch = (!self.name && !person.name) ||
[self.name isEqualToString:person.name];
BOOL birthdaysMatch = (!self.birthday && !person.birthday) ||
[self.birthday isEqualToDate:person.birthday];
return haveEqualNames && haveEqualBirthdays;
}</span>
一個對象實例的
hash
計算結果應該是確定的。當它被加入到一個容器對象(比如 NSArray
, NSSet
,
或者 NSDictionary
)的時候這是很重要的,否則行爲會無法預測(所有的容器對象使用對象的
hash 來查找或者實施特別的行爲,如確定唯一性)這也就是說,應該用不可變的屬性來計算 hash 值,或者,最好保證對象是不可變的。Categories
Protocols
Pragma
pragma-mark
#pragma-mark - NSObject
關於pragma
如果你知道你的代碼不會導致內存泄露,你可以通過加入這些代碼忽略這些警告。
如:performSelector may cause a leak because its selector is unknown(執行 selector 可能導致泄漏,因爲這個 selector 是未知的)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[myObj performSelector:mySelector withObject:name];
#pragma clang diagnostic pop
注意我們是如何在相關代碼上下文中用 pragma 停用 -Warc-performSelector-leaks 檢查的。這確保我們沒有全局禁用。如果全局禁用,可能會導致錯誤。
忽略沒用使用變量的編譯警告
NSString *foo;
#pragma unused (foo)
明確編譯器警告和錯誤
對象間的通訊
Block
深入block
- block 是在棧上創建的
- block 可以複製到堆上
- block 有自己的私有的棧變量(以及指針)的常量複製
- 可變的棧上的變量和指針必須用 __block 關鍵字聲明
self的循環引用
當使用代碼塊和異步分發的時候,要注意避免引用循環。
總是使用 weak
引用會導致引用循環。
此外,把持有 block 的屬性設置爲 nil (比如 self.completionBlock = nil
)
是一個好的實踐。它會打破 block 捕獲的作用域帶來的引用循環。
例子:
__weak __typeof(self) weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
[weakSelf doSomethingWithData:data];
}];
多個語句的例子:
<pre name="code" class="objc">__weak __typeof(self)weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf doSomethingWithData:data];
[strongSelf doSomethingWithData:data];
}
}];
。。。。。。。
在一個
ARC 的環境中,如果嘗試用 ->
符號來表示,編譯器會警告一個錯誤:
Dereferencing a __weak pointer is not allowed due to possible null value caused by race condition, assign it to a strong variable first. (對一個 __weak 指針的解引用不允許的,因爲可能在競態條件裏面變成 null, 所以先把他定義成 strong 的屬性)
可以用下面的代碼展示
__weak typeof(self) weakSelf = self;
myObj.myBlock = ^{
id localVal = weakSelf->someIVar;
};
委託和數據源
委託模式是單向的,消息的發送方(委託方)需要知道接收方(委託),反過來就不是了。對象之間沒有多少耦合,因爲發送方只要知道它的委託實現了對應的
protocol。
本質上,委託模式只需要委託提供一些回調方法,就是說委託實現了一系列空返回值的方法。不幸的是
Apple 的 API 並沒有尊重這個原則,如UITableViewDelegate協議。
- 委託模式:事件發生的時候,委託者需要通知委託
- 數據源模式: 委託方需要從數據源對象拉取數據
é¢ååé¢ç¼ç¨