-->

KVO broken in iOS 9.3

2019-01-18 06:50发布

问题:

This might be an awful bug in iOS 9.3 (release).

When adding a single observer to [NSUserDefaults standardUserDefaults] I've noticed that the responding method -observeValueForKeyPath:ofObject:change:context: is called multiple times.

In the simple example below, every time a UIButton is pressed once, observeValueForKeyPath fires twice. In more complicated examples it fires even more times. It is only present on iOS 9.3 (both on sim and devices).

This can obviously wreak havoc on an app. Anyone else experiencing the same?

// ViewController.m (barebones, single view app)

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"viewDidLoad");
    [[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:@"SomeKey" options:NSKeyValueObservingOptionNew context:NULL];
}

- (IBAction)buttonPressed:(id)sender {
    NSLog(@"buttonPressed");
    [[NSUserDefaults standardUserDefaults] setInteger:1 forKey:@"SomeKey"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
    NSLog(@"observeValueForKeyPath: %@", keyPath);
} 

回答1:

Yes I am experiencing this as well and it seems to be a bug, below is a quick workaround I’m using for the moment until this is fixed. I hope it helps!

Also to clarify, since iOS 7 KVO has been working great with NSUserDefaults and it certainly appears to be key value observable as Matt stated, it is explicitly written in NSUserDefaults.h in the iOS 9.3 SDK: “NSUserDefaults can be observed using Key-Value Observing for any key stored in it."

#include <mach/mach.h>
#include <mach/mach_time.h>

@property uint64_t newTime;
@property uint64_t previousTime;
@property NSString *previousKeyPath;

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    //Workaround for possible bug in iOS 9.3 SDK that is causing observeValueForKeyPath to be called multiple times.
    newTime = mach_absolute_time();
    NSLog(@"newTime:%llu", newTime);
    NSLog(@"previousTime:%llu", previousTime);

    //Try to avoid duplicate calls
    if (newTime > (previousTime + 5000000.0) || ![keyPath isEqualToString:previousKeyPath]) {
        if (newTime > (previousTime + 5000000.0)) {
            NSLog(@"newTime > previousTime");
            previousTime = newTime;
            NSLog(@"newTime:%llu", newTime);
            NSLog(@"previousTime:%llu", previousTime);
        }
        if (![keyPath isEqualToString:previousKeyPath]) {
            NSLog(@"new keyPath:%@", keyPath);
            previousKeyPath = keyPath;
            NSLog(@"previousKeyPath is now:%@", previousKeyPath);
        }
        //Proceed with handling changes
        if ([keyPath isEqualToString:@“MyKey"]) {
            //Do something
        }
    }
}


回答2:

When adding a single observer to [NSUserDefaults standardUserDefaults] I've noticed that the responding method -observeValueForKeyPath:ofObject:change:context: is called multiple times

This is a known issue and is reported (by Apple) as fixed in iOS 11 and macOS 10.13.



回答3:

Adding this answer for MacOS (10.13) which definitely has the bug getting multiple notifications for KVO of NSUserDefault Keys, and which also addresses deprecations. It is better to use a calculation for elapsed nano seconds that gets it for the machine you are running on. Do it like so:

#include <mach/mach.h>
#include <mach/mach_time.h>
static mach_timebase_info_data_t _sTimebaseInfo;

uint64_t  _newTime, _previousTime, _elapsed, _elapsedNano, _threshold;
NSString  *_previousKeyPath;

-(BOOL)timeThresholdForKeyPathExceeded:(NSString *)key thresholdValue:(uint64_t)threshold
{
   _previousTime = _newTime;
   _newTime = mach_absolute_time();

    if(_previousTime > 0) {
        _elapsed = _newTime - _previousTime;
        _elapsedNano = _elapsed * _sTimebaseInfo.numer / _sTimebaseInfo.denom;
    }

    if(_elapsedNano > threshold || ![key isEqualToString:_previousKeyPath]) {
        if(![key isEqualToString:_previousKeyPath]) _previousKeyPath = key;
            return YES;
        }
        return NO;
    }
}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if(![self timeThresholdForKeyPathExceeded:keyPath thresholdValue:5000000]) return;  // Delete this line of MacOS bug ever fixed
    }
    // Else this is the KeyPath you are looking for Obi Wan, process it.
}

This is based on Listing 2 of this Apple Doc: https://developer.apple.com/library/content/qa/qa1398/_index.html