【问题标题】:Best practices for context parameter in addObserver (KVO)addObserver (KVO) 中上下文参数的最佳实践
【发布时间】:2012-09-25 01:15:14
【问题描述】:

我想知道当你观察一个属性时你应该在 KVO 中设置什么上下文指针。我刚开始使用 KVO,我还没有从文档中收集到太多信息。我在这个页面上看到:http://www.jakeri.net/2009/12/custom-callout-bubble-in-mkmapview-final-solution/作者这样做:

[annView addObserver:self
forKeyPath:@"selected"
options:NSKeyValueObservingOptionNew
context:GMAP_ANNOTATION_SELECTED];

然后在回调中,这样做:

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

NSString *action = (NSString*)context;


if([action isEqualToString:GMAP_ANNOTATION_SELECTED]){

我假设在这种情况下,作者只是创建了一个字符串,以便稍后在回调中识别。

然后在 iOS 5 Pushing the Limits 一书中,我看到他这样做了:

[self.target addObserf:self forKeyPath:self.property options:0 context:(__bridge void *)self];

回调:

if ((__bridge id)context == self) {
}
else {
   [super observeValueForKeyPath .......];
}

我想知道是否有标准或最佳实践可以传递给上下文指针?

【问题讨论】:

    标签: objective-c key-value-observing


    【解决方案1】:

    重要的是(一般来说)您使用 something(而不是什么都没有)并且您使用的任何东西都是 唯一 并且 仅供您使用

    这里的主要陷阱发生在当您在某个类中进行观察时,然后有人对您的类进行子类化,并且他们添加了对相同观察对象和相同 keyPath 的另一个观察。如果您最初的observeValueForKeyPath:... 实现只检查了keyPath,或观察到的object,甚至两者都检查,则可能不足以知道它是您的 观察被回调。使用 context 其值对您来说是唯一且私有的,可以让您更加确定对 observeValueForKeyPath:... 的给定调用是您期望的调用。

    例如,如果您仅注册didChange 通知,但子类使用NSKeyValueObservingOptionPrior 选项注册相同的对象和keyPath,这将很重要。如果您没有使用context 过滤对observeValueForKeyPath:... 的调用(或检查更改字典),您的处理程序将执行多次,而您只希望它执行一次。不难想象这会如何导致问题。

    我使用的模式是:

    static void * const MyClassKVOContext = (void*)&MyClassKVOContext;
    

    这个指针将指向它自己的位置,并且那个位置是唯一的(没有其他静态或全局变量可以拥有这个地址,任何堆或堆栈分配的对象也不能拥有这个地址——这是一个非常强大的,尽管不可否认不是绝对,保证),感谢链接器。 const 使编译器在我们尝试编写会更改指针值的代码时发出警告,最后,static 将其设为该文件的私有,因此该文件之外的任何人都无法获得对它的引用(再次,使其更有可能避免冲突)。

    我要特别提醒反对使用的一种模式出现在问题中:

    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    
        NSString *action = (NSString*)context;
        if([action isEqualToString:GMAP_ANNOTATION_SELECTED]) {
    

    context 被声明为void*,这意味着可以保证它是什么。通过将其转换为NSString*,您将打开一大盒潜在的坏事。如果其他人的注册没有使用NSString* 作为context 参数,则当您将非对象值传递给isEqualToString: 时,此方法将崩溃。指针相等(或者intptr_tuintptr_t 相等)是唯一可以与context 值一起使用的安全检查。

    使用self 作为context 是一种常见的方法。总比没有好,但唯一性和隐私性要弱得多,因为其他对象(更不用说子类)可以访问self 的值,并且可能将其用作context(导致歧义),这与我的方法不同以上建议。

    还请记住,这里可能导致陷阱的不仅仅是 子类;尽管可以说这是一种罕见的模式,但没有什么可以阻止另一个对象注册您的对象以进行新的 KVO 观察。

    为了提高可读性,您还可以将其包装在预处理器宏中,例如:

    #define MyKVOContext(A) static void * const A = (void*)&A;
    

    【讨论】:

    • 这是一篇写得很好的小论文,我很惊讶 OP 没有接受它。
    • 让指针指向它自己的位置有什么好处(而不是仅仅声明没有值的指针)?
    • KVO 将上下文视为整数。它不在乎它“指向”什么,因此如前所述,让它指向自身可以提供良好的唯一性(即,不能有一个稍后分配的对象恰好具有相同的地址。)
    • 不使用self 作为上下文的一个更有趣的原因是self 由子类和超类中的代码共享。例如,如果您有一个超类 Person、一个名为 Employee 的子类和一个名为 Manager 的子类,它们都将拥有相同的self。他们可能不应该共享上下文,否则 Manager 的未来变化可能会影响 Person
    • self 是一个指向对象的指针几乎是“对它的使用不私有”的定义。所以,是的,有很多理由不将其用作 KVO 上下文。
    【解决方案2】:

    KVO 上下文应该是一个指向静态变量的指针,如this gist 所示。通常,我发现自己在做以下事情:

    在我的文件顶部附近ClassName.m 我会得到这条线

    static char ClassNameKVOContext = 0;
    

    当我开始观察 targetObjectTargetClass 的一个实例)上的 aspect 属性时,我将拥有

    [targetObject addObserver:self
                   forKeyPath:PFXKeyTargetClassAspect
                      options://...
                      context:&ClassNameKVOContext];
    

    其中 PFXKeyTargetClassAspect 是在TargetClass.m 中定义的NSString * 等于@"aspect" 并在TargetClass.h 中声明extern。 (当然,PFX 只是您在项目中使用的前缀的占位符。)这给了我自动完成的优势并保护我免受拼写错误。

    当我在 targetObject 上完成对 aspect 的观察后,我将拥有

    [targetObject removeObserver:self
                      forKeyPath:PFXKeyTargetClassAspect
                         context:&ClassNameKVOContext];
    

    为了避免在我的-observeValueForKeyPath:ofObject:change:context:的实现中缩进太多,我喜欢写

    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
    {
        if (context != &ClassNameKVOContext) {
            [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
            return;
        }
    
        if ([object isEqual:targetObject]) {
            if ([keyPath isEqualToString:PFXKeyTargetClassAspect]) {
                //targetObject has changed the value for the key @"aspect".
                //do something about it
            }
        }
    }
    

    【讨论】:

    • 嗨,我正在尝试将单元测试添加到我在-observeValueForKeyPath:ofObject:change:context: 中的处理方式中。我遇到的问题是我正在观察一个对象的只读属性,我无法在单元测试中更改以触发 KVO,因此我需要直接在单元测试中手动调用 -observeValueForKeyPath:ofObject:change:context: 方法。但是当我传递给那个调用并传递上下文时,它总是不同的,我的单元测试总是失败。您知道如何在单元测试中传递相同的上下文吗?
    • 我不同意这种模式。 KVO 上下文不是用于识别类,而是用于识别在任意数量的关键路径发生更改时应该发生的操作。例如。 name 和 age 键路径都应该导致 updateView 被调用。使用上下文指针可以快速完成,而无需检查是否进行关键路径字符串比较。
    【解决方案3】:

    我认为更好的方法是按照苹果文档的说法来实现它:

    类中唯一命名的静态变量的地址是一个很好的上下文。

    static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
    static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
    

    documentation

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-12-30
      • 1970-01-01
      • 1970-01-01
      • 2015-12-14
      • 2014-08-30
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多