對(duì)象之間的通訊

2018-08-12 21:19 更新

對(duì)象間的通訊

對(duì)象之間需要通信,這也是所有軟件的基礎(chǔ)。再非凡的軟件也需要通過對(duì)象通信來完成復(fù)雜的目標(biāo)。本章將深入討論一些設(shè)計(jì)概念,以及如何依據(jù)這些概念來設(shè)計(jì)出良好的架構(gòu)。

Blocks

Blocks 是 Objective-C 版本的 lambda 或者 closure(閉包)。

使用 block 定義異步接口:

- (void)downloadObjectsAtPath:(NSString *)path
                   completion:(void(^)(NSArray *objects, NSError *error))completion;

當(dāng)你定義一個(gè)類似上面的接口的時(shí)候,盡量使用一個(gè)單獨(dú)的 block 作為接口的最后一個(gè)參數(shù)。把需要提供的數(shù)據(jù)和錯(cuò)誤信息整合到一個(gè)單獨(dú) block 中,比分別提供成功和失敗的 block 要好。

以下是你應(yīng)該這樣做的原因:

  • 通常這成功處理和失敗處理會(huì)共享一些代碼(比如讓一個(gè)進(jìn)度條或者提示消失);
  • Apple 也是這樣做的,與平臺(tái)一致能夠帶來一些潛在的好處;
  • block 通常會(huì)有多行代碼,如果不是在最后一個(gè)參數(shù)的話會(huì)打破調(diào)用點(diǎn);
  • 使用多個(gè) block 作為參數(shù)可能會(huì)讓調(diào)用看起來顯得很笨拙,并且增加了復(fù)雜性。

看上面的方法,完成處理的 block 的參數(shù)很常見:第一個(gè)參數(shù)是調(diào)用者希望獲取的數(shù)據(jù),第二個(gè)是錯(cuò)誤相關(guān)的信息。這里需要遵循以下兩點(diǎn):

  • objects 不為 nil,則 error 必須為 nil
  • objects 為 nil,則 error 必須不為 nil

因?yàn)檎{(diào)用者更關(guān)心的是實(shí)際的數(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)問題。

深入 Blocks

一些關(guān)鍵點(diǎn):

  • block 是在棧上創(chuàng)建的
  • block 可以復(fù)制到堆上
  • block 有自己的私有的棧變量(以及指針)的常量復(fù)制
  • 可變的棧上的變量和指針必須用 __block 關(guān)鍵字聲明

如果 block 沒有在其他地方被保持,那么它會(huì)隨著棧生存并且當(dāng)棧幀(stack frame)返回的時(shí)候消失。當(dāng)在棧上的時(shí)候,一個(gè) block 對(duì)訪問的任何內(nèi)容不會(huì)有影響。如果 block 需要在棧幀返回的時(shí)候存在,它們需要明確地被復(fù)制到堆上,這樣,block 會(huì)像其他 Cocoa 對(duì)象一樣增加引用計(jì)數(shù)。當(dāng)它們被復(fù)制的時(shí)候,它會(huì)帶著它們的捕獲作用域一起,retain 他們所有引用的對(duì)象。如果一個(gè) block指向一個(gè)棧變量或者指針,那么這個(gè)block初始化的時(shí)候它會(huì)有一份聲明為 const 的副本,所以對(duì)它們賦值是沒用的。當(dāng)一個(gè) block 被復(fù)制后,__block 聲明的棧變量的引用被復(fù)制到了堆里,復(fù)制之后棧上的以及產(chǎn)生的堆上的 block 都會(huì)引用這個(gè)堆上的變量。

用 LLDB 來展示 block 是這樣子的:

最重要的事情是 __block 聲明的變量和指針在 block 里面是作為顯示操作真實(shí)值/對(duì)象的結(jié)構(gòu)來對(duì)待的。

block 在 Objective-C 里面被當(dāng)作一等公民對(duì)待:他們有一個(gè) isa 指針,一個(gè)類也是用 isa 指針來訪問 Objective-C 運(yùn)行時(shí)來訪問方法和存儲(chǔ)數(shù)據(jù)的。在非 ARC 環(huán)境肯定會(huì)把它搞得很糟糕,并且懸掛指針會(huì)導(dǎo)致 Crash。__block 僅僅對(duì) block 內(nèi)的變量起作用,它只是簡單地告訴 block:

嗨,這個(gè)指針或者原始的類型依賴它們?cè)诘臈?。?qǐng)用一個(gè)棧上的新變量來引用它。我是說,請(qǐng)對(duì)它進(jìn)行雙重解引用,不要 retain 它。 謝謝,哥們。

