@(iOS知行路)[原始碼閱讀|iOS開發|NSNotification|iOS印象]
GLPubSub 是 NSNotificationCenter
的封裝,目標是簡化 iOS 開發中的釋出訂閱模式。該庫通過新增 NSObject 的 Category,使得所有物件都可以方便地使用其訂閱或釋出通知。
原始碼閱讀
- GLEvent 類,傳遞的事件物件,包含
事件名稱
、事件釋出者
、事件附帶的額外資料
/**
`GLEvent` is the event object passed into handler block.
*/
@interface GLEvent : NSObject
/**
* Name of the event
*/
@property (nonatomic, copy) NSString *name;
/**
* The object triggering the event
*/
@property (nonatomic, retain) id obj;
/**
* Additional data of the event
*/
@property (nonatomic, retain) id data;
/**
* Init `GLEvent` with `name`, `obj` and `data`.
*
* @param name Name of the event
* @param obj The object triggering the event
* @param data Additional data of the event
*
* @return Initialized `GLEvent`
*/
- (id)initWithName:(NSString *)name obj:(id)obj data:(id)data;
@end
複製程式碼
- 通過為
NSObject
新增 Category,增加訂閱/取消訂閱
、釋出
事件等方法,以及設定事件處理的處理佇列
@interface NSObject (GLPubSub)
#pragma mark - Class Methods
/**
* Set the queue to handle events. If not set or set to `nil, events will be handled on the queue where the notification is posted. Otherwise, all the events will be handled on the set queue.
*
* @param queue The queue on which to handle events.
*/
+ (void)setPubSubQueue:(NSOperationQueue *)queue;
#pragma mark - Publish Methods
/**
* Publish an event without additional data.
*
* @param name Event name to publish.
*
* @see -publish:data:
*/
- (void)publish:(NSString *)name;
/**
* Publish an event with additional data.
*
* @param name Event name to publish.
* @param data Additional data.
*
* @see -publish:
*/
- (void)publish:(NSString *)name data:(id)data;
#pragma mark - Subscribe Methods with Selector
/**
* Subscribe an event with selector.
*
* @param eventName Event name to subscribe.
* @param selector Selector to handle the event.
*
* @return An opaque object to act as the observer.
*
* @see -subscribe:obj:selector:
* @see -subscribeOnce:selector:
* @see -subscribeOnce:obj:selector:
*/
- (id)subscribe:(NSString *)eventName selector:(SEL)selector;
/**
* Subscribe an event from a specified object with selector.
*
* @param eventName Event name to subscribe.
* @param object Publisher to subscribe from.
* @param selector Selector to handle the event.
*
* @return An opaque object to act as the observer.
*
* @see -subscribe:selector:
* @see -subscribeOnce:selector:
* @see -subscribeOnce:obj:selector:
*/
- (id)subscribe:(NSString *)eventName obj:(id)obj selector:(SEL)selector;
/**
* Subscribe an event only once with selector.
*
* @param eventName Event name to subscribe.
* @param selector Selector to handle the event.
*
* @return An opaque object to act as the observer.
*
* @see -subscribe:selector:
* @see -subscribe:obj:selector:
* @see -subscribeOnce:obj:selector:
*/
- (id)subscribeOnce:(NSString *)eventName selector:(SEL)selector;
/**
* Subscribe an event from a specified object only once with selector.
*
* @param eventName Event name to subscribe.
* @param object Publisher to subscribe from.
* @param selector Selector to handle the event.
*
* @return An opaque object to act as the observer.
*
* @see -subscribe:selector:
* @see -subscribe:obj:selector:
* @see -subscribeOnce:selector:
*/
- (id)subscribeOnce:(NSString *)eventName obj:(id)obj selector:(SEL)selector;
#pragma mark - Subscribe Methods with Handler Block
/**
* Subscribe an event with handler block.
*
* @param eventName Event name to subscribe.
* @param handler Handler block to handle the event.
*
* @return An opaque object to act as the observer.
*
* @see -subscribe:obj:handler:
* @see -subscribeOnce:handler:
* @see -subscribeOnce:obj:handler:
*/
- (id)subscribe:(NSString *)eventName handler:(GLEventHandler)handler;
/**
* Subscribe an event from a specified object with handler block.
*
* @param eventName Event name to subscribe.
* @param obj Publisher to subscribe from.
* @param handler Handler block to handle the event.
*
* @return An opaque object to act as the observer.
*
* @see -subscribe:handler:
* @see -subscribeOnce:handler:
* @see -subscribeOnce:obj:handler:
*/
- (id)subscribe:(NSString *)eventName obj:(id)obj handler:(GLEventHandler)handler;
/**
* Subscribe an event only once with handler block.
*
* @param eventName Event name to subscribe.
* @param handler Handler block to handle the event.
*
* @return An opaque object to act as the observer.
*
* @see -subscribe:handler:
* @see -subscribe:obj:handler:
* @see -subscribeOnce:obj:handler:
*/
- (id)subscribeOnce:(NSString *)eventName handler:(GLEventHandler)handler;
/**
* Subscribe an event from a specified object only once with handler block.
*
* @param eventName Event name to subscribe.
* @param obj Publisher to subscribe from.
* @param handler Handler block to handle the event.
*
* @return An opaque object to act as the observer.
*
* @see -subscribe:handler:
* @see -subscribe:obj:handler:
* @see -subscribeOnce:handler:
*/
- (id)subscribeOnce:(NSString *)eventName obj:(id)obj handler:(GLEventHandler)handler;
#pragma mark - Unsubscribe Methods
/**
* Unsubscribe speficied event for current instance.
*
* @param eventName Name of event to unsubscribe.
*
* @see -unsubscribeAll
*/
- (void)unsubscribe:(NSString *)eventName;
/**
* Unsubscribe all events for current instance.
*
* @see -unsubscribe:
*/
- (void)unsubscribeAll;
@end
複製程式碼
- 釋出事件的實現
- (void)publish:(NSString *)name data:(id)data {
// 通知中心發出通知,並將事件物件放入`userInfo`
NSDictionary *userInfo = nil;
if (data != nil) {
userInfo = @{kGLPubSubDataKey: data};
}
[[NSNotificationCenter defaultCenter] postNotificationName:name object:self userInfo:userInfo];
}
複製程式碼
- 訂閱事件的實現
- (id)subscribe:(NSString *)eventName obj:(id)obj selector:(SEL)selector {
// 用於判斷`selector`的引數個數
NSMethodSignature *sig = [self methodSignatureForSelector:selector];
// The hidden arguments self and _cmd (of type SEL) are at indices 0 and 1; method-specific arguments begin at index 2
BOOL passEventObj = ([sig numberOfArguments] == 3);
__weak __typeof__(self) weakSelf = self;
return [self subscribe:eventName obj:obj handler:^(GLEvent *event) {
__strong __typeof__(weakSelf) strongSelf = weakSelf;
if (passEventObj) {
[strongSelf performSelector:selector withObject:event];
} else {
[strongSelf performSelector:selector];
}
}];
}
- (id)subscribe:(NSString *)eventName obj:(id)obj handler:(GLEventHandler)handler {
// 通知中心通過新增觀察者,並在block中生成事件進行轉發
id observer = [[NSNotificationCenter defaultCenter] addObserverForName:eventName object:obj queue:_pubSubQueue usingBlock:^(NSNotification *note) {
GLEvent *event = [[GLEvent alloc] initWithName:eventName obj:note.object data:[note.userInfo objectForKey:kGLPubSubDataKey]];
handler(event);
}];
// 新增觀察者物件至該事件的觀察者陣列,用於取消訂閱事件
NSMutableDictionary *subscriptions = (NSMutableDictionary *)objc_getAssociatedObject(self, &kGLPubSubSubscriptionsKey);
if (!subscriptions) {
subscriptions = [[NSMutableDictionary alloc] init];
objc_setAssociatedObject(self, &kGLPubSubSubscriptionsKey, subscriptions, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
NSMutableSet *observers = [subscriptions objectForKey:eventName];
if (!observers) {
observers = [[NSMutableSet alloc] init];
[subscriptions setObject:observers forKey:eventName];
}
[observers addObject:observer];
return observer;
}
複製程式碼
- 取消事件的實現
- (void)unsubscribe:(NSString *)eventName {
NSMutableDictionary *subscriptions = (NSMutableDictionary *)objc_getAssociatedObject(self, &kGLPubSubSubscriptionsKey);
if (!subscriptions)
return;
NSMutableSet *observers = [subscriptions objectForKey:eventName];
if (observers) {
// 通知中心移除所有該事件的觀察者
for (id observer in observers) {
[[NSNotificationCenter defaultCenter] removeObserver:observer];
}
// 訂閱陣列中移除該事件觀察者陣列物件
[subscriptions removeObjectForKey:eventName];
}
}
複製程式碼
附:安裝與使用
CocoaPods
- 在
Podfile
裡新增以下依賴:
pod "GLPubSub", "~> 1.0"
複製程式碼
- 執行
pod install
來安裝 GLPubSub
原始檔
如果你的專案沒有用 CocoaPods 來管理第三方依賴,你也可以直接匯入原始檔。
- 下載最新程式碼並解壓
- 匯入
NSObject+GLPubSub.h
和NSObject+GLPubSub.m
到你的工程,記得在匯入時勾選 “Copy items if needed”
因為 GLPubSub 是基於 NSNotificationCenter
並註冊在 [NSNotificationCenter defaultCenter]
的,所以 GLPubSub 也支援大部分系統通知,例如 UIApplicationDidEnterBackgroundNotification
,UIApplicationDidBecomeActiveNotification
,UITextFieldTextDidChangeNotification
等等,但是轉發的過程中會丟棄系統通知的 userInfo
欄位。
設定 PubSub 的佇列
GLPubSub 主要基於 NSNotificationCenter
的 -addObserverForName:object:queue:usingBlock:
方法。你可以呼叫 NSObject
的 +setPubSubQueue:
方法來設定傳入該方法的 queue
。
預設傳入的 queue
為 nil
,這意味著所有事件會在釋出通知的執行緒中被執行。你可以手動設定為 [NSOperationQueue maniQueue]
使得所有事件在主執行緒被觸發:
[NSObject setPubSubQueue:[NSOperationQueue mainQueue]];
複製程式碼
通過 Selector 訂閱事件
大部分時候,我們用 self
作為訂閱者:
[self subscribe:@"YourEventName" selector:@selector(yourEventHandler)];
複製程式碼
你也可以指定事件的釋出者:
[self subscribe:@"YourEventName" obj:somePublisher selector:@selector(yourEventHandler)];
複製程式碼
如果你希望你的方法只觸發一次,你可以用:
[self subscribeOnce:@"YourEventName" selector:@selector(yourEventHandler)];
複製程式碼
這樣當該事件被觸發後,就會自動取消訂閱。
你的方法可以接受一個 GLEvent
引數,該引數包含了被觸發事件的相關資訊。
@interface GLEvent : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, retain) id obj;
@property (nonatomic, retain) id data;
複製程式碼
name
是事件名,obj
是釋出者,data
是附加資訊。
通過 Block 訂閱事件
方法與上面通過 selector 訂閱的方法類似:
GLEventHandler
定義如下:
typedef void (^GLEventHandler)(GLEvent *event);
複製程式碼
所以你可以如下用 block 訂閱一個事件:
__weak __typeof__(self) weakSelf = self;
[self subscribe:UIApplicationDidEnterBackgroundNotification handler:^(GLEvent *event) {
__strong __typeof__(weakSelf) strongSelf = weakSelf;
[strongSelf appDidEnterBackground];
}];
複製程式碼
這裡的 weak 化是為了避免迴圈引用。對應於前面 selector 的方法,用 block 也有 4 種呼叫方法:
- (id)subscribe:(NSString *)eventName handler:(GLEventHandler)handler;
- (id)subscribe:(NSString *)eventName obj:(id)obj handler:(GLEventHandler)handler;
- (id)subscribeOnce:(NSString *)eventName handler:(GLEventHandler)handler;
- (id)subscribeOnce:(NSString *)eventName obj:(id)obj handler:(GLEventHandler)handler;
複製程式碼
取消訂閱
取消訂閱某個事件:
- (void)unsubscribe:(NSString *)eventName;
複製程式碼
取消訂閱所有事件:
- (void)unsubscribeAll;
複製程式碼
雖然當例項被銷燬時,存在 associated object 中的觀察者也都會被銷燬,但還是建議手動取消訂閱,如根據不同需求,在 -dealloc
或 -viewDidDisappear
方法中取消訂閱。
- (void)dealloc
{
[self unsubscribeAll];
}
複製程式碼
釋出事件
你可以簡單地釋出一個事件:
[self publish:@"YourEventName"];
複製程式碼
也可以附帶一些資料,很多時候我們會傳入一個 NSDictionary
來附帶更多結構化的資料:
[self publish:@"YourEventName" data:@{@"key": value}]
複製程式碼
迴圈引用
因為所有生成的觀察者都會被 self
引用,所以當你的 block 引用 self
的時候就會形成迴圈引用導致例項無法被釋放,所以你必須 weakify self
。強烈推薦用 libextobjc 中的 EXTScope 來做 weakify/strongify:
@weakify(self);
[self subscribe:UIApplicationDidEnterBackgroundNotification handler:^(GLEvent *event) {
@strongify(self);
[self appDidEnterBackground];
}];
複製程式碼