繁体   English   中英

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

[英]Manual KVO notification causes crash in serial queue

我在与数据管理器类的特定属性上的手动KVO通知有关时遇到了相当奇怪的崩溃。

此类异步将其数据加载到自定义串行队列中。 加载完成后,该类将根据是否成功加载数据将其属性dataLoaded设置为适当的值。 观察者可以观察此属性,以便在加载完成时得到通知。

在正常情况下,这非常正常。 就会出现问题,当我允许数据加载到被取消,其从装载块早返回,套isDataLoaded为NO和wasLoadingCanceled为YES。 这是演示该问题的视频: 演示视频

从视频中可以看到,异常总是在行上发生:

[self willChangeValueForKey:...];

以下是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))];
//}

这是登录时开始加载的代码:

[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;
    }];

最后,这是填充表格视图的观察视图控制器的代码:

- (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];
}

是的,我确实尝试将手动KVO通知分派到主线程,但这导致UI完全锁定。

编辑:我将dataLoaded属性更改为不使用其他getter,从而无需手动KVO。 但是,现在尝试设置该属性时仍然会崩溃。

这是堆栈跟踪: 堆栈跟踪

属性的名称为dataLoaded 因此,与KVC和KVO一起使用的密钥应该是@"dataLoaded" ,而不是@"isDataLoaded" isDataLoaded只是获取方法的名称,而不是属性。 考虑一下,例如,如果该属性是公开读写的(我知道不是),您是否认为[object setValue:newValue forKey:@"isDataLoaded"]是正确的? 它将查找不存在的名为-setIsDataLoaded:的设置器。

如果已解决问题,则无需手动发布KVO更改通知。 -setDataLoaded:任何调用都将自动生成它们(假设您尚未通过覆盖+automaticallyNotifiesObserversForKey:禁用它)。

同样,诸如self.dataManager.isDataLoaded类的东西也是错误的。 使用点语法时,应使用属性名称,而不是getter名称。 声明的属性名为dataLoaded 它产生一个名为-isDataLoaded的吸气剂。 碰巧,吸气剂的存在意味着以吸气剂的名字存在的非正式财产的存在。 因此,声明的属性dataLoaded恰好意味着存在一个名为isDataLoaded的非正式属性-这就是您的代码进行编译的原因-但这并不是类属性的真正名称。

我不确定为什么要使用结构NSStringFromSelector(@selector(isDataLoaded)) ,但是我认为使用符号字符串常量会更好。

可以将属性的设置分配给主线程,但是您可能希望异步进行此操作,而不是像注释掉的代码所示那样同步进行。 同样,如果将KVO更改通知发布在主线程上,则-observeValueForKeyPath:...方法一定不能使用dispatch_sync(dispatch_get_main_queue(), ...)因为这肯定会导致死锁。 直接执行该代码或异步分配它。

除此之外,我们还需要查看崩溃的详细信息,以给出更加狭narrow的答案。

暂无
暂无

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

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