如果在定義之后但是 block 沒有被調(diào)用前,對(duì)象被釋放了,那么 block 的執(zhí)行會(huì)導(dǎo)致 Crash。 __block 變量不會(huì)在 block 中被持有,最后... 指針、引用、解引用以及引用計(jì)數(shù)變得一團(tuán)糟。

self 的循環(huán)引用

當(dāng)使用代碼塊和異步分發(fā)的時(shí)候,要注意避免引用循環(huán)。 總是使用 weak 引用會(huì)導(dǎo)致引用循環(huán)。 此外,把持有 blocks 的屬性設(shè)置為 nil (比如 self.completionBlock = nil) 是一個(gè)好的實(shí)踐。它會(huì)打破 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];
}];

多個(gè)語句的例子:

__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 里面的三種不同情況。

  1. 直接在 block 里面使用關(guān)鍵詞 self
  2. 在 block 外定義一個(gè) __weak 的 引用到 self,并且在 block 里面使用這個(gè)弱引用
  3. 在 block 外定義一個(gè) __weak 的 引用到 self,并在在 block 內(nèi)部通過這個(gè)弱引用定義一個(gè) __strong 的引用。

1. 直接在 block 里面使用關(guān)鍵詞 self

如果我們直接在 block 里面用 self 關(guān)鍵字,對(duì)象會(huì)在 block 的定義時(shí)候被 retain,(實(shí)際上 block 是 copied 但是為了簡單我們可以忽略這個(gè))。一個(gè) const 的對(duì) self 的引用在 block 里面有自己的位置并且它會(huì)影響對(duì)象的引用計(jì)數(shù)。如果 block 被其他 class 或者/并且傳送過去了,我們可能想要 retain self 就像其他被 block 使用的對(duì)象,從他們需要被block執(zhí)行

dispatch_block_t completionBlock = ^{
    NSLog(@"%@", self);
}

MyViewController *myController = [[MyViewController alloc] init...];
[self presentViewController:myController
                   animated:YES
                 completion:completionHandler];

