版本記錄
| 版本號(hào) | 時(shí)間 |
|---|---|
| V1.0 | 2017.07.17 |
前言
前面說了一下通知的一些基本問題,這里要說明的在多線程中通知的安全問題。感興趣的可以看我前面的幾篇文章。
1.通知NSNotificationCenter詳解(一)
下面我們說的就是通知的多線程安全問題
問題討論
NSNotificationCenter雖然是線程安全的,這是官方文檔給的,但是實(shí)際使用時(shí)候真的如此嗎?
通知的多線程問題其實(shí)在官方技術(shù)文檔里面已經(jīng)寫的很清楚了。
In a multithreaded application, notifications are always delivered in the thread in which the notification was posted, which may not be the same thread in which an observer registered itself.
它的意思就是
- 在多線程中,Notification在哪個(gè)線程中post,就在哪個(gè)線程中被轉(zhuǎn)發(fā),而不一定是在注冊(cè)觀察者的線程中,也就是說,Notification的發(fā)送與接收處理都是在同一個(gè)線程中。
我們?cè)诳匆幌孪旅孢@個(gè)例子。
#pragma mark - Override Base Function
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(@"current thread = %@", [NSThread currentThread]);
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:TEST_NOTIFICATION object:nil];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil userInfo:nil];
});
}
#pragma mark - Action && Notification
- (void)handleNotification:(NSNotification *)notification
{
NSLog(@"current thread = %@", [NSThread currentThread]);
NSLog(@"test notification");
}
下面看輸出結(jié)果
2017-07-17 17:16:10.259 test[865:45102] current thread = {number = 1, name = main}
2017-07-17 17:16:10.259 test[865:45174] current thread = {number = 2, name = (null)}
2017-07-17 17:16:10.259 test[865:45174] test notification
可以看到,雖然我們?cè)谥骶€程中注冊(cè)了通知的觀察者,但在全局隊(duì)列中post的Notification,并不是在主線程處理的。所以,這時(shí)候就需要注意,如果我們想在回調(diào)中處理與UI相關(guān)的操作,需要確保是在主線程中執(zhí)行回調(diào)。
這時(shí),就有一個(gè)問題了,如果我們的Notification是在二級(jí)線程中post的,如何能在主線程中對(duì)這個(gè)Notification進(jìn)行處理呢?
下面看一下官方文檔
For example, if an object running in a background thread is listening for notifications from the user interface, such as a window closing, you would like to receive the notifications in the background thread instead of the main thread. In these cases, you must capture the notifications as they are delivered on the default thread and redirect them to the appropriate thread.
- 這里講到了“重定向”,就是我們?cè)贜otification所在的默認(rèn)線程中捕獲這些分發(fā)的通知,然后將其重定向到指定的線程中。
- 一種重定向的實(shí)現(xiàn)思路是自定義一個(gè)通知隊(duì)列(注意,不是NSNotificationQueue對(duì)象,而是一個(gè)數(shù)組),讓這個(gè)隊(duì)列去維護(hù)那些我們需要重定向的Notification。我們?nèi)匀皇窍衿匠R粯尤プ?cè)一個(gè)通知的觀察者,當(dāng)Notification來了時(shí),先看看post這個(gè)Notification的線程是不是我們所期望的線程,如果不是,則將這個(gè)Notification存儲(chǔ)到我們的隊(duì)列中,并發(fā)送一個(gè)信號(hào)(signal)到期望的線程中,來告訴這個(gè)線程需要處理一個(gè)Notification。指定的線程在收到信號(hào)后,將Notification從隊(duì)列中移除,并進(jìn)行處理。
通知重定向
根據(jù)上面的思路,下面我們就看一下,通知重定向的實(shí)現(xiàn)。
#import "JJSecurityVC.h"
@interface JJSecurityVC () <NSMachPortDelegate>
@property (nonatomic, strong) NSMutableArray <NSNotification *> *notifications; // 通知隊(duì)列
@property (nonatomic, strong) NSThread *notificationThread; // 期望線程
@property (nonatomic, strong) NSLock *notificationLock; // 用于對(duì)通知隊(duì)列加鎖的鎖對(duì)象,避免線程沖突
@property (nonatomic, strong) NSMachPort *notificationPort; //消息端口,用于加到runloop中
@end
@implementation JJSecurityVC
#pragma mark - Override Base Function
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.backgroundColor = [UIColor darkGrayColor];
NSLog(@"current thread = %@", [NSThread currentThread]);
// 初始化
self.notifications = [[NSMutableArray alloc] init];
self.notificationLock = [[NSLock alloc] init];
self.notificationThread = [NSThread currentThread];
self.notificationPort = [[NSMachPort alloc] init];
self.notificationPort.delegate = self;
// 往當(dāng)前線程的run loop添加端口源
// 當(dāng)Mach消息到達(dá)而接收線程的run loop沒有運(yùn)行時(shí),則內(nèi)核會(huì)保存這條消息,直到下一次進(jìn)入run loop
[[NSRunLoop currentRunLoop] addPort:self.notificationPort
forMode:(__bridge NSString *)kCFRunLoopCommonModes];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(processNotification:) name:@"TestNotification" object:nil];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:@"TestNotification" object:nil userInfo:nil];
});
}
#pragma mark - Action && Notification
- (void)processNotification:(NSNotification *)notification
{
if ([NSThread currentThread] != _notificationThread) {
// 轉(zhuǎn)發(fā)通知到想要的線程,這里就是重定向
[self.notificationLock lock];
[self.notifications addObject:notification];
[self.notificationLock unlock];
[self.notificationPort sendBeforeDate:[NSDate date]
components:nil
from:nil
reserved:0];
}
else {
// 處理通知
NSLog(@"current thread = %@", [NSThread currentThread]);
NSLog(@"process notification");
}
}
#pragma mark - NSMachPortDelegate
- (void)handleMachMessage:(void *)msg
{
[self.notificationLock lock];
while ([self.notifications count]) {
NSNotification *notification = [self.notifications objectAtIndex:0];
[self.notifications removeObjectAtIndex:0];
[self.notificationLock unlock];
[self processNotification:notification];
[self.notificationLock lock];
};
[self.notificationLock unlock];
}
@end
下面我們就看輸出結(jié)果
2017-07-17 17:16:10.259 JJNotification[17266:1723827] current thread = <NSThread: 0x60000006eac0>{number = 1, name = main}
2017-07-17 17:16:10.265 JJNotification[17266:1723827] current thread = <NSThread: 0x60000006eac0>{number = 1, name = main}
2017-07-17 17:16:10.265 JJNotification[17266:1723827] process notification
可見都是在主線程處理的問題,實(shí)現(xiàn)了通知的重定向和轉(zhuǎn)發(fā)。
通知安全性具體分析
上面我們發(fā)送的通知發(fā)送和監(jiān)聽不在一個(gè)線程,采用重定向的思路解決了問題,但是問題的本質(zhì)是什么?下面我們分析一下通知安全問題的幾種情況。
蘋果之所以采取通知中心在同一個(gè)線程中post和轉(zhuǎn)發(fā)同一消息這一策略,應(yīng)該是出于線程安全的角度來考量的。官方文檔告訴我們,NSNotificationCenter是一個(gè)線程安全類,我們可以在多線程環(huán)境下使用同一個(gè)NSNotificationCenter對(duì)象而不需要加鎖。原文在Threading Programming Guide中。
但是事實(shí)真是如此嗎?讓我們看幾種情況。
情況1
#import "JJSafeConditionVC.h"
@interface JJSafeConditionVC ()
@property (nonatomic, strong) UIButton *senderButton;
@end
@implementation JJSafeConditionVC
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.backgroundColor = [UIColor lightGrayColor];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"notificationName" object:nil];
//UI
UIButton *senderButton = [UIButton buttonWithType:UIButtonTypeCustom];
[senderButton setTitle:@"發(fā)送通知" forState:UIControlStateNormal];
[senderButton setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
[senderButton sizeToFit];
senderButton.center = self.view.center;
[senderButton addTarget:self action:@selector(buttonDidClick) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:senderButton];
self.senderButton = senderButton;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - Action && Notification
- (void)handleNotification:(NSNotification *)noti
{
NSLog(@"handle notification ");
}
- (void)buttonDidClick
{
[[NSNotificationCenter defaultCenter] postNotificationName:@"notificationName" object:nil];
}
@end
下面看效果

點(diǎn)擊發(fā)送通知按鈕就接收到通知了,下面看輸出結(jié)果。
2017-07-17 20:55:18.257 JJNotification[1074:26357] handle notification
上面的代碼就是我們通常所做的事情:添加一個(gè)通知監(jiān)聽者,定義一個(gè)回調(diào),并在所屬對(duì)象釋放時(shí)移除監(jiān)聽者;然后在程序的某個(gè)地方post一個(gè)通知。簡(jiǎn)單明了,如果這一切都是發(fā)生在一個(gè)線程里面,或者至少dealloc方法是在-postNotificationName:的線程中運(yùn)行的(注意:NSNotification的post和轉(zhuǎn)發(fā)是同步的),那么都OK,沒有線程安全問題。但如果dealloc方法和-postNotificationName:方法不在同一個(gè)線程中運(yùn)行時(shí),會(huì)出現(xiàn)什么問題呢?
情況2
#pragma mark - Poster
@interface Poster : NSObject
@end
@implementation Poster
- (instancetype)init
{
self = [super init];
if (self)
{
[self performSelectorInBackground:@selector(postNotification) withObject:nil];
}
return self;
}
- (void)postNotification
{
[[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil];
}
@end
#pragma mark - Observer
@interface Observer : NSObject
{
Poster *_poster;
}
@property (nonatomic, assign) NSInteger i;
@end
@implementation Observer
- (instancetype)init
{
self = [super init];
if (self)
{
_poster = [[Poster alloc] init];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:TEST_NOTIFICATION object:nil];
}
return self;
}
- (void)handleNotification:(NSNotification *)notification
{
NSLog(@"handle notification begin");
sleep(1);
NSLog(@"handle notification end");
self.i = 10;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
NSLog(@"Observer dealloc");
}
@end
#pragma mark - ViewController
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
__autoreleasing Observer *observer = [[Observer alloc] init];
}
@end
程序在self.i = 10處拋出了"Thread 6: EXC_BAD_ACCESS(code=EXC_I386_GPFLT)"
經(jīng)典的內(nèi)存錯(cuò)誤,程序崩潰了。其實(shí)從輸出結(jié)果中,我們就可以看到到底是發(fā)生了什么事。我們簡(jiǎn)要描述一下:
- 當(dāng)我們注冊(cè)一個(gè)觀察者是,通知中心會(huì)持有觀察者的一個(gè)弱引用,來確保觀察者是可用的。
- 主線程調(diào)用dealloc操作會(huì)讓Observer對(duì)象的引用計(jì)數(shù)減為0,這時(shí)對(duì)象會(huì)被釋放掉。
- 后臺(tái)線程發(fā)送一個(gè)通知,如果此時(shí)Observer還未被釋放,則會(huì)用其轉(zhuǎn)出消息,并執(zhí)行回調(diào)方法。而如果在回調(diào)執(zhí)行的過程中對(duì)象被釋放了,就會(huì)出現(xiàn)上面的問題。
由于以上幾點(diǎn)因素,下面我們提出幾點(diǎn)建議:
- 盡量在一個(gè)線程中處理通知相關(guān)的操作。
- 使用帶有安全生命周期的對(duì)象,這一點(diǎn)對(duì)象單例對(duì)象來說再合適不過了。
- 注冊(cè)監(jiān)聽都時(shí),使用基于block的API。這樣我們?cè)赽lock還要繼續(xù)調(diào)用self的屬性或方法,就可以通過weak-strong的方式來處理。具體大家可以改造下上面的代碼試試是什么效果。
后記
這一篇主要說的是通知的安全問題,這里借鑒了很多別人的文章,謝謝的幫助。關(guān)于通知還有很多其他東西需要說明和講解。
