[译文]如何才能不崩溃 #1: KVO and Manual Bindings

原文地址:

http://inessential.com/2015/05/14/hownottocrash1kvoandmanualbind

我最近修复了一些bug,但是相比起修复bug,我想我对如何写出不导致崩溃的代码更感兴趣。

在第一部分,我将讨论KVO、manual bindings(手动绑定)、retain cycles(循环引用)、invalidate methods(无效方法)。

绑定意味着永远不会说再见

iOS开发者不会,但是Mac开发者会做下面的事情:我们可以绑定一个属性到另外一个属性上面。我们这么做是为了,如果x.foo更新了,那么y.foo也会一起更新。

AppKitNSKeyValueBinding.h中,看一下这个方法:bind: toObject: withKeyPath: options:

让我们想象一下,有一个button有一个title属性。无论何时,某个controller的title更新,这个title属性也要更新。我们说这个controller拥有这个button,我们也许会这么写:

static NSString *kTitleKey = @"title";
[self.button bind:kTitleKey toObject:self withKeyPath:kTitleKey options:nil];

很方便,并且它也工作的很好。

并且这也带你走向了崩溃。

这有一个问题:这个绑定保留了toObject的对象,这意味着这个button有效保留了这个controller。如果这个controller保留了这个button(它应该会的),那么会导致循环引用。这不会成为僵尸,但是会被遗弃。

有个方式会导致崩溃-这是一个真实的故事-如果这个被遗弃的controller监听着某个通知(我们叫它BSNotification),并且它在接收到这个通知之后需要做某个事情,然后它做这个事情的时候崩溃了,原因是它不再有效,并且它不知道如何正确的去处理这个事情。

KVO意味着每次不得不将任何事情都做得完美

让我们添加第三个对象,一个model object。下面是我们真正想要的:

每当modelObject.title改变,更新controller.title,更新button.title。

这次我们将使用KVO。

在这个controller中:

- (NSString *)title {
  return self.modelObject.​title;
}

+ (NSSet *)keyPaths​ForValues​AffectingTitle {
  return [NSSet setWithObject:​@"modelObject.title"];
}

好的,我们现在拥有了整个流程,当modelObject.title改变的时候,它会影响controller.title,然后更新button.title到正确的值。

很方便,并且它也工作的很好。

当然,modelObject被释放的时候,它将会崩溃(因为modelObject的实例被释放之后,它任然拥有一个观察者)。

如果controller保留了modelObject(它大概可能会),那么这第三个对象将会被遗弃,并且无法被释放,然后它将无所事事,并且越来越邪恶。

一种解决这种问题的方法但并不好

controller应该有一个方法类似于invalidate 来打破循环引用,一旦被打破,最终controller和它的button、model object都会调用dealloc方法。

当你知道你实际上已经结束了一个controller,你可以这样写代码:

- (void)invalidate {
  [self.button unbind:kTitleKey];
  self.modelObject = nil;
}

这里解释一下,为什么这个方式并不好:

引用计数是一个很好的解决方法 - 它保证了当dealloc被调用的时候,你知道对于将要被释放的对象来说,已经没有任何对象对它具有强引用。这个造就了dealloc是一个移除观察者或者类似情况的地方。

但是如果你使用了一些invalidate method(无效方法),你在尝试自己去做引用计数的工作。你不得不在正确的时候去调用invalidate。你能保证对于每一个对象都拥有invalidate method并且永远正确吗?如果某个东西改变了,一个或多个对象保留了这个controller呢?谁去调用invalidate,什么时候调用呢?

这会有一堆额外的工作和思考,部分编程的目标可能是创造更少的错误,然而依赖invalidate可能使创造错误的可能性更大。

一个解决这种问题的更好的方式

让我们回到我们尝试解决的这个问题:

每当modelObject.title改变,更新controller.title,更新button.title。

我们很清楚:controller知道modelObject和button,但是其他两个并不互相知道对方,并且其他两个也不知道controller。下面是我们在不依赖于invalidate方法而处理它的方式。

在controller中不需要自定义getter方法。不需要 keyPaths ForValues AffectingTitle。不需要使用 bind: toObject: withKeyPath: options:

创建一个setter方法来代替 - 因为,最终,当发生某些变化的时候,setter都会被调用。解决全部的问题在于传播title属性。

- (void)setTitle:(NSString *)title {
  _title = title;
  self.button.title = title;
}

这解决了一半的问题:当controller.title改变的时候,button.title改变。

我们不能对modelObject做类似的事情,因为它并不知道controller。我们让controller观察modelObject.title来代替。

[self.modelObject addObserver:self forKeyPath:​kTitleKey options:0 context:​kTitleContext];

在KVO的观察者方法中,监视kTitleContext,然后使得self.title = self.modelObject.title。这个将会调用controller的setTitle:方法,而这个方法将更新button.title。

在这个解决方法中,不会产生循环引用。在controller的dealloc方法中需要加这一行代码:

[_modelObject removeObserver:​self forKeyPath:kTitleKey context:​kTitleContext];

回顾和建议

这是我们想出的解决循环引用的办法,而不需要记住在真正正确的时候去调用invalidate方法。这是一个安全的代码。

代码更少且更清晰。

一些建议:

在任何情况下,永远不要使用bind: toObject: withKeyPath: options: 。(iOS开发者,考虑到你的幸运,这不是一个选项。你也要考虑这是否可能是不在iOS上这么做的原因。)

当你在传播改变的时候,使用一个自定义的setter要比自定义的getter好。(毕竟在setter中,会发生变化)

避免使用invalidate方法去做引用计数的事情 - 因为如果你去追踪引用,你将会犯错。我认识到不可能一直避免invalidate方法,但它可能比你想象的可能性更大。

任何一种连锁观察都会导致很难去想象在你的app中会发生什么:当它合理的时候,最好去明确。一旦你有足够多的观察,那么你就是在创建一个不可能通过的丛林,当发生变化的时候,会特别可怕。

理论上,绑定和KVO都是去促进更低的耦合,但是实际上耦合总是很紧密 - 如果不严格,在某种意义上 - 很难去debug并且使它正确。通常最好的方法去明确观察(和keyPaths ForValues AffectingXyz截然相反),并且保持keyPaths没有.字符在外面。

Article by 付军