1. 程式人生 > >iOS NSNotificationCenter與自定義通知的封裝(PSSNotificationCenter)

iOS NSNotificationCenter與自定義通知的封裝(PSSNotificationCenter)

前言

作為iOS開發者,大家應該都使用過系統通知(NSNotificationCenter),無非就是三步,1. 註冊通知,2.傳送通知,3.銷燬觀察者,我在這裡就不多解釋了;。如果忘記銷燬觀察者,ios9之前是會崩潰的。因此我就有了自己實現全域性一對多分發通知的想法,於是封裝了PSSNotificationCenter

系統通知如何使用

通知的使用為3步:

  1. 註冊通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(receiveSysNoti) name:@"target" object:nil];

- (void)receiveSysNoti:(NSNotification *)noti {
    NSLog(@"%@", [[NSThread currentThread] isMainThread] ? @"收到系統通知:主執行緒" : @"收到系統通知:子執行緒");
}
  1. 在需要的時候傳送通知
[[NSNotificationCenter defaultCenter] postNotificationName:@"target" object:nil];
  1. 移出觀察者
    如果iOS9之前,不移除觀察者是會崩潰的,而且崩潰不會有斷點,如果你繼承了友盟,甚至不會給你列印崩潰資訊,很難找,所以務必要記得。
	// 移除目標所有通知
	[[NSNotificationCenter defaultCenter] removeObserver:self];
	// 移除目標對應通知
    [[NSNotificationCenter defaultCenter] removeObserver:self name:@"target" object:nil];

系統通知還可以使用block的方式:

但是使用block方式會有記憶體問題,即便用上面兩個方法移除,block本身依舊被NotificationCenter持有,當傳送通知時依然會執行;請看程式碼

// 註冊通知
[[NSNotificationCenter defaultCenter] addObserverForName:@"block" object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
	NSLog(@"block方式受到系統通知");
}];

// 點選登出通知
- (IBAction)click3:(id)sender {
    // [[NSNotificationCenter defaultCenter] removeObserver:self];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:@"block" object:nil];
}
// 點擊發送通知
- (IBAction)clickSysNoti:(id)sender {
    [[NSNotificationCenter defaultCenter] postNotificationName:@"block" object:nil];
}

上述程式碼,首先點擊發送通知,控制檯會列印block方式受到系統通,然後點選登出通知,再點擊發送通知,依然會列印;如果退出當前控制器,並且再進入一次控制器,點擊發送通知,控制器會列印2次,證明block沒有被銷燬(知道怎麼正確登出的同學歡迎留言告知)
那麼為什麼會出現這種情況呢?

首先,在使用block方式註冊通知的時候,我們只是傳了block,並沒有傳Observer監聽者NSNotificationCenter直接持有block,而我們用的兩個登出方式都是針對target-action方式的;我並沒有找到block方式的通知的登出方法,所以如果你使用通知,我建議使用target-action方式,或者用我封裝的PSSNotificationCenter

系統通知的原理,以及出現的常見問題

使用target-action方式的原理大概是這個樣子的:

在ios9之前,有些類是不支援weak指標的,所以使用的是unsafe_unretained型別的指標;顧名思義,unsafe_unretained的意思就是不安全的,不會對物件持有的指標weak所指向的地址被登出時,指標自動指向nil,向nil指標傳送訊息是不會崩潰的;而unsafe_unretained指標指向的記憶體被銷燬時,是不會指向nil的,從而導致野指標,所以ios9之前是需要登出的;
當你呼叫登出方法時,NSNotificationCenter會把指標指向nil,避免出現野指標

記憶體大概是這個樣子的

NSNotificationCenter 內部根據每個通知名稱name都引用著一個數組(這個陣列應該是弱引用陣列NSHashTable),數組裡存著每個註冊者的target-action,當發通知時([[NSNotificationCenter defaultCenter] postNotificationName:@"target" object:nil];),會根據NotificationName找到對應的陣列,然後便利數組裡的target-action,達到批量分發的功能。

使用block方式大概是這個樣子的:

註冊通知時,不需要你傳Observer,NSNotificationCenter根據NotificationName持有一個數組,然後把block放到這個陣列中,當你呼叫postNotification時,根據NotificationName找到對應陣列並遍歷呼叫;(但是會出現block不會被釋放的問題)

