NSNotificationCenter

轉(zhuǎn)載自南峰子的技術(shù)博客

一個NSNotificationCenter對象(通知中心)提供了在程序中廣播消息的機制,它實質(zhì)上就是一個通知分發(fā)表毕箍。這個分發(fā)表負(fù)責(zé)維護(hù)為各個通知注冊的觀察者弛房,并在通知到達(dá)時,去查找相應(yīng)的觀察者霉晕,將通知轉(zhuǎn)發(fā)給他們進(jìn)行處理庭再。本文主要了整理了一下NSNotificationCenter的使用及需要注意的一些問題,并提出了一些未解決的問題牺堰,希望能在此得到解答拄轻。獲取通知中心每個程序都會有一個默認(rèn)的通知中心。為此伟葫,NSNotificationCenter提供了一個類方法來獲取這個通知中心:+ (NSNotificationCenter *)defaultCenter獲取了這個默認(rèn)的通知中心對象后恨搓,我們就可以使用它來處理通知相關(guān)的操作了,包括注冊觀察者斧抱,移除觀察者、發(fā)送通知等弄抬。通常如果不是出于必要,我們一般都使用這個默認(rèn)的通知中心宪郊,而不自己創(chuàng)建維護(hù)一個通知中心掂恕。添加觀察者如果想讓對象監(jiān)聽某個通知弛槐,則需要在通知中心中將這個對象注冊為通知的觀察者。早先乎串,NSNotificationCenter提供了以下方法來添加觀察者:- (void)addObserver:(id)notificationObserver? ? ? ? ? selector:(SEL)notificationSelector? ? ? ? ? ? ? name:(NSString *)notificationName? ? ? ? ? ? object:(id)notificationSender這個方法帶有4個參數(shù)店枣,分別指定了通知的觀察者叹誉、處理通知的回調(diào)鸯两、通知名及通知的發(fā)送對象桂对。這里需要注意幾個問題:notificationObserver不能為nil鸠匀。notificationSelector回調(diào)方法有且只有一個參數(shù)(NSNotification對象)。如果notificationName為nil宅此,則會接收所有的通知(如果notificationSender不為空爬范,則接收所有來自于notificationSender的所有通知)。如代碼清單1所示青瀑。如果notificationSender為nil,則會接收所有notificationName定義的通知枝嘶;否則哑诊,接收由notificationSender發(fā)送的通知。監(jiān)聽同一條通知的多個觀察者,在通知到達(dá)時缴饭,它們執(zhí)行回調(diào)的順序是不確定的骆莹,所以我們不能去假設(shè)操作的執(zhí)行會按照添加觀察者的順序來執(zhí)行。對于以上幾點峭火,我們來重點關(guān)注一下第3條智嚷。以下代碼演示了當(dāng)我們的notificationName設(shè)置為nil時卖丸,通知的監(jiān)聽情況。代碼清單1:添加一個Observer盏道,其中notificationName為nil@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:nil object:nil];[[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil];}- (void)handleNotification:(NSNotification *)notification{NSLog(@"notification = %@", notification.name);}@end運行后的輸出結(jié)果如下:notification = TestNotificationnotification = UIWindowDidBecomeVisibleNotificationnotification = UIWindowDidBecomeKeyNotificationnotification = UIApplicationDidFinishLaunchingNotificationnotification = _UIWindowContentWillRotateNotificationnotification = _UIApplicationWillAddDeactivationReasonNotificationnotification = _UIApplicationDidRemoveDeactivationReasonNotificationnotification = UIDeviceOrientationDidChangeNotificationnotification = _UIApplicationDidRemoveDeactivationReasonNotificationnotification = UIApplicationDidBecomeActiveNotification可以看出稍浆,我們的對象基本上監(jiān)聽了測試程序啟動后的所示消息衅枫。當(dāng)然朗伶,我們很少會去這么做。而對于第4條论皆,使用得比較多的場景是監(jiān)聽UITextField的修改事件,通常我們在一個ViewController中感凤,只希望去監(jiān)聽當(dāng)前視圖中的UITextField修改事件粒督,而不希望監(jiān)聽所有UITextField的修改事件,這時我們就可以將當(dāng)前頁面的UITextField對象指定為notificationSender族跛。在iOS 4.0之后锐墙,NSNotificationCenter為了跟上時代,又提供了一個以block方式實現(xiàn)的添加觀察者的方法姐仅,如下所示:- (id)addObserverForName:(NSString *)name? ? ? ? ? ? ? ? ? ? ? ? ? ? object:(id)obj? ? ? ? ? ? ? ? ? ? ? ? ? ? queue:(NSOperationQueue *)queue? ? ? ? ? ? ? ? ? ? ? ? usingBlock:(void (^)(NSNotification *note))block大家第一次看到這個方法時是否會有這樣的疑問:觀察者呢?參數(shù)中并沒有指定具體的觀察者劳翰,那誰是觀察者呢馒疹?實際上,與前一個方法不同的是颖变,前者使用一個現(xiàn)存的對象作為觀察者,而這個方法會創(chuàng)建一個匿名的對象作為觀察者(即方法返回的id對象)马胧,這個匿名對象會在指定的隊列(queue)上去執(zhí)行我們的block衔峰。這個方法的優(yōu)點在于添加觀察者的操作與回調(diào)處理操作的代碼更加緊湊,不需要拼命滾動鼠標(biāo)就能直接找到處理代碼威彰,簡單直觀穴肘。這個方法也有幾個地方需要注意:name和obj為nil時的情形與前面一個方法是相同的。如果queue為nil豹缀,則消息是默認(rèn)在post線程中同步處理盈咳,即通知的post與轉(zhuǎn)發(fā)是在同一線程中鱼响;但如果我們指定了操作隊列组底,情況就變得有點意思了,我們一會再講江滨。block塊會被通知中心拷貝一份(執(zhí)行copy操作)厌均,以在堆中維護(hù)一個block對象,直到觀察者被從通知中心中移除。所以擒悬,應(yīng)該特別注意在block中使用外部對象稻艰,避免出現(xiàn)對象的循環(huán)引用,這個我們在下面將舉例說明僧凤。如果一個給定的通知觸發(fā)了多個觀察者的block操作元扔,則這些操作會在各自的Operation Queue中被并發(fā)執(zhí)行。所以我們不能去假設(shè)操作的執(zhí)行會按照添加觀察者的順序來執(zhí)行吻氧。該方法會返回一個表示觀察者的對象咏连,記得在不用時釋放這個對象。下面我們重點說明一下第2點和第3點振惰。關(guān)于第2點垄懂,當(dāng)我們指定一個Operation Queue時,不管通知是在哪個線程中post的草慧,都會在Operation Queue所屬的線程中進(jìn)行轉(zhuǎn)發(fā)漫谷,如代碼清單2所示:代碼清單2:在指定隊列中接收通知@implementation ViewController- (void)viewDidLoad {? ? [super viewDidLoad];? ? [[NSNotificationCenter defaultCenter] addObserverForName:TEST_NOTIFICATION object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {NSLog(@"receive thread = %@", [NSThread currentThread]);? ? }];? ? dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{NSLog(@"post thread = %@", [NSThread currentThread]);[[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil];? ? });}@end在這里,我們在主線程里添加了一個觀察者碟婆,并指定在主線程隊列中去接收處理這個通知惕稻。然后我們在一個全局隊列中post了一個通知。我們來看下輸出結(jié)果:post thread ={number = 2, name = (null)}receive thread ={number = 1, name = main}可以看到公给,消息的post與接收處理并不是在同一個線程中。如上面所提到的淌铐,如果queue為nil,則消息是默認(rèn)在post線程中同步處理狰挡,大家可以試一下释涛。對于第3點,由于使用的是block它匕,所以需要注意的就是避免引起循環(huán)引用的問題窖认,如代碼清單3所示:代碼清單3:block引發(fā)的循環(huán)引用問題@interface Observer : NSObject@property (nonatomic, assign) NSInteger i;@property (nonatomic, weak) idobserver;

