手动 KVO 通知导致串行队列崩溃
Manual KVO notification causes crash in serial queue
我在数据管理器 class 的特定 属性 上遇到与手动 KVO 通知相关的相当奇怪的崩溃。
此 class 在自定义串行队列上异步加载其数据。加载完成后,class 将其 属性 dataLoaded
设置为适当的值,具体取决于数据是否成功加载。观察者可以观察这个 属性 以便在加载完成时得到通知。
在正常情况下,这工作得很好。当我允许取消数据加载时出现问题,从加载块开始,returns 将 isDataLoaded
设置为 NO,将 wasLoadingCanceled
设置为 YES。这是演示该问题的视频:
Demo video
从视频中可以看出,异常总是出现在以下行:
[self willChangeValueForKey:...];
下面是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))];
//}
登录时开始加载的代码如下:
[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;
}];
最后,这里是填充 table 视图的观察视图控制器的代码:
- (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
只是 getter 的名称,而不是 属性。考虑一下,例如,如果 属性 是公开读写的(我知道不是),您认为 [object setValue:newValue forKey:@"isDataLoaded"]
是正确的吗?那会寻找一个名为 -setIsDataLoaded:
的 setter,它不存在。
如果您修复了该问题,则无需手动 post KVO 更改通知。对 -setDataLoaded:
的任何调用都会自动生成它们(假设您没有通过覆盖 +automaticallyNotifiesObserversForKey:
来禁用它)。
同样,self.dataManager.isDataLoaded
之类的东西也是错误的。对于点语法,您应该使用 属性 名称,而不是 getter 名称。 声明 属性 命名为dataLoaded
。它产生一个名为 -isDataLoaded
的 getter。碰巧 getter 的存在意味着 非正式 属性 的存在 getter 的名字。因此,声明的 属性 dataLoaded
恰好暗示存在一个名为 isDataLoaded
的非正式 属性 — 这就是您的代码编译的原因 — 但这并不是您的 class的属性.
我不确定您为什么要使用结构 NSStringFromSelector(@selector(isDataLoaded))
,但我认为使用符号字符串常量会更好。
将 属性 的设置分派到主线程可能会起作用,但您可能希望异步进行,而不是像注释掉的代码所示那样同步进行。此外,如果 KVO 更改通知在主线程上 posted,那么你的 -observeValueForKeyPath:...
方法 一定不能 使用 dispatch_sync(dispatch_get_main_queue(), ...)
因为那肯定会死锁.直接执行该代码或异步分派它。
除此之外,我们还需要查看崩溃详细信息以给出更具体的答案。
我在数据管理器 class 的特定 属性 上遇到与手动 KVO 通知相关的相当奇怪的崩溃。
此 class 在自定义串行队列上异步加载其数据。加载完成后,class 将其 属性 dataLoaded
设置为适当的值,具体取决于数据是否成功加载。观察者可以观察这个 属性 以便在加载完成时得到通知。
在正常情况下,这工作得很好。当我允许取消数据加载时出现问题,从加载块开始,returns 将 isDataLoaded
设置为 NO,将 wasLoadingCanceled
设置为 YES。这是演示该问题的视频:
Demo video
从视频中可以看出,异常总是出现在以下行:
[self willChangeValueForKey:...];
下面是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))];
//}
登录时开始加载的代码如下:
[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;
}];
最后,这里是填充 table 视图的观察视图控制器的代码:
- (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
只是 getter 的名称,而不是 属性。考虑一下,例如,如果 属性 是公开读写的(我知道不是),您认为 [object setValue:newValue forKey:@"isDataLoaded"]
是正确的吗?那会寻找一个名为 -setIsDataLoaded:
的 setter,它不存在。
如果您修复了该问题,则无需手动 post KVO 更改通知。对 -setDataLoaded:
的任何调用都会自动生成它们(假设您没有通过覆盖 +automaticallyNotifiesObserversForKey:
来禁用它)。
同样,self.dataManager.isDataLoaded
之类的东西也是错误的。对于点语法,您应该使用 属性 名称,而不是 getter 名称。 声明 属性 命名为dataLoaded
。它产生一个名为 -isDataLoaded
的 getter。碰巧 getter 的存在意味着 非正式 属性 的存在 getter 的名字。因此,声明的 属性 dataLoaded
恰好暗示存在一个名为 isDataLoaded
的非正式 属性 — 这就是您的代码编译的原因 — 但这并不是您的 class的属性.
我不确定您为什么要使用结构 NSStringFromSelector(@selector(isDataLoaded))
,但我认为使用符号字符串常量会更好。
将 属性 的设置分派到主线程可能会起作用,但您可能希望异步进行,而不是像注释掉的代码所示那样同步进行。此外,如果 KVO 更改通知在主线程上 posted,那么你的 -observeValueForKeyPath:...
方法 一定不能 使用 dispatch_sync(dispatch_get_main_queue(), ...)
因为那肯定会死锁.直接执行该代码或异步分派它。
除此之外,我们还需要查看崩溃详细信息以给出更具体的答案。