子執行緒傳送通知時:

在傳送時都是遍歷target-actionblock呼叫方法,因此,在子執行緒傳送通知,方法呼叫也是發生在子執行緒,也就是說,接收也是預設在子執行緒的,所以如果你需要在接收通知時重新整理UI,建議跳轉到主執行緒哦。

target-action方式需要宣告單獨的方法接收,如果支援ios9之前的版本還得登出,真是好麻煩。block方式呢,還會出現記憶體問題,API提供的較少,因此我決定,封裝一款自己的通知 - PSSNotificationCenter

PSSNotificationCenter 自己封裝的全域性通知

封裝這個框架,首先,不能存在記憶體問題;其次,用法必須簡單容易理解。因此我選擇block方式進行封裝;

  • 使用方法:不需要登出,也不存在記憶體問題,子執行緒發通知也會自動跳轉到主執行緒接收通知,是執行緒安全的。
// 註冊通知
	// 不帶NotificationName,字用預設的name kDefaultNotificationName。observer必須傳,而且要是NSObject
    [[PSSNotificationCenter defaultCenter] addEvent:^(id info) {
        NSLog(@"%@", info);
    } observer:self.obj_1];
    
    [[PSSNotificationCenter defaultCenter] addEvent:^(id info) {
        NSLog(@"%@", info);
    } observer:self];
    
    [[PSSNotificationCenter defaultCenter] addEvent:^(id info) {
        
    } eventName:@"PSS" observer:self];

// 傳送通知
	[[PSSNotificationCenter defaultCenter] postDefaultNotification:@"11111"];
    [[PSSNotificationCenter defaultCenter] postNotificationByName:@"PSS" info:@"附帶資訊"];

實現原理及原始碼

就像系統的NSNotificationCenter一樣,我們也需要一個單利PSSNotificationCenter,然後說一說我們記憶體方案:
記憶體
上圖中的EventSet是通過執行時動態給VC1(也可以說是監聽者,NSObject型別)新增的屬性

PSSNotificationCenter下有一個Dictionary,結構大致如下:

@property (nonatomic, strong) NSMutableDictionary <NSString *, NSMapTable<NSString *, PSSEventSet *> *> *eventDict;
@{
      @"通知的名字_1": @{ // 這個字典是NSMapTable,可以對持有的Value弱引用
              @"觀察者記憶體地址生成的字串_1": PSSEventSet物件,
              @"觀察者記憶體地址生成的字串_2": PSSEventSet物件,
              },
      @"通知的名字_1": @{ // 這個字典是NSMapTable
              @"觀察者記憶體地址生成的字串_1": PSSEventSet物件,
              @"觀察者記憶體地址生成的字串_2": PSSEventSet物件,
              },
};

NSMapTable: 類似於字典,可以對value進行弱引用;

PSSEventSet: 動態新增給觀察者的屬性

@interface PSSEventSet : NSObject
@property (nonatomic, strong) NSMutableArray<PSSBlockObject *> *blockObjectArray;
@end

PSSBlockObject: 用於持有block

typedef void(^PSSNotiEvent)(id info);

@interface PSSBlockObject : NSObject

@property (nonatomic, copy) PSSNotiEvent eventHandler;

@end

** .h中暴露的方法 **

#define kDefaultNotificationName @"PSSDefaultNotification"
@class PSSEventSet;
@interface PSSNotificationCenter : NSObject

+ (instancetype)defaultCenter;

- (void)addEvent:(PSSNotiEvent)event observer:(NSObject *)observer;
- (void)addEvent:(PSSNotiEvent)event eventName:(NSString *)eventName observer:(NSObject *)observer;

/// info: 傳值
- (void)postNotificationByName:(NSString *)name info:(id)info;
- (void)postDefaultNotification:(id)info;
/// 移出對應通知事件
- (void)removeNotificationName:(NSString *)name;
/// 移出所有通知下的 observer對應的事件(不給此observer傳送事件了)
- (void)removeObserver:(NSObject *)observer;
- (void)removeObserverByEventSet:(PSSEventSet *)eventSet;
/// 移出對應通知下,對應observer的事件
- (void)removeNotificationName:(NSString *)name observer:(NSObject *)observer;
/// 移出所有事件
- (void)removeAllNoti;

@end

覺得有用給個star唄