不是很麻煩的事情。但是, 當(dāng) block 被 self 在一個(gè)屬性 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` 的強(qiáng)引用,可能會(huì)導(dǎo)致循環(huán)引用)

所以可以用 weak 修飾

2. 在 block 外定義一個(gè) __weak 的 引用到 self,并且在 block 里面使用這個(gè)弱引用

這樣會(huì)避免循環(huán)引用,也是我們通常在 block 已經(jīng)被 self 的 property 屬性里面 retain 的時(shí)候會(huì)做的。

__weak typeof(self) weakSelf = self;
self.completionHandler = ^{
    NSLog(@"%@", weakSelf);
};

MyViewController *myController = [[MyViewController alloc] init...];
[self presentViewController:myController
                   animated:YES
                 completion:self.completionHandler];

這個(gè)情況下 block 沒有 retain 對(duì)象并且對(duì)象在屬性里面 retain 了 block 。所以這樣我們能保證了安全的訪問 self。 不過糟糕的是,它可能被設(shè)置成 nil 的。問題是:如果和讓 self 在 block 里面安全地被銷毀。

舉個(gè)例子, block 被一個(gè)對(duì)象復(fù)制到了另外一個(gè)(比如 myControler)作為屬性賦值的結(jié)果。之前的對(duì)象在可能在被復(fù)制的 block 有機(jī)會(huì)執(zhí)行被銷毀。

下面的更有意思。

3. 在 block 外定義一個(gè) __weak 的 引用到 self,并在在 block 內(nèi)部通過這個(gè)弱引用定義一個(gè) __strong 的引用

你可能會(huì)想,首先,這是避免 retain cycle 警告的一個(gè)技巧。然而不是,這個(gè)到 self 的強(qiáng)引用在 block 的執(zhí)行時(shí)間 被創(chuàng)建。當(dāng) block 在定義的時(shí)候, block 如果使用 self 的時(shí)候,就會(huì) retain 了 self 對(duì)象。

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...
    }
};

首先,我覺得這個(gè)例子看起來是錯(cuò)誤的。如果 block 本身被 completionHandler 屬性里面 retain 了,那么 self 如何被 delloc 和在 block 之外賦值為 nil 呢? completionHandler 屬性可以被聲明為 assign 或者 unsafe_unretained 的,來允許對(duì)象在 block 被傳遞之后被銷毀。

我不能理解這樣做的理由,如果其他對(duì)象需要這個(gè)對(duì)象(self),block 被傳遞的時(shí)候應(yīng)該 retain 對(duì)象,所以 block 應(yīng)該不被作為屬性存儲(chǔ)。這種情況下不應(yīng)該用 __weak/__strong

總之,其他情況下,希望 weakSelf 變成 nil 的話,就像第二種情況解釋那么寫(在 block 之外定義一個(gè)弱應(yīng)用并且在 block 里面使用)。

還有,Apple的 "trivial block" 是什么呢。我們的理解是 trivial block 是一個(gè)不被傳送的 block ,它在一個(gè)良好定義和控制的作用域里面,weak 修飾只是為了避免循環(huán)引用。

雖然有 Kazuki Sakamoto 和 Tomohiko Furumoto) 討論的 在線 參考, Matt Galloway 的 (Effective Objective-C 2.0Pro Multithreading and Memory Management for iOS and OS X ,大多數(shù)開發(fā)者始終沒有弄清楚概念。

在 block 內(nèi)用強(qiáng)引用的優(yōu)點(diǎn)是,搶占執(zhí)行的時(shí)候的魯棒性??瓷厦娴娜齻€(gè)例子,在 block 執(zhí)行的時(shí)候

1. 直接在 block 里面使用關(guān)鍵詞 self

如果 block 被屬性 retain,self 和 block 之間會(huì)有一個(gè)循環(huán)引用并且它們不會(huì)再被釋放。如果 block 被傳送并且被其他的對(duì)象 copy 了,self 在每一個(gè) copy 里面被 retain

2. 在 block 外定義一個(gè) __weak 的 引用到 self,并且在 block 里面使用這個(gè)弱引用

沒有循環(huán)引用的時(shí)候,block 是否被 retain 或者是一個(gè)屬性都沒關(guān)系。如果 block 被傳遞或者 copy 了,在執(zhí)行的時(shí)候,weakSelf 可能會(huì)變成 nil。

block 的執(zhí)行可以搶占,并且后來的對(duì) weakSelf 的不同調(diào)用可以導(dǎo)致不同的值(比如,在 一個(gè)特定的執(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 外定義一個(gè) __weak 的 引用到 self,并在在 block 內(nèi)部通過這個(gè)弱引用定義一個(gè) __strong 的引用。

不論管 block 是否被 retain 或者是一個(gè)屬性,這樣也不會(huì)有循環(huán)引用。如果 block 被傳遞到其他對(duì)象并且被復(fù)制了,執(zhí)行的時(shí)候,weakSelf 可能被nil,因?yàn)閺?qiáng)引用被復(fù)制并且不會(huì)變成nil的時(shí)候,我們確保對(duì)象 在 block 調(diào)用的完整周期里面被 retain了,如果搶占發(fā)生了,隨后的對(duì) strongSelf 的執(zhí)行會(huì)繼續(xù)并且會(huì)產(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(搶占的時(shí)候,strongSelf 還是非 nil 的)
      [strongSelf doSomethingElse]; // strongSelf != nil
    }
    else {
        // Probably nothing...
        return;
    }
};

在一個(gè) ARC 的環(huán)境中,如果嘗試用 ->符號(hào)來表示,編譯器會(huì)警告一個(gè)錯(cuò)誤:

Dereferencing a __weak pointer is not allowed due to possible null value caused by race condition, assign it to a strong variable first. (對(duì)一個(gè) __weak 指針的解引用不允許的,因?yàn)榭赡茉诟倯B(tài)條件里面變成 null, 所以先把他定義成 strong 的屬性)

可以用下面的代碼展示

__weak typeof(self) weakSelf = self;
myObj.myBlock =  ^{
    id localVal = weakSelf->someIVar;
};

在最后【疑問】

  • 1: 只能在 block 不是作為一個(gè) property 的時(shí)候使用,否則會(huì)導(dǎo)致 retain cycle。

  • 2: 當(dāng) block 被聲明為一個(gè) property 的時(shí)候使用。

  • Case 3: 和并發(fā)執(zhí)行有關(guān)。當(dāng)涉及異步的服務(wù)的時(shí)候,block 可以在之后被執(zhí)行,并且不會(huì)發(fā)生關(guān)于 self 是否存在的問題。

委托和數(shù)據(jù)源

委托是 Apple 的框架里面使用廣泛的模式,同時(shí)它是一個(gè)重要的 四人幫的書“設(shè)計(jì)模式”中的模式。委托模式是單向的,消息的發(fā)送方(委托方)需要知道接收方(委托),反過來就不是了。對(duì)象之間沒有多少耦合,因?yàn)榘l(fā)送方只要知道它的委托實(shí)現(xiàn)了對(duì)應(yīng)的 protocol。

本質(zhì)上,委托模式只需要委托提供一些回調(diào)方法,就是說委托實(shí)現(xiàn)了一系列空返回值的方法。

不幸的是 Apple 的 API 并沒有尊重這個(gè)原則,開發(fā)者也效仿 Apple 進(jìn)入了歧途。一個(gè)典型的例子是 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)委托者詢問委托對(duì)象一些信息的時(shí)候,這就暗示著信息是從委托對(duì)象流向委托者,而不會(huì)反過來。 這個(gè)概念就和委托模式有些不同,它是一個(gè)另外的模式:數(shù)據(jù)源。

可能有人會(huì)說 Apple 有一個(gè) UITableViewDataSouce protocol 來做這個(gè)(雖然使用委托模式的名字),但是實(shí)際上它的方法是用來提供真實(shí)的數(shù)據(jù)應(yīng)該如何被展示的信息的。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView;

此外,以上兩個(gè)方法 Apple 混合了展示層和數(shù)據(jù)層,這顯的非常糟糕,但是很少的開發(fā)者感到糟糕。而且我們?cè)谶@里把空返回值和非空返回值的方法都天真地叫做委托方法。

為了分離概念,我們應(yīng)該這樣做:

  • 委托模式:事件發(fā)生的時(shí)候,委托者需要通知委托
  • 數(shù)據(jù)源模式: 委托方需要從數(shù)據(jù)源對(duì)象拉取數(shù)據(jù)

這個(gè)是實(shí)際的例子:

@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

在上面的例子里面,委托方法需要總是有一個(gè)調(diào)用方作為第一個(gè)參數(shù),否則委托對(duì)象可能被不能區(qū)別不同的委托者的實(shí)例。此外,如果調(diào)用者沒有被傳遞到委托對(duì)象,那么就沒有辦法讓一個(gè)委托對(duì)象處理兩個(gè)不同的委托者了。所以,下面這樣的方法就是人神共憤的:

- (void)calculatorDidCalculateValue:(CGFloat)value;

默認(rèn)情況下,委托對(duì)象需要實(shí)現(xiàn) protocol 的方法??梢杂?code>@required 和 @optional 關(guān)鍵字來標(biāo)記方法是否是必要的還是可選的。

@protocol ZOCSignUpViewControllerDelegate <NSObject>
@required
- (void)signUpViewController:(ZOCSignUpViewController *)controller didProvideSignUpInfo:(NSDictionary *);
@optional
- (void)signUpViewControllerDidPressSignUpButton:(ZOCSignUpViewController *)controller;
@end

對(duì)于可選的方法,委托者必須在發(fā)送消息前檢查委托是否確實(shí)實(shí)現(xiàn)了特定的方法(否則會(huì)Crash):

if ([self.delegate respondsToSelector:@selector(signUpViewControllerDidPressSignUpButton:)]) {
    [self.delegate signUpViewControllerDidPressSignUpButton:self];
}

繼承

有時(shí)候你可能需要重載委托方法??紤]有兩個(gè) UIViewController 子類的情況:UIViewControllerA 和 UIViewControllerB,有下面的類繼承關(guān)系。

UIViewControllerB < UIViewControllerA < UIViewController

UIViewControllerA conforms to UITableViewDelegate and implements - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath.

UIViewControllerA 遵從 UITableViewDelegate 并且實(shí)現(xiàn)了 - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath.

你可能會(huì)想要提供一個(gè)和 UIViewControllerB 不同的實(shí)現(xiàn)。一個(gè)實(shí)現(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)沒有實(shí)現(xiàn)這個(gè)方法呢?

調(diào)用過程

[super respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]

會(huì)用 NSObject 的實(shí)現(xiàn),尋找,在 self 的上下文中無疑有它的實(shí)現(xiàn),但是 app 會(huì)在下一行 Crash 并且報(bào)下面的錯(cuò):

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[UIViewControllerB tableView:heightForRowAtIndexPath:]: unrecognized selector sent to instance 0x8d82820'

這種情況下我們需要來詢問特定的類實(shí)例是否可以響應(yīng)對(duì)應(yīng)的 selector。下面的代碼提供了一個(gè)小技巧:


- (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;
}

就像上面的丑陋的代碼,一個(gè)委托方法也比重載方法好。

多重委托

多重委托是一個(gè)非?;A(chǔ)的概念,但是,大多數(shù)開發(fā)者對(duì)此非常不熟悉而使用 NSNotifications。就像你可能注意到的,委托和數(shù)據(jù)源是對(duì)象之間的通訊模式,但是只涉及兩個(gè)對(duì)象:委托者和委托。

數(shù)據(jù)源模式強(qiáng)制一對(duì)一的關(guān)系,發(fā)送者來像一個(gè)并且只是一個(gè)對(duì)象來請(qǐng)求信息。但是委托模式不一樣,它可以完美得有多個(gè)委托來等待回調(diào)操作。

至少兩個(gè)對(duì)象需要接收來自特定委托者的回調(diào),并且后一個(gè)需要知道所有的委托,這個(gè)方法更好的適用于分布式系統(tǒng)并且更加廣泛用于大多數(shù)軟件的復(fù)雜信息流傳遞。

多重委托可以用很多方式實(shí)現(xiàn),讀者當(dāng)然喜歡找到一個(gè)好的個(gè)人實(shí)現(xiàn),一個(gè)非常靈巧的多重委托實(shí)現(xiàn)可以參考 Luca Bernardi 在他的 LBDelegateMatrioska 的原理。

一個(gè)基本的實(shí)現(xiàn)在下面給出。Cocoa 在數(shù)據(jù)結(jié)構(gòu)中使用弱引用來避免引用循環(huán),我們使用一個(gè)類來作為委托者持有委托對(duì)象的弱引用。

@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

一個(gè)簡單的使用 weak 對(duì)象來完成多重引用的組成部分:

@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: 方法的幫助下,連接/解除組成部分很簡單:如果委托對(duì)象不需要接收委托者的回調(diào),僅僅需要'unsubscribe'.

這在一些不同的 view 等待同一個(gè)回調(diào)來更新界面展示的時(shí)候很有用:如果 view 只是暫時(shí)隱藏(但是仍然存在),它可以僅僅需要取消對(duì)回調(diào)的訂閱。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)