一、概述
在多线程中,无论在哪个线程注册了观察者,Notification接收和处理都是在发送Notification的线程中的。所以,当我们需要在接收到Notification后作出更新UI操作的话,就需要考虑线程的问题了,如果在子线程中发送Notification,想要在接收到Notification后更新UI的话就要切换回到主线程。先看一个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
-
(
void
)
viewDidLoad
{
[
super
viewDidLoad
]
;
NSString *
NOTIFICATION_NAME
=
@
"NOTIFICATION_NAME"
;
NSLog
(
@
"Current thread = %@"
,
[
NSThread
currentThread
]
)
;
[
[
NSNotificationCenter
defaultCenter
]
addObserver
:
self
selector
:
@
selector
(
handleNotification
:
)
name
:
NOTIFICATION_NAME
object
:
nil
]
;
dispatch_async
(
dispatch_get_global_queue
(
DISPATCH_QUEUE_PRIORITY_DEFAULT
,
0
)
,
^
{
NSLog
(
@
"Post notification,Current thread = %@"
,
[
NSThread
currentThread
]
)
;
[
[
NSNotificationCenter
defaultCenter
]
postNotificationName
:
NOTIFICATION_NAME
object
:
nil
userInfo
:
nil
]
;
}
)
;
}
-
(
void
)
handleNotification
:
(
NSNotification *
)
notification
{
NSLog
(
@
"Receive notification,Current thread = %@"
,
[
NSThread
currentThread
]
)
;
}
|
运行结果:
1
2
3
|
NotificationTest
[
23457
:
1615587
]
Current
thread
=
<
NSThread
:
0x608000078080
>
{
number
=
1
,
name
=
main
}
NotificationTest
[
23457
:
1615738
]
Post
notification,
Current
thread
=
<
NSThread
:
0x60000026c500
>
{
number
=
3
,
name
=
(
null
)
}
NotificationTest
[
23457
:
1615738
]
Receive
notification,
Current
thread
=
<
NSThread
:
0x60000026c500
>
{
number
=
3
,
name
=
(
null
)
}
|
上面我们在主线程注册观察者,在子线程发送Notification,最后Notification的接收和处理也是在子线程。
二、重定向Notification到指定线程
当然,想要在子线程发送Notification、接收到Notification后在主线程中做后续操作,可以用一个很笨的方法,在 handleNotification 里面强制切换线程:
1
2
3
4
5
6
|
-
(
void
)
handleNotification
:
(
NSNotification *
)
notification
{
NSLog
(
@
"Receive notification,Current thread = %@"
,
[
NSThread
currentThread
]
)
;
dispatch_async
(
dispatch_get_main_queue
(
)
,
^
{
NSLog
(
@
"Current thread = %@"
,
[
NSThread
currentThread
]
)
;
}
)
;
}
|
在简单情况下可以使用这种方法,但是当我们发送了多个Notification并且有多个观察者的时候,难道我们要在每个地方都手动切换线程?所以,这种方法并不是一个有效的方法。
最好的方法是在Notification所在的默认线程中捕获发送的通知,然后将其重定向到指定的线程中。关于Notification的重定向官方文档给出了一个方法:
翻译成中文:
1
|
一种重定向的实现思路是自定义一个通知队列
(不是
NSNotificationQueue对象
),让这个队列去维护那些我们需要重定向的
Notification。我们仍然是像之前一样去注册一个通知的观察者,当
Notification到达时,先看看
post这个
Notification的线程是不是我们所期望的线程,如果不是,就将这个
Notification放到我们的队列中,然后发送一个信号
(
signal
)到期望的线程中,来告诉这个线程需要处理一个
Notification。指定的线程收到这个信号
(
signal
)后,将
Notification从队列中移除,并进行后续处理。
|
我们根据官方文档中的教程测试一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
|
//
// ViewController.m
// NotificationTest
//
// Created by 胡明昊 on 2017/3/13.
// Copyright © 2017年 胡明昊. All rights reserved.
//
#import "ViewController.h"
@
interface
ViewController
(
)
<
NSMachPortDelegate
>
@
property
(
nonatomic
)
NSMutableArray *
notifications
;
// 通知队列
@
property
(
nonatomic
)
NSThread *
notificationThread
;
// 想要处理通知的线程(目标线程)
@
property
(
nonatomic
)
NSLock *
notificationLock
;
// 用于对通知队列加锁的锁对象,避免线程冲突
@
property
(
nonatomic
)
NSMachPort *
notificationPort
;
// 用于向目标线程发送信号的通信端口
@
end
@
implementation
ViewController
-
(
void
)
viewDidLoad
{
[
super
viewDidLoad
]
;
NSString *
NOTIFICATION_NAME
=
@
"NOTIFICATION_NAME"
;
NSLog
(
@
"Current thread = %@"
,
[
NSThread
currentThread
]
)
;
[
self
setUpThreadingSupport
]
;
//注册观察者
[
[
NSNotificationCenter
defaultCenter
]
addObserver
:
self
selector
:
@
selector
(
processNotification
:
)
name
:
NOTIFICATION_NAME
object
:
nil
]
;
dispatch_async
(
dispatch_get_global_queue
(
DISPATCH_QUEUE_PRIORITY_DEFAULT
,
0
)
,
^
{
//发送Notification
NSLog
(
@
"Post notification,Current thread = %@"
,
[
NSThread
currentThread
]
)
;
[
[
NSNotificationCenter
defaultCenter
]
postNotificationName
:
NOTIFICATION_NAME
object
:
nil
userInfo
:
nil
]
;
}
)
;
}
/*
在注册任何通知之前,需要先初始化属性。下面方法初始化了队列和锁定对象,保留对当前线程对象的引用,并创建一个Mach通信端口,将其添加到当前线程的运行循环中。
此方法运行后,发送到notificationPort的任何消息都会在首次运行此方法的线程的run loop中接收。如果接收线程的run loop在Mach消息到达时没有运行,则内核保持该消息,直到下一次进入run loop。接收线程的run loop将传入消息发送到端口delegate的handleMachMessage:方法。
*/
-
(
void
)
setUpThreadingSupport
{
if
(
self
.
notifications
)
{
return
;
}
self
.
notifications
=
[
[
NSMutableArray
alloc
]
init
]
;
self
.
notificationLock
=
[
[
NSLock
alloc
]
init
]
;
self
.
notificationThread
=
[
NSThread
currentThread
]
;
self
.
notificationPort
=
[
[
NSMachPort
alloc
]
init
]
;
[
self
.
notificationPort
setDelegate
:
self
]
;
[
[
NSRunLoop
currentRunLoop
]
addPort
:
self
.
notificationPort
forMode
:
(
__bridge
NSString*
)
kCFRunLoopCommonModes
]
;
}
/**
端口的代理方法
*/
-
(
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
]
;
}
-
(
void
)
processNotification
:
(
NSNotification *
)
notification
{
//判断是不是目标线程,不是则转发到目标线程
if
(
[
NSThread
currentThread
]
!=
_notificationThread
)
{
// 将Notification转发到目标线程
[
self
.
notificationLock
lock
]
;
[
self
.
notifications
addObject
:
notification
]
;
[
self
.
notificationLock
unlock
]
;
[
self
.
notificationPort
sendBeforeDate
:
[
NSDate
date
]
components
:
nil
from
:
nil
reserved
:
0
]
;
}
else
{
// 在此处理通知
NSLog
(
@
"Receive notification,Current thread = %@"
,
[
NSThread
currentThread
]
)
;
NSLog
(
@
"Process notification"
)
;
}
}
@
end
|
打印结果:
1
2
3
4
|
NotificationTest
[
24080
:
1665269
]
Current
thread
=
<
NSThread
:
0x60800006d4c0
>
{
number
=
1
,
name
=
main
}
NotificationTest
[
24080
:
1665396
]
Post
notification,
Current
thread
=
<
NSThread
:
0x60800026bc40
>
{
number
=
4
,
name
=
(
null
)
}
NotificationTest
[
24080
:
1665269
]
Receive
notification,
Current
thread
=
<
NSThread
:
0x60800006d4c0
>
{
number
=
1
,
name
=
main
}
NotificationTest
[
24080
:
1665269
]
Process
notification
|
可以看到,运行结果结果我们想要的:在子线程中发送Notification,在主线程中接收与处理Notification。
上面的实现方法也不是绝对完美的,苹果官方指出了这种方法的限制:
(1)所有线程的Notification的处理都必须通过相同的方法(processNotification :)。
(2)每个对象必须提供自己的实现和通信端口。
更好但更复杂的方法是我们自己去子类化一个NSNotificationCenter,或者单独写一个类来处理这种转发。
除了上面苹果官方给我们提供的方法外,我们还可以利用基于block的NSNotification去实现,apple 从 ios4 之后提供了带有 block 的 NSNotification。使用方式如下:
1
2
3
4
|
-
(
id
<
NSObject
>
)
addObserverForName
:
(
NSString *
)
name
object
:
(
id
)
obj
queue
:
(
NSOperationQueue *
)
queue
usingBlock
:
(
void
(
^
)
(
NSNotification *
note
)
)
block
|
其中:
- 观察者就是当前对象
- queue 定义了 block 执行的线程,nil 则表示 block 的执行线程和发通知在同一个线程
- block 就是相应通知的处理函数
这个 API 已经能够让我们方便的控制通知的线程切换。但是,这里有个问题需要注意。就是其 remove 操作。
原来的 NSNotification 的 remove 方式如下:
1
2
3
|
-
(
void
)
removeObservers
{
[
[
NSNotificationCenter
defaultCenter
]
removeObserver
:
self
name
:
POST_NOTIFICATION
object
:
nil
]
;
}
|
但是带 block 方式的 remove 便不能像上面这样处理了。其方式如下:
1
2
3
4
5
|
-
(
void
)
removeObservers
{
if(
_observer)
{
[
[
NSNotificationCenter
defaultCenter
]
removeObserver
:
_observer
]
;
}
}
|
其中 _observer 是 addObserverForName 方式的 api 返回观察者对象。这也就意味着,你需要为每一个观察者记录一个成员对象,然后在 remove 的时候依次删除。试想一下,你如果需要 10 个观察者,则需要记录 10 个成员对象,这个想想就是很麻烦,而且它还不能够方便的指定 observer 。因此,理想的做法就是自己再做一层封装,将这些细节封装起来。