@end

@implementation Observer

- (instancetype)init

{

self = [super init];

if (self)

{

NSLog(@"Init Observer");

// 添加觀察者

_observer =? [[NSNotificationCenter defaultCenter] addObserverForName:TEST_NOTIFICATION object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {

NSLog(@"handle notification");

// 使用self

self.i = 10;

}];

}

return self;

}

@end

#pragma mark - ViewController

@implementation ViewController

- (void)viewDidLoad {

[super viewDidLoad];

[self createObserver];

// 發(fā)送消息

[[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil];

}

- (void)createObserver {

Observer *observer = [[Observer alloc] init];

}

@end

運行后的輸出如下:

Init Observer

handle notification

我們可以看到createObserver中創(chuàng)建的observer并沒有被釋放扑浸。所以,使用 – addObserverForName:object:queue:usingBlock:一定要注意這個問題础嫡。

移除觀察者

與注冊觀察者相對應(yīng)的酝惧,NSNotificationCenter為我們提供了兩個移除觀察者的方法。它們的定義如下:

- (void)removeObserver:(id)notificationObserver

- (void)removeObserver:(id)notificationObserver name:(NSString *)notificationName object:(id)notificationSender

前一個方法會將notificationObserver從通知中心中移除巫财,這樣notificationObserver就無法再監(jiān)聽任何消息哩陕。而后一個會根據(jù)三個參數(shù)來移除相應(yīng)的觀察者。

這兩個方法也有幾點需要注意:

由于注冊觀察者時(不管是哪個方法)葵礼,通知中心會維護(hù)一個觀察者的弱引用并鸵,所以在釋放對象時扔涧,要確保移除對象所有監(jiān)聽的通知届谈。否則弯汰,可能會導(dǎo)致程序崩潰或一些莫名其妙的問題。

對于第二個方法曙搬,如果notificationName為nil鸽嫂,則會移除所有匹配notificationObserver和notificationSender的通知,同理notificationSender也是一樣的橡娄。而如果notificationName和notificationSender都為nil癣籽,則其效果就與第一個方法是一樣的了。大家可以試一下瓶籽。

最有趣的應(yīng)該是這兩個方法的使用時機埂材。–removeObserver:適合于在類的dealloc方法中調(diào)用,這樣可以確保將對象從通知中心中清除茬暇;而在viewWillDisappear:這樣的方法中寡喝,則適合于使用-removeObserver:name:object:方法,以避免不知情的情況下移除了不應(yīng)該移除的通知觀察者巧骚。例如格二,假設(shè)我們的ViewController繼承自一個類庫的某個ViewController類(假設(shè)為SKViewController吧),可能SKViewController自身也監(jiān)聽了某些通知以執(zhí)行特定的操作沧奴,但我們使用時并不知道长窄。如果直接在viewWillDisappear:中調(diào)用–removeObserver:纲菌,則也會把父類監(jiān)聽的通知也給移除疮绷。

關(guān)于注冊監(jiān)聽者,還有一個需要注意的問題是椅贱,每次調(diào)用addObserver時只冻,都會在通知中心重新注冊一次,即使是同一對象監(jiān)聽同一個消息女器,而不是去覆蓋原來的監(jiān)聽住诸。這樣,當(dāng)通知中心轉(zhuǎn)發(fā)某一消息時丧诺,如果同一對象多次注冊了這個通知的觀察者奄薇,則會收到多個通知,如代碼清單4所示:

代碼清單4:同一對象多次注冊同一消息

@implementation ViewController

- (void)viewDidLoad {

[super viewDidLoad];

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:TEST_NOTIFICATION object:nil];

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:TEST_NOTIFICATION object:nil];

[[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil];

}

- (void)handleNotification:(NSNotification *)notification

{

NSLog(@"notification = %@", notification.name);

}

@end

其輸出結(jié)果如下所示:

notification = TestNotification

notification = TestNotification

可以看到對象處理了兩次通知呵晚。所以饵隙,如果我們需要在viewWillAppear監(jiān)聽一個通知時沮脖,一定要記得在對應(yīng)的viewWillDisappear里面將觀察者移除,否則就可能會出現(xiàn)上面的情況勺届。

最后免姿,再特別重點強調(diào)的非常重要的一點是,在釋放對象前胚膊,一定要記住如果它監(jiān)聽了通知澜掩,一定要將它從通知中心移除。如果是用 – addObserverForName:object:queue:usingBlock:肩榕,也記得一定得移除這個匿名觀察者株汉。說白了就一句話,添加和移除要配對出現(xiàn)乔妈。

post消息

注冊了通知觀察者,我們便可以隨時隨地的去post一個通知了(當(dāng)然勃刨,如果閑著沒事股淡,也可以不注冊觀察者唯灵,post通知隨便玩,只是沒人理睬罷了)埠帕。NSNotificationCenter提供了三個方法來post一個通知,如下所示:

- postNotification:

– postNotificationName:object:

– postNotificationName:object:userInfo:

