簡體   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