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

标签 ios objective-c asynchronous grand-central-dispatch key-value-observing

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

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

在正常情况下,这工作得很好。当我允许取消数据加载时会出现问题,它会提前从加载 block 返回,将 isDataLoaded 设置为 NO 并将 wasLoadingCanceled 设置为 YES。这是演示该问题的视频: Demo video

从视频中可以看出,异常总是出现在线路上:

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

最后,这是填充 TableView 的观察 View Controller 的代码:

- (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 通知分派(dispatch)到主线程,但这会导致 UI 完全锁定。

编辑:我更改了 dataLoaded 属性以不使用不同的 getter,从而消除了手动 KVO 的需要。但是,现在尝试设置该属性时仍然会发生崩溃。

这是堆栈跟踪: Stack trace

最佳答案

您的属性名称是 dataLoaded。因此,您使用 KVC 和 KVO 的 key 应该是 @"dataLoaded",而不是 @"isDataLoaded"isDataLoaded 只是 getter 的名称,而不是属性。考虑一下,例如,如果该属性是公开读写的(我知道它不是),您会认为 [object setValue:newValue forKey:@"isDataLoaded"] 是正确的吗?这将查找名为 -setIsDataLoaded: 的 setter,它不存在。

如果您修复了该问题,则无需手动发布 KVO 更改通知。任何对 -setDataLoaded: 的调用都会自动生成它们(假设您没有通过覆盖 +automaticallyNotifiesObserversForKey: 来禁用它)。

同样,self.dataManager.isDataLoaded 之类的东西也是错误的。使用点语法,您应该使用属性名称,而不是 getter 名称。 声明的 属性被命名为dataLoaded。它产生一个名为 -isDataLoaded 的 getter。碰巧 getter 的存在意味着存在具有 getter 名称的非正式属性。因此,声明的属性 dataLoaded 恰好暗示存在一个名为 isDataLoaded 的非正式属性——这就是您的代码编译的原因——但这并不是您的类属性的真正名称。

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

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

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

关于ios - 手动 KVO 通知导致串行队列崩溃,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/29399901/

相关文章:

iPhone:从根据索引排序的核心数据中获取数据

ios - Swift URLSession 完成处理程序

ios - 在树莓派中创建 iBeacon 服务

iOS : Google Sigin error, 您的应用程序被阻止

objective-c - 如何在 watchOS 2 中获取当前位置?

objective-c - Mac OSX 10.9.4 中的应用程序图标在运行时不会更改

ios - 为每个 UISegementedControl 索引设置不同的文本颜色

用于流式传输请求正文内容的 Python 服务器

asynchronous - C 异步编程的示例代码

javascript - Angular.js : Initializing a controller on an asynchronously loaded template?