当观察对象被“释放”时,Cocoa绑定和KVO取消注册观察者

时间:2013-11-20 16:34:31

标签: cocoa cocoa-bindings key-value-observing dealloc

当观察对象获得dealloced时,如何取消注册观察者?

当观察对象被解除分配时,cocoa绑定如何处理这种情况?

通过使用手动KVO,我必须删除dealloc对象之前的观察(removeObserver)... Cocoa绑定如何处理这个(停止观察被观察对象的dealloc)?

3 个答案:

答案 0 :(得分:5)

2017年更新

正如@GregBrown在评论中指出的那样,2013年的原始答案在2017年不起作用。我假设原始答案在2013年确实有效,因为我的做法是不经测试就回答,但是我不再使用任何代码。

那你怎么在2017年解决这个问题呢?最简单的答案是混合,有些人会发现矛盾,但不一定是在使用积木时。以下是一个快速概念验证,其中包含以下警告:

  • 这不是线程安全的。考虑如果两个或多个线程同时执行代码可能会发生什么。标准技术将解决这个问题。

  • 效率不是考虑因素!例如,您可能希望每个类调用dealloc一次,并在每个实例关联对象中保留观察者/密钥路径列表。

  • 此代码支持自动删除,您无法手动选择删除观察者。你可能希望改变它。

代码:

@implementation AutoRemovedKVO

typedef void (*DeallocImp)(id, SEL);

+ (void)forTarget:(NSObject *)target
      addObserver:(NSObject *)observer
       forKeyPath:(NSString *)keyPath
          options:(NSKeyValueObservingOptions)options
          context:(nullable void *)context
{
   // register the observer
   [target addObserver:observer forKeyPath:keyPath options:options context:context];

   // swizzle dealloc to remove it

   Class targetClass = target.class;
   SEL deallocSelector = NSSelectorFromString(@"dealloc");
   DeallocImp currentDealloc = (DeallocImp)method_getImplementation(  class_getInstanceMethod(targetClass, deallocSelector) );

   // don't capture target strongly in block or dealloc will never get called!
   __unsafe_unretained NSObject *targetPointer = target;

   void (^replacementBlock)(id self) = ^(__unsafe_unretained id self)
   {
      if (self == targetPointer)
         [targetPointer removeObserver:observer forKeyPath:keyPath];

      currentDealloc(self, deallocSelector);
   };

   class_replaceMethod(targetClass, deallocSelector, imp_implementationWithBlock(replacementBlock), "v@:");
}

@end

__unsafe_unretained的两种用途都是为了解决ARC的后果。特别是方法通常保留它们的self参数,dealloc方法不保留,并且块遵循相同的retain-as-needed模型。要使用块作为dealloc的实现,需要重写此行为,这是__unsafe_unretained正在使用的行为。

要使用上述代码,您只需替换:

[b addObserver:a forKeyPath:keyPath options:options context:NULL];

使用:

[AutoRemovedKVO forTarget:b addObserver:a forKeyPath:keyPath options:options context:NULL]; 

考虑到上述警告,上述代码将在2017年完成工作(未来几年无法保证!)

原创2013答案

这里概述了如何处理这种情况以及类似的情况。

首先查找关联对象。简而言之,您可以将关联对象附加到任何其他对象(使用objc_setAssociatedObject),并指定关联对象应保留,只要它附加的对象在周围(使用OBJC_ASSOCIATION_RETAIN)。

使用关联对象,可以安排在取消分配观察对象时自动删除观察者。设X为观察者,Y为观察对象。

创建一个“取消注册”类,比如Z,它采用(通过init)X& Y及其dealloc方法执行removeObserver

要设置观察,X:

  1. 创建Z的实例,传递自身和Y。
  2. 将自己注册为Y的观察员。
  3. 将Z与Y联系。
  4. 现在当Y被解除分配时,Z将被释放,这将导致调用Z dealloc并取消注册X的观察。

    如果你需要在Y仍处于活动状态时删除对X的观察,你可以通过删除相关对象来执行此操作 - 这样做会触发其dealloc ...

    当您想要在解除分配另一个对象时触发某些内容时,可以使用此模式。

    HTH

答案 1 :(得分:1)

从KVO指南(强调我的):键值观察addObserver:forKeyPath:options:context:方法不保持对观察对象,观察对象或上下文的强引用。 您应确保在必要时保持对观察,观察,对象和上下文的强引用

Cocoa Bindings在bind:unbind:期间管理强引用。

回答您的问题“当观察对象被解除分配时,如何取消注册观察者?”:您不注销观察者,因为观察者应该保持对观察对象的强引用。因此,观察到的对象不会被解除分配。

另一种想到这一点的方法是,您正在观察属性,而不是观察到的对象本身。所以你只关心属性设置为nil。因为您作为观察者维护强引用,所以在观察对象完成观察之前,观察对象将不会被释放。

因此,如果观察对象作为属性被移除,您正在观察另一个对象的 ,那将导致您停止观察它(释放强引用),然后允许被观察对象被释放。您正在观察的属性可能是对象的集合,因此您将观察被删除的对象,导致您停止观察并允许该对象解除分类。

答案 2 :(得分:0)

我尝试过的模式并且看起来很有效:X想要观察Y

1)在Y中声明一个属性

@property (assign) id dealloced;

2)在Y的dealloc

- (void)dealloc {
   self.dealloced = self;
}

3)在X中观察Y的keyPath dealloced

[Y addObserver:self forKeyPath:@"dealloced" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];

4)在X中只处理解除分配的KVO更改并取消注册所有观察

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {

    ...
    if ([keyPath isEqual:@"dealloced"]) {
       id obj = change[@"new"];
       [obj removeObserver:self forKeyPath:@"dealloced"];
       // remove other observing as well
    }
    ...
}

在X中,我们甚至不需要持有对Y

的引用