對象之間需要通信,這也是所有軟件的基礎(chǔ)。再非凡的軟件也需要通過對象通信來完成復(fù)雜的目標。本章將深入討論一些設(shè)計概念,以及如何依據(jù)這些概念來設(shè)計出良好的架構(gòu)。
Blocks 是 Objective-C 版本的 lambda 或者 closure(閉包)。
使用 block 定義異步接口:
- (void)downloadObjectsAtPath:(NSString *)path
completion:(void(^)(NSArray *objects, NSError *error))completion;
當(dāng)你定義一個類似上面的接口的時候,盡量使用一個單獨的 block 作為接口的最后一個參數(shù)。把需要提供的數(shù)據(jù)和錯誤信息整合到一個單獨 block 中,比分別提供成功和失敗的 block 要好。
以下是你應(yīng)該這樣做的原因:
看上面的方法,完成處理的 block 的參數(shù)很常見:第一個參數(shù)是調(diào)用者希望獲取的數(shù)據(jù),第二個是錯誤相關(guān)的信息。這里需要遵循以下兩點:
objects
不為 nil,則 error
必須為 nilobjects
為 nil,則 error
必須不為 nil因為調(diào)用者更關(guān)心的是實際的數(shù)據(jù),就像這樣:
- (void)downloadObjectsAtPath:(NSString *)path
completion:(void(^)(NSArray *objects, NSError *error))completion {
if (objects) {
// do something with the data
}
else {
// some error occurred, 'error' variable should not be nil by contract
}
}
此外,Apple 提供的一些同步接口在成功狀態(tài)下向 error 參數(shù)(如果非 NULL) 寫入了垃圾值,所以檢查 error 的值可能出現(xiàn)問題。
一些關(guān)鍵點:
如果 block 沒有在其他地方被保持,那么它會隨著棧生存并且當(dāng)棧幀(stack frame)返回的時候消失。當(dāng)在棧上的時候,一個 block 對訪問的任何內(nèi)容不會有影響。如果 block 需要在棧幀返回的時候存在,它們需要明確地被復(fù)制到堆上,這樣,block 會像其他 Cocoa 對象一樣增加引用計數(shù)。當(dāng)它們被復(fù)制的時候,它會帶著它們的捕獲作用域一起,retain 他們所有引用的對象。如果一個 block指向一個棧變量或者指針,那么這個block初始化的時候它會有一份聲明為 const 的副本,所以對它們賦值是沒用的。當(dāng)一個 block 被復(fù)制后,__block
聲明的棧變量的引用被復(fù)制到了堆里,復(fù)制之后棧上的以及產(chǎn)生的堆上的 block 都會引用這個堆上的變量。
用 LLDB 來展示 block 是這樣子的:
最重要的事情是 __block
聲明的變量和指針在 block 里面是作為顯示操作真實值/對象的結(jié)構(gòu)來對待的。
block 在 Objective-C 里面被當(dāng)作一等公民對待:他們有一個 isa
指針,一個類也是用 isa
指針來訪問 Objective-C 運行時來訪問方法和存儲數(shù)據(jù)的。在非 ARC 環(huán)境肯定會把它搞得很糟糕,并且懸掛指針會導(dǎo)致 Crash。__block
僅僅對 block 內(nèi)的變量起作用,它只是簡單地告訴 block:
嗨,這個指針或者原始的類型依賴它們在的棧。請用一個棧上的新變量來引用它。我是說,請對它進行雙重解引用,不要 retain 它。 謝謝,哥們。
如果在定義之后但是 block 沒有被調(diào)用前,對象被釋放了,那么 block 的執(zhí)行會導(dǎo)致 Crash。 __block
變量不會在 block 中被持有,最后... 指針、引用、解引用以及引用計數(shù)變得一團糟。
當(dāng)使用代碼塊和異步分發(fā)的時候,要注意避免引用循環(huán)。 總是使用 weak
引用會導(dǎo)致引用循環(huán)。 此外,把持有 blocks 的屬性設(shè)置為 nil (比如 self.completionBlock = nil
) 是一個好的實踐。它會打破 blocks 捕獲的作用域帶來的引用循環(huán)。
例子:
__weak __typeof(self) weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
[weakSelf doSomethingWithData:data];
}];
不要這樣做:
[self executeBlock:^(NSData *data, NSError *error) {
[self doSomethingWithData:data];
}];
多個語句的例子:
__weak __typeof(self)weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf doSomethingWithData:data];
[strongSelf doSomethingWithData:data];
}
}];
不要這樣做:
__weak __typeof(self)weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
[weakSelf doSomethingWithData:data];
[weakSelf doSomethingWithData:data];
}];
你應(yīng)該把這兩行代碼作為 snippet 加到 Xcode 里面并且總是這樣使用它們。
__weak __typeof(self)weakSelf = self;
__strong __typeof(weakSelf)strongSelf = weakSelf;
這里我們來討論下 block 里面的 self 的 __weak
和 __strong
限定詞的一些微妙的地方。簡而言之,我們可以參考 self 在 block 里面的三種不同情況。
__weak
的 引用到 self,并且在 block 里面使用這個弱引用__weak
的 引用到 self,并在在 block 內(nèi)部通過這個弱引用定義一個 __strong
的引用。1. 直接在 block 里面使用關(guān)鍵詞 self
如果我們直接在 block 里面用 self 關(guān)鍵字,對象會在 block 的定義時候被 retain,(實際上 block 是 copied 但是為了簡單我們可以忽略這個)。一個 const 的對 self 的引用在 block 里面有自己的位置并且它會影響對象的引用計數(shù)。如果 block 被其他 class 或者/并且傳送過去了,我們可能想要 retain self 就像其他被 block 使用的對象,從他們需要被block執(zhí)行
dispatch_block_t completionBlock = ^{
NSLog(@"%@", self);
}
MyViewController *myController = [[MyViewController alloc] init...];
[self presentViewController:myController
animated:YES
completion:completionHandler];
不是很麻煩的事情。但是, 當(dāng) block 被 self 在一個屬性 retain(就像下面的例子)呢
self.completionHandler = ^{
NSLog(@"%@", self);
}
MyViewController *myController = [[MyViewController alloc] init...];
[self presentViewController:myController
animated:YES
completion:self.completionHandler];
這就是有名的 retain cycle, 并且我們通常應(yīng)該避免它。這種情況下我們收到 CLANG 的警告:
Capturing 'self' strongly in this block is likely to lead to a retain cycle (在 block 里面發(fā)現(xiàn)了 `self` 的強引用,可能會導(dǎo)致循環(huán)引用)
所以可以用 weak
修飾
2. 在 block 外定義一個 __weak
的 引用到 self,并且在 block 里面使用這個弱引用
這樣會避免循環(huán)引用,也是我們通常在 block 已經(jīng)被 self 的 property 屬性里面 retain 的時候會做的。
__weak typeof(self) weakSelf = self;
self.completionHandler = ^{
NSLog(@"%@", weakSelf);
};
MyViewController *myController = [[MyViewController alloc] init...];
[self presentViewController:myController
animated:YES
completion:self.completionHandler];
這個情況下 block 沒有 retain 對象并且對象在屬性里面 retain 了 block 。所以這樣我們能保證了安全的訪問 self。 不過糟糕的是,它可能被設(shè)置成 nil 的。問題是:如果和讓 self 在 block 里面安全地被銷毀。
舉個例子, block 被一個對象復(fù)制到了另外一個(比如 myControler)作為屬性賦值的結(jié)果。之前的對象在可能在被復(fù)制的 block 有機會執(zhí)行被銷毀。
下面的更有意思。
3. 在 block 外定義一個 __weak
的 引用到 self,并在在 block 內(nèi)部通過這個弱引用定義一個 __strong
的引用
你可能會想,首先,這是避免 retain cycle 警告的一個技巧。然而不是,這個到 self 的強引用在 block 的執(zhí)行時間 被創(chuàng)建。當(dāng) block 在定義的時候, block 如果使用 self 的時候,就會 retain 了 self 對象。
Apple 文檔 中表示 "為了 non-trivial cycles ,你應(yīng)該這樣" :
MyViewController *myController = [[MyViewController alloc] init...];
// ...
MyViewController * __weak weakMyController = myController;
myController.completionHandler = ^(NSInteger result) {
MyViewController *strongMyController = weakMyController;
if (strongMyController) {
// ...
[strongMyController dismissViewControllerAnimated:YES completion:nil];
// ...
}
else {
// Probably nothing...
}
};
首先,我覺得這個例子看起來是錯誤的。如果 block 本身被 completionHandler 屬性里面 retain 了,那么 self 如何被 delloc 和在 block 之外賦值為 nil 呢? completionHandler 屬性可以被聲明為 assign
或者 unsafe_unretained
的,來允許對象在 block 被傳遞之后被銷毀。
我不能理解這樣做的理由,如果其他對象需要這個對象(self),block 被傳遞的時候應(yīng)該 retain 對象,所以 block 應(yīng)該不被作為屬性存儲。這種情況下不應(yīng)該用 __weak
/__strong
總之,其他情況下,希望 weakSelf 變成 nil 的話,就像第二種情況解釋那么寫(在 block 之外定義一個弱應(yīng)用并且在 block 里面使用)。
還有,Apple的 "trivial block" 是什么呢。我們的理解是 trivial block 是一個不被傳送的 block ,它在一個良好定義和控制的作用域里面,weak 修飾只是為了避免循環(huán)引用。
雖然有 Kazuki Sakamoto 和 Tomohiko Furumoto) 討論的 一 些 的 在線 參考, Matt Galloway 的 (Effective Objective-C 2.0 和 Pro Multithreading and Memory Management for iOS and OS X ,大多數(shù)開發(fā)者始終沒有弄清楚概念。
在 block 內(nèi)用強引用的優(yōu)點是,搶占執(zhí)行的時候的魯棒性。看上面的三個例子,在 block 執(zhí)行的時候
1. 直接在 block 里面使用關(guān)鍵詞 self
如果 block 被屬性 retain,self 和 block 之間會有一個循環(huán)引用并且它們不會再被釋放。如果 block 被傳送并且被其他的對象 copy 了,self 在每一個 copy 里面被 retain
2. 在 block 外定義一個 __weak
的 引用到 self,并且在 block 里面使用這個弱引用
沒有循環(huán)引用的時候,block 是否被 retain 或者是一個屬性都沒關(guān)系。如果 block 被傳遞或者 copy 了,在執(zhí)行的時候,weakSelf 可能會變成 nil。
block 的執(zhí)行可以搶占,并且后來的對 weakSelf 的不同調(diào)用可以導(dǎo)致不同的值(比如,在 一個特定的執(zhí)行 weakSelf 可能賦值為 nil )
__weak typeof(self) weakSelf = self;
dispatch_block_t block = ^{
[weakSelf doSomething]; // weakSelf != nil
// preemption, weakSelf turned nil
[weakSelf doSomethingElse]; // weakSelf == nil
};
3. 在 block 外定義一個 __weak
的 引用到 self,并在在 block 內(nèi)部通過這個弱引用定義一個 __strong
的引用。
不論管 block 是否被 retain 或者是一個屬性,這樣也不會有循環(huán)引用。如果 block 被傳遞到其他對象并且被復(fù)制了,執(zhí)行的時候,weakSelf 可能被nil,因為強引用被復(fù)制并且不會變成nil的時候,我們確保對象 在 block 調(diào)用的完整周期里面被 retain了,如果搶占發(fā)生了,隨后的對 strongSelf 的執(zhí)行會繼續(xù)并且會產(chǎn)生一樣的值。如果 strongSelf 的執(zhí)行到 nil,那么在 block 不能正確執(zhí)行前已經(jīng)返回了。
__weak typeof(self) weakSelf = self;
myObj.myBlock = ^{
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf doSomething]; // strongSelf != nil
// preemption, strongSelf still not nil(搶占的時候,strongSelf 還是非 nil 的)
[strongSelf doSomethingElse]; // strongSelf != nil
}
else {
// Probably nothing...
return;
}
};
在一個 ARC 的環(huán)境中,如果嘗試用 ->
符號來表示,編譯器會警告一個錯誤:
Dereferencing a __weak pointer is not allowed due to possible null value caused by race condition, assign it to a strong variable first. (對一個 __weak 指針的解引用不允許的,因為可能在競態(tài)條件里面變成 null, 所以先把他定義成 strong 的屬性)
可以用下面的代碼展示
__weak typeof(self) weakSelf = self;
myObj.myBlock = ^{
id localVal = weakSelf->someIVar;
};
在最后【疑問】
1: 只能在 block 不是作為一個 property 的時候使用,否則會導(dǎo)致 retain cycle。
2: 當(dāng) block 被聲明為一個 property 的時候使用。
委托是 Apple 的框架里面使用廣泛的模式,同時它是一個重要的 四人幫的書“設(shè)計模式”中的模式。委托模式是單向的,消息的發(fā)送方(委托方)需要知道接收方(委托),反過來就不是了。對象之間沒有多少耦合,因為發(fā)送方只要知道它的委托實現(xiàn)了對應(yīng)的 protocol。
本質(zhì)上,委托模式只需要委托提供一些回調(diào)方法,就是說委托實現(xiàn)了一系列空返回值的方法。
不幸的是 Apple 的 API 并沒有尊重這個原則,開發(fā)者也效仿 Apple 進入了歧途。一個典型的例子是 UITableViewDelegate 協(xié)議。
一些有 void 返回類型的方法就像回調(diào)
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
- (void)tableView:(UITableView *)tableView didHighlightRowAtIndexPath:(NSIndexPath *)indexPath;
但是其他的不是
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender;
當(dāng)委托者詢問委托對象一些信息的時候,這就暗示著信息是從委托對象流向委托者,而不會反過來。 這個概念就和委托模式有些不同,它是一個另外的模式:數(shù)據(jù)源。
可能有人會說 Apple 有一個 UITableViewDataSouce protocol 來做這個(雖然使用委托模式的名字),但是實際上它的方法是用來提供真實的數(shù)據(jù)應(yīng)該如何被展示的信息的。
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView;
此外,以上兩個方法 Apple 混合了展示層和數(shù)據(jù)層,這顯的非常糟糕,但是很少的開發(fā)者感到糟糕。而且我們在這里把空返回值和非空返回值的方法都天真地叫做委托方法。
為了分離概念,我們應(yīng)該這樣做:
這個是實際的例子:
@class ZOCSignUpViewController;
@protocol ZOCSignUpViewControllerDelegate <NSObject>
- (void)signUpViewControllerDidPressSignUpButton:(ZOCSignUpViewController *)controller;
@end
@protocol ZOCSignUpViewControllerDataSource <NSObject>
- (ZOCUserCredentials *)credentialsForSignUpViewController:(ZOCSignUpViewController *)controller;
@end
@protocol ZOCSignUpViewControllerDataSource <NSObject>
@interface ZOCSignUpViewController : UIViewController
@property (nonatomic, weak) id<ZOCSignUpViewControllerDelegate> delegate;
@property (nonatomic, weak) id<ZOCSignUpViewControllerDataSource> dataSource;
@end
在上面的例子里面,委托方法需要總是有一個調(diào)用方作為第一個參數(shù),否則委托對象可能被不能區(qū)別不同的委托者的實例。此外,如果調(diào)用者沒有被傳遞到委托對象,那么就沒有辦法讓一個委托對象處理兩個不同的委托者了。所以,下面這樣的方法就是人神共憤的:
- (void)calculatorDidCalculateValue:(CGFloat)value;
默認情況下,委托對象需要實現(xiàn) protocol 的方法??梢杂?code>@required 和 @optional
關(guān)鍵字來標記方法是否是必要的還是可選的。
@protocol ZOCSignUpViewControllerDelegate <NSObject>
@required
- (void)signUpViewController:(ZOCSignUpViewController *)controller didProvideSignUpInfo:(NSDictionary *);
@optional
- (void)signUpViewControllerDidPressSignUpButton:(ZOCSignUpViewController *)controller;
@end
對于可選的方法,委托者必須在發(fā)送消息前檢查委托是否確實實現(xiàn)了特定的方法(否則會Crash):
if ([self.delegate respondsToSelector:@selector(signUpViewControllerDidPressSignUpButton:)]) {
[self.delegate signUpViewControllerDidPressSignUpButton:self];
}
有時候你可能需要重載委托方法。考慮有兩個 UIViewController 子類的情況:UIViewControllerA 和 UIViewControllerB,有下面的類繼承關(guān)系。
UIViewControllerB < UIViewControllerA < UIViewController
UIViewControllerA
conforms to UITableViewDelegate
and implements - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
.
UIViewControllerA
遵從 UITableViewDelegate
并且實現(xiàn)了 - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
.
你可能會想要提供一個和 UIViewControllerB
不同的實現(xiàn)。一個實現(xiàn)可能是這樣子的:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CGFloat retVal = 0;
if ([super respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {
retVal = [super tableView:self.tableView heightForRowAtIndexPath:indexPath];
}
return retVal + 10.0f;
}
但是如果超類(UIViewControllerA
)沒有實現(xiàn)這個方法呢?
調(diào)用過程
[super respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]
會用 NSObject 的實現(xiàn),尋找,在 self
的上下文中無疑有它的實現(xiàn),但是 app 會在下一行 Crash 并且報下面的錯:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[UIViewControllerB tableView:heightForRowAtIndexPath:]: unrecognized selector sent to instance 0x8d82820'
這種情況下我們需要來詢問特定的類實例是否可以響應(yīng)對應(yīng)的 selector。下面的代碼提供了一個小技巧:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CGFloat retVal = 0;
if ([[UIViewControllerA class] instancesRespondToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {
retVal = [super tableView:self.tableView heightForRowAtIndexPath:indexPath];
}
return retVal + 10.0f;
}
就像上面的丑陋的代碼,一個委托方法也比重載方法好。
多重委托是一個非?;A(chǔ)的概念,但是,大多數(shù)開發(fā)者對此非常不熟悉而使用 NSNotifications。就像你可能注意到的,委托和數(shù)據(jù)源是對象之間的通訊模式,但是只涉及兩個對象:委托者和委托。
數(shù)據(jù)源模式強制一對一的關(guān)系,發(fā)送者來像一個并且只是一個對象來請求信息。但是委托模式不一樣,它可以完美得有多個委托來等待回調(diào)操作。
至少兩個對象需要接收來自特定委托者的回調(diào),并且后一個需要知道所有的委托,這個方法更好的適用于分布式系統(tǒng)并且更加廣泛用于大多數(shù)軟件的復(fù)雜信息流傳遞。
多重委托可以用很多方式實現(xiàn),讀者當(dāng)然喜歡找到一個好的個人實現(xiàn),一個非常靈巧的多重委托實現(xiàn)可以參考 Luca Bernardi 在他的 LBDelegateMatrioska 的原理。
一個基本的實現(xiàn)在下面給出。Cocoa 在數(shù)據(jù)結(jié)構(gòu)中使用弱引用來避免引用循環(huán),我們使用一個類來作為委托者持有委托對象的弱引用。
@interface ZOCWeakObject : NSObject
@property (nonatomic, weak, readonly) id object;
+ (instancetype)weakObjectWithObject:(id)object;
- (instancetype)initWithObject:(id)object;
@end
@interface ZOCWeakObject ()
@property (nonatomic, weak) id object;
@end
@implementation ZOCWeakObject
+ (instancetype)weakObjectWithObject:(id)object {
return [[[self class] alloc] initWithObject:object];
}
- (instancetype)initWithObject:(id)object {
if ((self = [super init])) {
_object = object;
}
return self;
}
- (BOOL)isEqual:(id)object {
if (self == object) {
return YES;
}
if (![object isKindOfClass:[object class]]) {
return NO;
}
return [self isEqualToWeakObject:(ZOCWeakObject *)object];
}
- (BOOL)isEqualToWeakObject:(ZOCWeakObject *)object {
if (!object) {
return NO;
}
BOOL objectsMatch = [self.object isEqual:object.object];
return objectsMatch;
}
- (NSUInteger)hash {
return [self.object hash];
}
@end
一個簡單的使用 weak 對象來完成多重引用的組成部分:
@protocol ZOCServiceDelegate <NSObject>
@optional
- (void)generalService:(ZOCGeneralService *)service didRetrieveEntries:(NSArray *)entries;
@end
@interface ZOCGeneralService : NSObject
- (void)registerDelegate:(id<ZOCServiceDelegate>)delegate;
- (void)deregisterDelegate:(id<ZOCServiceDelegate>)delegate;
@end
@interface ZOCGeneralService ()
@property (nonatomic, strong) NSMutableSet *delegates;
@end
@implementation ZOCGeneralService
- (void)registerDelegate:(id<ZOCServiceDelegate>)delegate {
if ([delegate conformsToProtocol:@protocol(ZOCServiceDelegate)]) {
[self.delegates addObject:[[ZOCWeakObject alloc] initWithObject:delegate]];
}
}
- (void)deregisterDelegate:(id<ZOCServiceDelegate>)delegate {
if ([delegate conformsToProtocol:@protocol(ZOCServiceDelegate)]) {
[self.delegates removeObject:[[ZOCWeakObject alloc] initWithObject:delegate]];
}
}
- (void)_notifyDelegates {
...
for (ZOCWeakObject *object in self.delegates) {
if (object.object) {
if ([object.object respondsToSelector:@selector(generalService:didRetrieveEntries:)]) {
[object.object generalService:self didRetrieveEntries:entries];
}
}
}
}
@end
在 registerDelegate:
和 deregisterDelegate:
方法的幫助下,連接/解除組成部分很簡單:如果委托對象不需要接收委托者的回調(diào),僅僅需要'unsubscribe'.
這在一些不同的 view 等待同一個回調(diào)來更新界面展示的時候很有用:如果 view 只是暫時隱藏(但是仍然存在),它可以僅僅需要取消對回調(diào)的訂閱。
更多建議: