iOS單例中 Block 回調一對多設計

起因:今天在開發(fā)過程中,小伙伴告訴我,我寫的全局音樂播放器(單例模式實現)在多個地方同時接收監(jiān)聽狀態(tài) Block 時,除了最后一次接收有效以外,其它調用的地方都無法正常執(zhí)行 Block 里代碼。

需求背景

?播放器是通過代理委托來告知外部當前展示的 VC 類關于音樂播放信息,但需求迭代過程中新增了一個App全局頁面展示的音樂懸浮窗,懸浮窗需要實時監(jiān)聽當前播放器的播放狀態(tài)并更新 view ,而且保持原有 VC 類遵循播放器的代理并更新 view。原本通過代理委托一對一實現的場景被打破,現在要滿足一對多的場景。產品最終要實現下面的效果:


效果圖

解決方案選擇

首先想到的第一個方案是,監(jiān)聽播放狀態(tài)改用 Notification 通知。
?使用通知,實現起來簡單,可以滿足想要的結果,但也意味著外部每一處需要監(jiān)聽的播放狀態(tài),若是后續(xù)有更多的需要監(jiān)聽狀態(tài),肯定不能每一處都要添加Notification 通知。當初設計單例播放器的目的,就是 高內斂、低耦合,用通知的話實現方式太不優(yōu)雅,肯定不能讓小伙伴在所有要監(jiān)聽狀態(tài)的地方都添加通知代碼,決定放棄這個方案。
第二個方案,播放器單例代理改為一對多代理。
?原本播放器單例是通過代理一對一的形式實現的,如果是讓單例的代理實現一對多呢?想起了之前看到的文章:多播代理,主要參考 iOS多播代理 文章??戳讼露嗖ゴ韺崿F目標,發(fā)現與自己的業(yè)務場景多少有些出入。播放器通過代理實現一對一的初衷,為了只讓展示在用戶前的 ViewController 去作為代理類去響應播放器的代理調用,UINavigationController 堆棧中被 topViewController 壓在下面的 ViewController 沒有必要繼續(xù)去響應播放器的代理調用。再加上若采用該方案,意味著音樂播放器整體的消息傳遞方式要發(fā)生變動,工作量巨大。多播代理的方案也放棄了。
?回到現在已有的實現中,小伙伴在多處地方已經添加代碼去接收這個 block,而且接收的對象都是普通對象,播放器本身是一個單例,分析下來,問題有了眉頭——單例中的 block 若在外部多處接收,block 本身已有的代碼塊會被覆蓋,最終就會造成前言提到的問題,只有最后一次可以接收到 block 的消息,其余全部失效。
?如果是讓單例中的 block 也能夠像多播代理實現一對多呢?
在網上搜羅了一番,發(fā)現了這篇文章 一個關于單例的 Block 回調設計 ,采用了 NSMapTable + NSPointerFunctionsWeakMemory 的組合方案來實現。

設計思路

整理了上面文章最終的實現思路:

  1. block 持有者為單例中的 NSMapTable ,而非由注冊 block 回調對象 observer 持有,并且單例播放器本身僅維護 block 映射關系;
  2. 為了解決 block 自動釋放問題,由 NSMapTable 來持有 block ,通過給 observer 綁定一個對象 DeallocWatcher ,利用 objc_setAssociatedObject 把 observer 與綁定對象 DeallocWatcher 進行關聯(lián),以此監(jiān)聽 DeallocWatcher 的 dealloc 釋放,從而間接得知 observer 釋放時機,達到 block 自動釋放目的。
    文章中提到的間接監(jiān)聽釋放時機,在 ReactiveCocoa 中的 onExit 方法也是類似的思路來實現。

實現步驟

  1. 創(chuàng)建 NSMapTable 映射表
// key為 observer 注冊對象,用 weak 屬性表示不持有 observer,僅指向 observer
// value 為 observer 注冊的 block 回調,使用 strong 屬性意味著映射表要持有 block
self.blockTable = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsWeakMemory valueOptions:NSPointerFunctionsStrongMemory capacity:1];
  1. 聲明 observer 要綁定的對象 DeallocWatcher 類實現方法
@interface DeallocWatcher: NSObject

@property (nonatomic, copy) dispatch_block_t deallocCallback;

- (instancetype)initWithDeallocCallback:(dispatch_block_t)callback;

@end

@implementation DeallocWatcher

- (instancetype)initWithDeallocCallback:(dispatch_block_t)callback {
    self = [super init];
    if (self) {
        self.deallocCallback = callback;
    }
    return self;
}
// 關鍵代碼,當該對象釋放觸發(fā) dealloc 方法時,會去執(zhí)行 callback 回調
- (void)dealloc
{
    if (self.deallocCallback) {
        self.deallocCallback();
    }
}

@end
  1. 給 observer 添加關聯(lián)綁定對象 watch,并添加至映射表中。
- (void)addObserver:(id)observer callback:(isPlayingChangedBlock)callback {
// 這里要打破循環(huán)引用,因為關聯(lián)代碼中 watch 被 observer 持有,而 watch 中的 callback 去調用了 observer
    __weak typeof (observer) weakObserver = observer;
  DeallocWatcher *watch = [[DeallocWatcher alloc] initWithDeallocCallback:^{
    __strong typeof (observer) strongObserver = weakObserver;
    [self removeObserver:strongObserver];
  }];
  [self.blockTable setObject:callback forKey:observer];
// 將 observer 與 watch 進行綁定關聯(lián),key 則使用 observer 指針指向的內存地址
  objc_setAssociatedObject(observer,(__bridge const void * _Nonnull)([NSString stringWithFormat:@"%p", observer]), watch, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
  1. observer 自動釋放(后續(xù)更正:此處代碼已經沒必要執(zhí)行。原因:當 watch 的 block 中執(zhí)行 remove 方法時,這里 observer 已經釋放了,手動關聯(lián)的watch對象只是觸發(fā)了dealloc回調,映射表中因為以observer為key弱引用存儲,也在表中刪除了該key值以及value值)
- (void)removeObserver:(id)observer {
  [self.blockTable removeObjectForKey:observer];
  objc_setAssociatedObject(observer, (__bridge const void * _Nonnull)([NSString stringWithFormat:@"%p", observer]), nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
  1. 映射表中 block 的觸發(fā)調用方法。
    下面代碼就是項目中是否正在播放狀態(tài)的成員變量 set 方法。每當 isPlaying 發(fā)生變化時,都會將映射表中的 block 執(zhí)行一遍,最終達到單例中的 block 實現一對多的目的。
- (void)setIsPlaying:(BOOL)isPlaying{
  _isPlaying = isPlaying;

  [[[self.blockTable objectEnumerator] allObjects] enumerateObjectsUsingBlock:^(isPlayingChangedBlock callback, NSUInteger idx, BOOL * _Nonnull stop) {
    callback(isPlaying);
  }];
}

應小伙伴要求demo查看,我花了十來分鐘寫了個簡易demo,有興趣的可以去下載了。鏈接:https://github.com/RoganZheng/Block-one-to-many-demo-in-a-single-case

后續(xù)文章,凡是涉及到實踐代碼的內容,我會注意提供相關代碼demo鏈接。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容