我們可以根據(jù)需要指定通知的發(fā)送者(object)并附帶一些與通知相關(guān)的信息(userInfo)叁巨,當(dāng)然這些發(fā)送者和userInfo可以封裝在一個NSNotification對象中琐驴,由- postNotification:來發(fā)送绝淡。注意,- postNotification:的參數(shù)不能為空牢酵,否則會引發(fā)一個異常馍乙,如下所示:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSNotificationCenter postNotification:]: notification is nil'

每次post一個通知時垫释,通知中心都會去遍歷一下它的分發(fā)表撑瞧,然后將通知轉(zhuǎn)發(fā)給相應(yīng)的觀察者。

另外订咸,通知的發(fā)送與處理是同步的酬诀,在某個地方post一個消息時,會等到所有觀察者對象執(zhí)行完處理操作后父叙,才回到post的地方肴裙,繼續(xù)執(zhí)行后面的代碼。如代碼清單5所示:

代碼清單5:通知的同步處理

@implementation ViewController

- (void)viewDidLoad {

[super viewDidLoad];

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:TEST_NOTIFICATION object:nil];

[[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil];

NSLog(@"continue");

}

- (void)handleNotification:(NSNotification *)notification

{

NSLog(@"handle notification");

}

@end

運行后輸出結(jié)果是:

handle notification

continue

一些思考

翻了好些資料鲸匿,還有兩個問題始終沒有明確的答案带欢。

首先就是通知中心是如何維護(hù)觀察者對象的烤惊。可以明確的是渡贾,添加觀察者時雄右,通知中心沒有對觀察者做retain操作,即不會使觀察者的引用計數(shù)加1囤屹。那通知中心維護(hù)的是觀察者的weak引用呢還是unsafe_unretained引用呢逢渔?

個人認(rèn)為可能是unsafe_unretained的引用,因為我們知道如果是weak引用智厌,其所指的對象被釋放后,這個引用會被置成nil铣鹏。而實際情況是通知中心還會給這個對象發(fā)送消息吝沫,并引發(fā)一個異常。而如果向nil發(fā)送一個消息是不會導(dǎo)致異常的。

另外脊髓,我們知道NSNotificationCenter實現(xiàn)的是觀察者模式,而且通常情況下消息在哪個線程被post恭朗,就在哪個線程被轉(zhuǎn)發(fā)依疼。而從上面的描述可以發(fā)現(xiàn), -addObserverForName:object:queue:usingBlock:添加的匿名觀察者可以在指定的隊列中處理通知膀值,那它的實現(xiàn)機制是什么呢误辑?

小結(jié)

在我們的應(yīng)用程序中,一個大的話題就是兩個對象之間如何通信翘狱。我們需要根據(jù)對象之間的關(guān)系來確定采用哪一種通信方式砰苍。對象之間的通信方式主要有以下幾種:

直接方法調(diào)用

Target-Action

Delegate

回調(diào)(block)

KVO

通知

一般情況下赚导,我們可以根據(jù)以下兩點來確定使用哪種方式:

通信對象是一對一的還是一對多的

對象之間的耦合度,是強耦合還是松耦合

Objective-C中的通知由于其廣播性及松耦合性寒屯,非常適合于大的范圍內(nèi)對象之間的通信(模塊與模塊,或一些框架層級)处面。通知使用起來非常方便菩掏,也正因為如此,所以容易導(dǎo)致濫用。所以在使用前還是需要多想想斯稳,是否有更好的方法來實現(xiàn)我們所需要的對象間通信迹恐。畢竟,通知機制會在一定程度上會影響到程序的性能憎茂。

對于使用NSNotificationCenter锤岸,最后總結(jié)一些小建議:

在需要的地方使用通知。

注冊的觀察者在不使用時一定要記得移除拳氢,即添加和移除要配對出現(xiàn)晓猛。

盡可能遲地去注冊一個觀察者戒职,并盡可能早將其移除,這樣可以改善程序的性能洪燥。因為捧韵,每post一個通知,都會是遍歷通知中心的分發(fā)表蒙兰,確保通知發(fā)給每一個觀察者。

記住通知的發(fā)送和處理是在同一個線程中搜变。

使用-addObserverForName:object:queue:usingBlock:務(wù)必處理好內(nèi)存問題挠他,避免出現(xiàn)循環(huán)引用。

NSNotificationCenter是線程安全的贸呢,但并不意味著在多線程環(huán)境中不需要關(guān)注線程安全問題拢军。不恰當(dāng)?shù)氖褂萌匀粫l(fā)線程問題。

最后猜谚,“@葉孤城___”葉大大在微博中推薦了幾篇文章赌渣,即參考中的4-7昌犹,值得細(xì)讀一下斜姥。

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市缚忧,隨后出現(xiàn)的幾起案子杈笔,更是在濱河造成了極大的恐慌蒙具,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件持钉,死亡現(xiàn)場離奇詭異篱昔,居然都是意外死亡,警方通過查閱死者的電腦和手機空执,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門脆烟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人驼抹,你說我怎么就攤上這事拜鹤。” “怎么了明也?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵温数,是天一觀的道長蜻势。 經(jīng)常有香客問我,道長够傍,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任挠铲,我火速辦了婚禮冕屯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘拂苹。我一直安慰自己安聘,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布醋寝。 她就那樣靜靜地躺著搞挣,像睡著了一般。 火紅的嫁衣襯著肌膚如雪音羞。 梳的紋絲不亂的頭發(fā)上囱桨,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天嗅绰,我揣著相機與錄音舍肠,去河邊找鬼搀继。 笑死,一個胖子當(dāng)著我的面吹牛翠语,可吹牛的內(nèi)容都是我干的叽躯。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼肌括,長吁一口氣:“原來是場噩夢啊……” “哼点骑!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起谍夭,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤黑滴,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后紧索,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體袁辈,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年珠漂,在試婚紗的時候發(fā)現(xiàn)自己被綠了晚缩。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡媳危,死狀恐怖荞彼,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情待笑,我是刑警寧澤卿泽,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站滋觉,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏齐邦。R本人自食惡果不足惜椎侠,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望措拇。 院中可真熱鬧我纪,春花似錦、人聲如沸丐吓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽券犁。三九已至术健,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間粘衬,已是汗流浹背荞估。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工咳促, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人勘伺。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓跪腹,卻偏偏與公主長得像,于是被迫代替她去往敵國和親飞醉。 傳聞我的和親對象是個殘疾皇子冲茸,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容