简体   繁体   中英

Manual KVO notification causes crash in serial queue

I'm experiencing a rather odd crash relating to manual KVO notification on a particular property of my data manager class.

This class loads its data asynchronously on a custom serial queue. After loading is finished, the class sets it's property dataLoaded to the appropriate value depending on whether data was loaded successfully or not. Observers can observe this property in order to be notified when loading has completed.

Under normal circumstances, this works perfectly fine. The problem arises when I allow the data loading to be canceled, which returns early from the loading block, sets isDataLoaded to NO and wasLoadingCanceled to YES. Here is a video demonstrating the issue: Demo video

As can be seen in the video, the exception always occurs on the line:

[self willChangeValueForKey:...];

Here are the relevant methods for the DataManager class:

// .h
@property (nonatomic, readonly) BOOL dataLoaded;
@property (nonatomic, readonly, getter=isDataLoading) BOOL dataLoading;
@property (nonatomic, readonly, getter=wasLoadingCanceled) BOOL loadingCanceled;

// .m
- (id)init
{
    self = [super init];
    if (self) {
        _data = @[];
        _dataLoaded = NO;
        _dataLoading = NO;
        _loadingCanceled = NO;
    }
    return self;
}

- (void)_clearData:(NSNotification *)notification
{
    if (self.isDataLoading) {
        _loadingCanceled = YES;
    } else {
        self.dataLoaded = NO;
    }

    _data = @[];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:@"WillLogOut" object:nil];
}

- (void)loadDataWithBlock:(NSArray* (^)(void))block
{
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_clearData:) name:@"WillLogOut" object:nil];

    dispatch_queue_t loadingQueue = dispatch_queue_create("com.LoadingQueue", NULL);

    __weak typeof(self) weakSelf = self;
    dispatch_async(loadingQueue, ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;

        strongSelf->_dataLoading = YES;
        strongSelf->_loadingCanceled = NO;

        NSLog(@"Data loading...");
        strongSelf.data = block();
        strongSelf->_dataLoading = NO;
        NSLog(@"Data loaded.");

        BOOL dataLoaded = (strongSelf.data != nil);
        dispatch_async(dispatch_get_main_queue(), ^{
            // CRASH here now...
            strongSelf.dataLoaded = dataLoaded;
        });
    });
}

//- (void)setDataLoaded:(BOOL)dataLoaded
//{
// CRASH: Exception always on the following line:
//    [self willChangeValueForKey:NSStringFromSelector(@selector(isDataLoaded))];
//    _dataLoaded = dataLoaded;
//    [self didChangeValueForKey:NSStringFromSelector(@selector(isDataLoaded))];
//}

Here is the code that starts the loading when logging in:

[dataManager loadDataWithBlock:^NSArray *{
        NSMutableArray *data = [NSMutableArray array];
        [data addObject:@"One"];
        [data addObject:@"Two"];
        // NOTE: Simulating longer loading time.
        usleep(1.0 * 1.0e6);

        if (dataManager.wasLoadingCanceled) {
            NSLog(@"Loading canceled.");
            return nil;
        }

        [data addObject:@"Three"];
        [data addObject:@"Four"];
        [data addObject:@"Five"];
        // NOTE: Simulating longer loading time.
        usleep(1.0 * 1.0e6);

        if (dataManager.wasLoadingCanceled) {
            NSLog(@"Loading canceled.");
            return nil;
        }

        [data addObject:@"Six"];
        [data addObject:@"Seven"];

        return data;
    }];

And finally, here is the code for the observing view controller that populates the table view:

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    if (self.dataManager.dataLoaded) {
        [self.dataTable reloadData];
    } else {
        [self.dataManager addObserver:self
                           forKeyPath:NSStringFromSelector(@selector(dataLoaded))
                              options:NSKeyValueObservingOptionNew
                              context:nil];
    }
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataLoaded))]) {
        BOOL check = [[change objectForKey:NSKeyValueChangeNewKey] boolValue];
        if (check) {
            dispatch_sync(dispatch_get_main_queue(), ^{
                [self.dataTable reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationFade];
            });

            [self.dataManager removeObserver:self forKeyPath:NSStringFromSelector(@selector(dataLoaded)) context:nil];
        }
    }
}

- (IBAction)logOut:(id)sender
{
    [[NSNotificationCenter defaultCenter] postNotificationName:@"WillLogOut" object:self userInfo:nil];
    [self dismissViewControllerAnimated:YES completion:nil];
}

And yes, I did try to dispatch the manual KVO notification to the main thread, but that results in a complete lock in the UI.

EDIT: I changed the dataLoaded property to not use a different getter, removing the need for manual KVO. However, there is still a crash now when trying to set the property.

Here is the stack trace: 堆栈跟踪

The name of your property is dataLoaded . Therefore, the key you use with KVC and KVO should be @"dataLoaded" , not @"isDataLoaded" . isDataLoaded is just the name of a getter, not the property. Consider, for example, if the property were publicly read-write (I know it's not), would you think that [object setValue:newValue forKey:@"isDataLoaded"] to be correct? That would look for a setter named -setIsDataLoaded: , which doesn't exist.

If you fix that, then there's no need to manually post KVO change notifications. Any call to -setDataLoaded: is going to generate them automatically (assuming you haven't disabled that by overriding +automaticallyNotifiesObserversForKey: ).

Likewise, things like self.dataManager.isDataLoaded are kind of wrong. With dot syntax, you should use the property name, not the getter name. The declared property is named dataLoaded . It produces a getter named -isDataLoaded . It happens that the existence of a getter implies the existence of an informal property with the getter's name. So, the declared property dataLoaded happens to imply the existence of an informal property named isDataLoaded — which is why your code compiles — but that's not really the name of your class's property.

I'm not sure why you're using the construct NSStringFromSelector(@selector(isDataLoaded)) , but I think it would just be better to use a symbolic string constant.

Dispatching the setting of the property to the main thread could probably be made to work, but you would want to do it asynchronously, not synchronously as your commented-out code shows. Also, if the KVO change notification is posted on the main thread, then your -observeValueForKeyPath:... method must not use dispatch_sync(dispatch_get_main_queue(), ...) because that will definitely deadlock. Either execute that code directly or dispatch it asynchronously.

Beyond that, we'd need to see crash details to give a more narrowly-focused answer.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM