Objective-C:在哪里移除NSNotification的观察者?

104

我有一个Objective-C类。在其中,我创建了一个初始化方法并设置了一个NSNotification。

//Set up NSNotification
[[NSNotificationCenter defaultCenter] addObserver:self 
                                         selector:@selector(getData)
                                             name:@"Answer Submitted"
                                           object:nil];

在这个类中,我应该在哪里设置 [[NSNotificationCenter defaultCenter] removeObserver:self]?我知道对于一个UIViewController,我可以将它添加到viewDidUnload方法中。那么如果我刚刚创建了一个Objective-C类,需要做些什么呢?


我把它放在dealloc方法中。 - onnoweb
1
当我创建Objective-C类时,dealloc方法没有自动创建,所以我可以添加它吗? - Zhen
2
是的,在ARC项目中使用dealloc是可以的,只要你不调用[super dealloc](如果你调用[super dealloc],编译器会报错)。而且,你肯定可以在dealloc中放置你的removeObserver。 - Phil
Dealloc仍然存在,但这不是它的位置,由于notificationcenter引用了您的对象,因此永远不会调用您的dealloc。 - mcfedr
@mcfedr 当NotificationCenter引用一个对象时,您提供什么? - Shamsiddin Saidov
显示剩余3条评论
14个回答

112
通常的回答是“当你不再需要通知时”。显然,这并不是一个令人满意的答案。
我建议在那些你打算用作观察者的类的dealloc方法中添加调用[notificationCenter removeObserver:self],因为这是最后一次干净地取消注册观察者的机会。但是,这只能保护您免受由于通知中心通知死对象而导致的崩溃。它无法保护您的代码免受在您的对象尚未/不再处于可以正确处理通知的状态时接收通知的影响。关于这个...请参见上面的内容。
编辑(因为答案似乎引起了比我想象的更多的评论):我想说的是:很难给出通常建议,告诉何时最好从通知中心中删除观察者,因为这取决于:
  • 您的用例(观察哪些通知?何时发送通知?)
  • 观察者的实现(何时准备好接收通知?何时不再准备好?)
  • 观察者的预期生存时间(它是否与某些其他对象捆绑在一起,比如视图或视图控制器?)
  • ...
所以,我能想出的最好的通用建议是:为了保护您的应用程序免受至少一种可能的故障,请在dealloc中进行removeObserver:操作,因为这是您可以干净地这样做的最后一点(在对象的生命周期中)。这并不意味着“只需将移除延迟到dealloc被调用,一切都会好起来”。相反,一旦对象不再准备好(或需要)接收通知,就立即删除观察者。那才是确切的正确时刻。不幸的是,由于我不知道上面提到的任何问题的答案,我甚至无法猜测那个时刻会是什么时候。

您始终可以安全地多次removeObserver:一个对象(对于给定的观察者,除了第一次调用之外的所有调用都将不起作用)。因此:先考虑在dealloc中再次执行它以确保安全,但首要任务是在适当的时刻(由您的用例确定)执行它。


4
使用ARC时这不安全,可能会导致内存泄漏。请参考此讨论:http://www.cocoabuilder.com/archive/cocoa/311831-arc-and-dealloc.html。 - MobileMon
3
@MobileMon,您提供的链接文章似乎支持了我的观点。请问我有什么遗漏吗? - Dirk
1
@MobileMon -- 是的。我希望我的回答能够表达出这一点。在dealloc中移除观察者只是为了防止由于稍后访问已释放对象而导致应用程序崩溃的最后一道防线。但通常注销观察者的适当位置是在其他地方(而且通常是在对象的生命周期早期)。我并不是在说“嘿,在dealloc中做就可以了,一切都会好起来”。 - Dirk
这会导致内存泄漏!因为通知中心仍然持有对你的对象的引用,所以dealloc永远不会被调用。 - mcfedr
@mcfedr:不,NSNotificationCenter 不会保留使用 addObserver:selector:name:object: 添加的对象。虽然文档没有明确说明这一点,但是你可以从类似“在 notificationObserver 或任何在 addObserver:selector:name:object: 中指定的对象被释放之前,请务必调用 removeObserver: 或 removeObserver:name:object:”这样的句子中推断出来(或者你可以运行一个小测试程序并验证其行为,就像我刚刚做的那样)。 - Dirk
显示剩余7条评论

40

2
也许他们不会向观察者发送消息,但我相信他们会保持对它们的强引用。在这种情况下,所有观察者都将留在内存中并产生泄漏。如果我错了,请纠正我。 - fir
6
关联的文档详细介绍了这个。简而言之,它是一个弱引用。 - Sebastian
当然,如果您仍然需要保留引用这些对象并且不想再收听通知,则仍然需要它。 - TheEye

39

注意:这已经经过测试,百分之百有效。

Swift

override func viewWillDisappear(animated: Bool){
    super.viewWillDisappear(animated)
    
    if self.navigationController!.viewControllers.contains(self) == false  //any other hierarchy compare if it contains self or not
    {
        // the view has been removed from the navigation stack or hierarchy, back is probably the cause
        // this will be slow with a large stack however.
        
        NSNotificationCenter.defaultCenter().removeObserver(self)
    }
}

呈现视图控制器:

override func viewWillDisappear(animated: Bool){
    super.viewWillDisappear(animated)
    
    if self.isBeingDismissed()  //presented view controller
    {
        // remove observer here
        NSNotificationCenter.defaultCenter().removeObserver(self)
    }
}

Objective-C

iOS 6.0及以上版本中,最好在viewWillDisappear方法中移除观察者,因为viewDidUnload方法已被弃用。

 [[NSNotificationCenter defaultCenter] removeObserver:observerObjectHere];

当视图已从导航栈或层次结构中移除时,许多情况下最好移除观察者。

- (void)viewWillDisappear:(BOOL)animated{
 if (![[self.navigationController viewControllers] containsObject: self]) //any other hierarchy compare if it contains self or not
    {
        // the view has been removed from the navigation stack or hierarchy, back is probably the cause
        // this will be slow with a large stack however.
        
        [[NSNotificationCenter defaultCenter] removeObserver:observerObjectHere];
    }
}

弹出视图控制器:

- (void)viewWillDisappear:(BOOL)animated{
    if ([self isBeingDismissed] == YES) ///presented view controller
    {
        // remove observer here
        [[NSNotificationCenter defaultCenter] removeObserver:observerObjectHere];
    }
}

8
除非控制器不希望在其视图显示时接收通知(例如重新加载tableView),否则仍可能需要通知。 - wcochran
2
@wcochran 在 viewWillAppear: 中自动重新加载/刷新 - Richard
1
如果控制器是通过 pushViewController 呈现的,那么将 removeObserver 调用放在 viewWillDisappear 中肯定是正确的方法。如果你把它们放在 dealloc 中,那么 dealloc 将永远不会被调用 -- 至少在我的经验中是这样... - Christopher King
如何在Swift中不使用类型转换[self.navigationController viewControllers]来执行相同的操作? - raw3d
EDIT建议检查控制器的导航层次结构是@wcochran问题的完美解决方案。非常有帮助。 - wuf810
显示剩余2条评论

25
如果将观察者添加到一个视图控制器中,我强烈建议在viewWillAppear中添加它,并在viewWillDisappear中移除它。

我很好奇,@RickiG:为什么你建议在视图控制器中使用viewWillAppearviewWillDisappear - Isaac Overacker
2
@IsaacOveracker 有几个原因:你的设置代码(例如loadView和viewDidLoad)可能会导致通知被触发,而你的控制器需要在显示之前反映出来。如果你像这样做,就有几个好处。目前,当你决定“离开”控制器时,你不关心通知,它们不会在控制器被推出屏幕时引起逻辑问题等。当控制器处于非活动状态时,有一些特殊情况下应该接收通知,我想你不能这样做。但是像这样的事件应该放在你的模型中。 - RickiG
1
@IsaacOveracker,使用ARC实现dealloc来取消通知订阅会很奇怪。 - RickiG
4
在我所尝试过的方法中,使用iOS7时,这是在处理UIViewController时注册/删除观察者的最佳方法。唯一的问题是,在许多情况下,当使用UINavigationController并将另一个UIViewController推入堆栈时,您不希望移除观察者。解决方案:您可以通过调用[self isBeingDismissed]来检查视图将要消失时是否正在弹出VC。 - lekksi
从导航控制器中弹出视图控制器可能不会立即调用dealloc。如果在初始化命令中添加了观察者,则返回到视图控制器可能会导致多个通知。 - Jonathan Lin

20
-(void) dealloc {
      [[NSNotificationCenter defaultCenter] removeObserver:self];
      [super dealloc];
}

4
我会调整这些指令的顺序... 在 [super dealloc] 之后使用 self 让我感到有些紧张...(即使接收器实际上不太可能以任何方式取消引用指针,但你永远不知道他们如何实现 NSNotificationCenter - Dirk
1
Dirk是正确的 - 这是不正确的。 [super dealloc]必须始终是您的dealloc方法的最后语句。它会销毁您的对象;运行后,您将不再拥有有效的self。/cc @Dirk - jscs
39
如果在 iOS 5+ 上使用 ARC,我认为不再需要 [super dealloc] - pixelfreak
3
@pixelfreak,根据ARC的规定,不允许调用[super dealloc]方法来释放内存。 - tapmonkey
在使用ARC的情况下,dealloc中是否仍需要调用removeObserver方法? - Alexandre
显示剩余9条评论

7
一般来说,我把它放在dealloc方法中。

6

在Swift中使用deinit,因为dealloc不可用:

deinit {
    ...
}

Swift文档:

析构器会在类实例被销毁前立即调用。您可以使用deinit关键字编写析构器,就像使用init关键字编写初始化器一样。析构器仅适用于类类型。

通常情况下,在实例被销毁时不需要手动清理。但是,当您使用自己的资源时,可能需要执行一些额外的清理工作。例如,如果您创建了一个自定义类来打开文件并向其中写入一些数据,则可能需要在类实例被销毁之前关闭文件。


错误!您不应该在 deinit 中取消订阅,因为这是无用的。当实例从内存中清除时,iOS 会自动取消订阅,然后调用 deinit,所以这是无用的。 - Alexander Volkov
@AlexanderVolkov 请记住,这个答案是5年前的,当时Swift还很新。那时它是解决方案,但现在已经改变了。 - Morten Holmgaard
不是的。如果调用了deinit,则iOS会自动取消订阅观察者。这在ObjC和Swift中都是相同的,它是系统相关的,而不是语言相关的。也许我错了,但请提供文档参考。 - Alexander Volkov

5

在我的看法中,下面的代码在 ARC 中没有意义:

- (void)dealloc
{
      [[NSNotificationCenter defaultCenter] removeObserver:self];
      [super dealloc];
}

在iOS 6中,没有必要在viewDidUnload中移除观察者,因为它已被弃用。总的来说,我总是在viewDidDisappear中处理它。但是,这也取决于您的要求,就像@Dirk所说的那样。

许多人仍在编写比iOS6旧的iOS版本的代码.... :-) - lnafziger
在ARC中,您可以使用此代码,但不包括 [super dealloc] 这一行;您可以在此处查看更多信息:http://developer.apple.com/library/ios/#releasenotes/ObjectiveC/RN-TransitioningToARC/Introduction/Introduction.html#//apple_ref/doc/uid/TP40011226-CH1-SW14 - Alex
1
如果您有一个普通的NSObject成为通知观察者,那该怎么办?在这种情况下,您会使用dealloc吗? - qix

5
*编辑:本建议适用于iOS <= 5(即使在这种情况下,您也应该在viewWillAppear中添加并在viewWillDisappear中删除 - 但是如果由于某种原因您已经在viewDidLoad中添加了观察者,则建议适用)
如果您已将观察者添加到viewDidLoad中,则应在deallocviewDidUnload中都将其删除。否则,当viewDidLoad在内存警告后被调用时,它会再次添加,从而导致重复添加。在iOS 6中,这不是必需的,因为viewDidUnload已被弃用且不会被调用(因为视图不再自动取消加载)。

2
欢迎来到StackOverflow。请查看MarkDown FAQ(问题/答案编辑框旁边的问号图标)。使用Markdwon将提高您的答案的可用性。 - marko

4
我认为我找到了一个可靠的答案!因为上面的答案含糊不清,看起来相互矛盾。我查阅了菜谱和编程指南。
首先,在`viewWillAppear:`中使用`addObserver:`风格,在`viewWillDisappear:`中使用`removeObserver:`风格对我不起作用(我进行了测试),因为我在子视图控制器中发布通知以执行父视图控制器中的代码。如果我在同一个视图控制器中发布和监听通知,我才会使用这种风格。
我最信任的答案是在《iOS Programming: Big Nerd Ranch Guide 4th》中找到的。我相信BNR的人,因为他们有iOS培训中心,他们不仅仅是写另一本菜谱。准确无误可能是他们的最佳利益。
BNR示例1:在`init:`中使用`addObserver:`,在`dealloc:`中使用`removeObserver:`。
BNR示例2:在`awakeFromNib:`中使用`addObserver:`,在`dealloc:`中使用`removeObserver:`。
...当在`dealloc:`中移除观察者时,他们不使用`[super dealloc];`。
我希望这可以帮助下一个人...
我正在更新此帖子,因为Apple现在几乎完全采用Storyboards,因此上述内容可能不适用于所有情况。重要的是(也是我首次添加此帖子的原因)要注意您的`viewWillDisappear:`是否被调用。当应用程序进入后台时,对我而言它并没有被调用。

很难说这是否正确,因为上下文很重要。已经提到过几次了,但在ARC上下文中(现在是唯一的上下文),dealloc几乎没有意义。而且dealloc何时被调用也是不可预测的——viewWillDisappear更容易控制。顺便说一句:如果您的子视图需要向其父视图传达信息,则委托模式似乎是更好的选择。 - RickiG

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接