简体   繁体   English

手动KVO通知导致串行队列崩溃

[英]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. 我在与数据管理器类的特定属性上的手动KVO通知有关时遇到了相当奇怪的崩溃。

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. 加载完成后,该类将根据是否成功加载数据将其属性dataLoaded设置为适当的值。 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. 就会出现问题,当我允许数据加载到被取消,其从装载块早返回,套isDataLoaded为NO和wasLoadingCanceled为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: 以下是DataManager类的相关方法:

// .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. 是的,我确实尝试将手动KVO通知分派到主线程,但这导致UI完全锁定。

EDIT: I changed the dataLoaded property to not use a different getter, removing the need for manual KVO. 编辑:我将dataLoaded属性更改为不使用其他getter,从而无需手动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 . 属性的名称为dataLoaded Therefore, the key you use with KVC and KVO should be @"dataLoaded" , not @"isDataLoaded" . 因此,与KVC和KVO一起使用的密钥应该是@"dataLoaded" ,而不是@"isDataLoaded" isDataLoaded is just the name of a getter, not the property. isDataLoaded只是获取方法的名称,而不是属性。 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? 考虑一下,例如,如果该属性是公开读写的(我知道不是),您是否认为[object setValue:newValue forKey:@"isDataLoaded"]是正确的? That would look for a setter named -setIsDataLoaded: , which doesn't exist. 它将查找不存在的名为-setIsDataLoaded:的设置器。

If you fix that, then there's no need to manually post KVO change notifications. 如果已解决问题,则无需手动发布KVO更改通知。 Any call to -setDataLoaded: is going to generate them automatically (assuming you haven't disabled that by overriding +automaticallyNotifiesObserversForKey: ). -setDataLoaded:任何调用都将自动生成它们(假设您尚未通过覆盖+automaticallyNotifiesObserversForKey:禁用它)。

Likewise, things like self.dataManager.isDataLoaded are kind of wrong. 同样,诸如self.dataManager.isDataLoaded类的东西也是错误的。 With dot syntax, you should use the property name, not the getter name. 使用点语法时,应使用属性名称,而不是getter名称。 The declared property is named dataLoaded . 声明的属性名为dataLoaded It produces a getter named -isDataLoaded . 它产生一个名为-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. 因此,声明的属性dataLoaded恰好意味着存在一个名为isDataLoaded的非正式属性-这就是您的代码进行编译的原因-但这并不是类属性的真正名称。

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. 我不确定为什么要使用结构NSStringFromSelector(@selector(isDataLoaded)) ,但是我认为使用符号字符串常量会更好。

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. 同样,如果将KVO更改通知发布在主线程上,则-observeValueForKeyPath:...方法一定不能使用dispatch_sync(dispatch_get_main_queue(), ...)因为这肯定会导致死锁。 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. 除此之外,我们还需要查看崩溃的详细信息,以给出更加狭narrow的答案。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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