25

編輯7:NSFetchedResultsController試圖插入零反對

這裏是我的拯救方法。這是漂亮的樣板。 DEBUG_LOG()宏只有在調試版本時纔會執行。

- (void)saveManagedObjectContext:(NSManagedObjectContext *)moc 
{ 
    if ([moc hasChanges]) { 
     DEBUG_LOG(@"Saving managed object context %@", moc); 
     NSError *error; 
     BOOL success = [moc save:&error]; 
     if (!success || error) { 
      DEBUG_LOG(@"ERROR: Couldn't save to managed object context %@: %@", 
        moc, error.localizedDescription); 
     } 
     DEBUG_LOG(@"Finished saving managed object context %@", moc); 
    } else { 
     DEBUG_LOG(@"Managed object context %@ had no changes", moc); 
    } 
} 

編輯6:

的iOS 8是在這裏,這個問題又回來了。幸運的我。以前我把問題縮小到在表格視圖上使用estimatedRowHeight(順便說一句,我從來沒有完全解決這個問題,我剛剛停止使用estimatedRowHeight)。現在我在不同的情況下再次看到這個問題。我跟蹤了幾天前的一個提交,當時我的導航欄/標籤欄變得透明。這包括禁用故事板中的「調整滾動視圖插圖」,並選中框以使我的視圖顯示在頂部條和底部條之下。我需要做的一系列步驟才能實現,但每次以這種方式配置我的故事板時,我都可以重現它。如果我恢復提交,它不會再發生。

雖然我說「它不再發生」,但我真的認爲這只是使它不太可能發生。這個bug絕對是b ****。我現在的直覺是,這是一個iOS錯誤。我只是不知道我能做些什麼來將它變成一個錯誤報告。這是瘋狂。

編輯5:

如果你想讀我的痛苦的整體,請繼續一路走過這個職位。如果你遇到這個問題,你只是想得到一些幫助,這裏有一些東西需要研究。

我上次編輯指出,當我使用基本的表格視圖單元格時,一切正常。我的下一步行動就是從零開始嘗試構建一個新的定製單元,並看看它在哪裏混亂。對於它而言,我重新啓用了舊的自定義單元代碼,它工作得很好。唔?哦,等等,我還有estimatedHeightForRowAtIndexPath註釋掉了。當我刪除這些評論並啓用estimatedHeightForRowAtIndexPath時,它再次變得糟糕透頂。有趣。

我在API文檔中查找了該方法,並提到了一個常量,名爲UITableViewAutomaticDimension。我估計的價值實際上只是常見的細胞高度之一,所以切換到這個常數不會有什麼傷害。切換到該常數後,它正常工作。沒有奇怪的例外/圖形故障報告。

原帖

我有一個非常標準的iPhone應用程序,從在一個表視圖的背景和顯示數據的Web服務獲取數據。後臺更新工作具有爲NSPrivateQueueConcurrencyType配置的自己的託管對象上下文。我的表視圖的抓取結果控制器爲NSMainQueueConcurrencyType配置了自己的託管對象上下文。當後臺上下文解析新數據時,它會通過mergeChangesFromContextDidSaveNotification將該數據傳遞到主要上下文。有時,在合併過程中,我的應用程序點擊這裏異常...

Thread 1, Queue : com.apple.main-thread 
#0 0x3ac1b6a0 in objc_exception_throw() 
#1 0x308575ac in -[__NSArrayM insertObject:atIndex:]() 
#2 0x33354306 in __46-[UITableView _updateWithItems:updateSupport:]_block_invoke687() 
#3 0x330d88d2 in +[UIView(UIViewAnimationWithBlocks)  _setupAnimationWithDuration:delay:view:options:factory:animations:start:animationStateGenerator:completion:]() 
#4 0x330ef7e4 in +[UIView(UIViewAnimationWithBlocks) animateWithDuration:delay:options:animations:completion:]() 
#5 0x3329e908 in -[UITableView _updateWithItems:updateSupport:]() 
#6 0x332766c6 in -[UITableView _endCellAnimationsWithContext:]() 
#7 0x0005ae72 in -[ICLocalShowsTableViewController controllerDidChangeContent:] at ICLocalShowsTableViewController.m:475 
#8 0x3069976c in -[NSFetchedResultsController(PrivateMethods) _managedObjectContextDidChange:]() 
#9 0x308dfe78 in __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__() 
#10 0x30853b80 in _CFXNotificationPost() 
#11 0x3123a054 in -[NSNotificationCenter postNotificationName:object:userInfo:]() 
#12 0x306987a2 in -[NSManagedObjectContext(_NSInternalNotificationHandling) _postObjectsDidChangeNotificationWithUserInfo:]() 
#13 0x306f952a in -[NSManagedObjectContext _mergeChangesFromDidSaveDictionary:usingObjectIDs:]() 
#14 0x306f9734 in -[NSManagedObjectContext mergeChangesFromContextDidSaveNotification:]() 
#15 0x0006b5be in __65-[ICManagedObjectContexts backgroundManagedObjectContextDidSave:]_block_invoke at ICManagedObjectContexts.m:133 
#16 0x306f9854 in developerSubmittedBlockToNSManagedObjectContextPerform() 
#17 0x3b1000ee in _dispatch_client_callout() 
#18 0x3b1029a8 in _dispatch_main_queue_callback_4CF() 
#19 0x308e85b8 in __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() 
#20 0x308e6e84 in __CFRunLoopRun() 
#21 0x30851540 in CFRunLoopRunSpecific() 
#22 0x30851322 in CFRunLoopRunInMode() 
#23 0x355812ea in GSEventRunModal() 
#24 0x331081e4 in UIApplicationMain() 
#25 0x000554f4 in main at main.m:16 

這是我看到的例外......

CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. *** -[__NSArrayM insertObject:atIndex:]: object cannot be nil with userInfo (null) 

我的應用程序實際上是擊中controllerDidChangeContent例外,在我的呼籲endUpdates。我基本上看到了與此相同的東西(NSFetchedResultsController attempting to insert nil object?),但我有更多的信息和案例可重現。我所有的合併事件都是插入。在合併期間,似乎沒有任何待處理的插入,刪除或更新背景上下文。直到我從WWDC視頻中瞭解到performBlock和performBlockAndWait之間的區別之前,我最初都在使用performBlockAndWait。我切換到執行塊,這使它更好一點。起初,我將這作爲一個線程問題來解決,這是由於不完全理解塊造成的一個奇怪的內存問題的可能性,現在我回到它是一個競爭條件。好像只有一件我錯過了。有兩種方式不會發生......

(1)註冊上下文將保存通知,當我得到它時將FRC委託刪除,並在合併後設置委託。這不是完全不使用FRC,所以這不是一個解決方法的選項。 (2)做一些阻塞主線程足夠長的事情,所以競態條件不會發生。例如,當我將很多調試日誌消息添加到我的表視圖委託中時,這會降低速度,以免發生。

下面是我認爲是重要的代碼段(我縮短了某些地方縮小了這個已經很大的帖子)。

在滾動過程中各點之後,視圖控制器會通過調用具有此在它的功能要求更多資料...

AFJSONRequestOperation *operation = 
    [AFJSONRequestOperation JSONRequestOperationWithRequest:request 
     success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) { 
      // Parsing happens on MOC background queue 
      [backgroundMOC performBlock:^ { 
       [self parseJSON:JSON]; 

       // Handle everything else on the main thread 
       [mainMOC performBlock:^ { 
        if (completion) { 
         // Remove activitiy indicators and such from the main thread 
        } 
       }]; 
      }]; 
     } 

     failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) { 
      [[NSOperationQueue mainQueue] performBlock:^ { 
       if (completion) { 
        // Remove activitiy indicators and such from the main thread 
       } 
       // Show an alert view saying that the request failed 
      }]; 
     } 
    ]; 

[operation setCacheResponseBlock:^NSCachedURLResponse *(NSURLConnection *connection, NSCachedURLResponse *cachedResponse) { 
    return nil; 
}]; 

[_operationQueue addOperation:operation]; 

在大多數情況下,parseJSON並沒有真正有什麼有趣的它...

- (void)parseJSON:(NSDictionary *)json 
{  
    NSError *error; 
    NSArray *idExistsResults; 
    NSNumber *eventId; 
    NSFetchRequest *idExistsFetchRequest; 
    LastFMEvent *event; 
    NSManagedObjectModel *model = backgroundMOC.persistentStoreCoordinator.managedObjectModel; 
    for (NSDictionary *jsonEvent in jsonEvents) { 
     eventId = [NSNumber numberWithInt:[jsonEvent[@"id"] intValue]]; 
     idExistsFetchRequest = [model fetchRequestFromTemplateWithName:kGetEventByIDFetchRequest substitutionVariables:@{@"eventID" : eventId}]; 
     idExistsResults = [backgroundMOC executeFetchRequest:idExistsFetchRequest error:&error]; 
     // Here I check for errors - omitted that part 

     if ([idExistsResults count] == 0) { 
      // Add a new event 
      event = [NSEntityDescription insertNewObjectForEntityForName:[LastFMEvent entityName] inManagedObjectContext:backgroundMOC]; 
      [event populateWithJSON:jsonEvent]; 
     } else if ([idExistsResults count] == 1) { 
      // Get here if I knew about the event already, so I update a few fields 
     } 
    } 
    [self.mocManager saveManagedObjectContext:backgroundMOC]; 
} 

保存和合並的實現是它可能會變得有趣的地方。保存期望已經在相應的performBlock中被調用,所以它不會對performBlock執行任何操作。

- (void)saveManagedObjectContext:(NSManagedObjectContext *)moc 
{ 
    if ([moc hasChanges]) { 
     NSError *error; 
     BOOL success = [moc save:&error]; 
     if (!success || error) { 
      NSLog(@"ERROR: Couldn't save to managed object context %@: %@", 
        moc, error.localizedDescription); 
     } 
    } 
} 

保存時,合併通知被觸發。我只是從背景合併到main,所以我幾乎只想知道是否可以內聯合並調用,或者如果我需要在performBlock中執行它。

- (void)backgroundManagedObjectContextDidSave:(NSNotification *)notification 
{ 
    if (![NSThread isMainThread]) { 
     [mainMOC performBlock:^ { 
      [self.mainMOC mergeChangesFromContextDidSaveNotification:notification]; 
     }]; 
    } else { 
     [mainMOC mergeChangesFromContextDidSaveNotification:notification]; 
    } 
} 

我取結果控制器委託方法是相當鍋爐板的東西...其他

- (void)controller:(NSFetchedResultsController *)controller 
    didChangeObject:(id)anObject 
     atIndexPath:(NSIndexPath *)indexPath 
    forChangeType:(NSFetchedResultsChangeType)type 
     newIndexPath:(NSIndexPath *)newIndexPath 
{ 
    UITableView *tableView = self.tableView; 

    switch (type) { 

     case NSFetchedResultsChangeInsert: 
      [tableView insertRowsAtIndexPaths:@[newIndexPath] 
          withRowAnimation:UITableViewRowAnimationAutomatic]; 
      break; 
     case NSFetchedResultsChangeDelete: 
      [tableView deleteRowsAtIndexPaths:@[indexPath] 
          withRowAnimation:UITableViewRowAnimationAutomatic]; 
      break; 
     case NSFetchedResultsChangeUpdate: 
      [self configureCell:(ICLocalShowsTableViewCell *)[tableView cellForRowAtIndexPath:indexPath] 
          atIndexPath:indexPath]; 
      break; 
     case NSFetchedResultsChangeMove: 
      [tableView deleteRowsAtIndexPaths:@[indexPath] 
          withRowAnimation:UITableViewRowAnimationAutomatic]; 
      [tableView insertRowsAtIndexPaths:@[newIndexPath] 
          withRowAnimation:UITableViewRowAnimationAutomatic]; 
      break; 
    } 
} 

- (void)controller:(NSFetchedResultsController *)controller 
    didChangeSection:(id)sectionInfo 
      atIndex:(NSUInteger)sectionIndex 
    forChangeType:(NSFetchedResultsChangeType)type 
{ 
    switch(type) { 

     case NSFetchedResultsChangeInsert: 
      [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] 
          withRowAnimation:UITableViewRowAnimationAutomatic]; 
      break; 

     case NSFetchedResultsChangeDelete: 
      [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] 
          withRowAnimation:UITableViewRowAnimationAutomatic]; 
      break; 
    } 
} 

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller 
{ 
    [self.tableView beginUpdates]; 
} 

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller 
{ 
    [self.tableView endUpdates]; 
} 

一段代碼可能會感興趣。我爲我的表格視圖單元格使用了自動佈局,而動態單元格高度使用了新的estimatedHeightForRowAtIndexPath API。這意味着在調用[self.tableView endUpdates]的過程中,最後一步實際上會到達一些被管理對象,而另一個調用段/行數只需要知道FRC的計數。

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath 
{ 
    NSAssert([NSThread isMainThread], @""); 
    LastFMEvent *event = [self.fetchedResultsController objectAtIndexPath:indexPath]; 

    if (!_offscreenLayoutCell) { 
     _offscreenLayoutCell = [self.tableView dequeueReusableCellWithIdentifier:kLocalShowsCellIdentifier]; 
    } 

    [_offscreenLayoutCell configureWithLastFMEvent:event]; 
    [_offscreenLayoutCell setNeedsLayout]; 
    [_offscreenLayoutCell layoutIfNeeded]; 

    CGSize cellSize = [_offscreenLayoutCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]; 
    return cellSize.height; 
} 

現在已經停留了將近一週。在這個過程中學到了很多東西,但是我準備繼續前進。任何建議將不勝感激。

編輯

我組建了一個相當大的調試日誌,以試圖告訴這是怎麼回事與udpates的故事。我看到一些很奇怪的東西。我一次更新50行,所以我只會包含我的調試輸出中有趣的部分。每當一個單元格被配置時,我將打印出我剛剛出隊的單元格的標題以及新標題的內容。當我點擊表格視圖中的最後一個單元格時,我會向Web服務查詢更多數據。該輸出與最後的更新我打的異常之前...

// Lots of output was here that I omitted 

configure cell at sect 5 row 18 WAS Suphala NOW Keller Williams 
configure cell at sect 5 row 19 WAS Advocate Of Wordz NOW Gates 
configure cell at sect 5 row 20 WAS Emanuel and the Fear NOW Beats Antique 
configure cell at sect 5 row 21 WAS The Julie Ruin NOW Ashrae Fax 

// At this point I hit the end of the table and query for more data - for some reason row 18 gets configured again. Possibly no big deal. 

configure cell at sect 5 row 18 WAS Keller Williams NOW Keller Williams 
configure cell at sect 5 row 22 WAS Old Wounds NOW Kurt Vile 

JSON size 100479 
Starting JSON parsing 
page 3 of 15. total events 709. events per page 50. current low idx 100 next trigger idx 149 

// Parsing data finished, saving background context 

Saving managed object context <NSManagedObjectContext: 0x17e912f0> 
Background context will save 
Finished saving managed object context <NSManagedObjectContext: 0x17e912f0> 
Merging background context into main context 
JSON parsing finished 

** controllerWillChangeContent called ** 
** BEGIN UPDATES triggered ** 

inserting SECTION 6 
inserting SECTION 7 
inserting SECTION 8 
inserting ROW sect 5 row 17 
inserting ROW sect 5 row 22 
inserting ROW sect 5 row 25 
inserting ROW sect 5 row 26 
inserting ROW sect 5 row 27 
inserting ROW sect 5 row 28 
inserting ROW sect 5 row 29 

// A bunch more rows added here that I omitted 

** controllerDidChangeContent called ** 

// This configure cell happens before the endUpdates call has completed 

configure cell at sect 5 row 18 WAS Conflict NOW Conflict 

在它試圖在S5 R17插入最後的更新,但我已經在該行的單元格。它也試圖插入s5 r22,但我也已經在該行有一個單元格。最後它在s5 r25插入一行,其實一個新行。在我看來,似乎考慮到r17和r22作爲插頁在桌面上留下了一個空白。那些索引中的先前單元不應該將事件移至r23和r24?

我提取的結果控制器正在使用按日期和開始時間排序的排序描述符。也許現有的r17和r22事件沒有得到移動事件,因爲沒有任何與NSManagedObjects相關的變化。從本質上講,他們需要移動,因爲我的排序描述符早於他們的事件,而不是因爲他們的數據發生了變化。

編輯2:

看起來像那些插入也只是觸發現有的細胞進行降檔:(

編輯3:

事情我今天試了...

  1. 使AFNetworking成功塊等待合併完成,然後返回
  2. 如果提取的結果控制器位於beginUpdates/endUpdates中間,則cellForRowAtIndexPath會返回一個陳舊的單元(實質上是將它退出並立即將其返回)。認爲在更新過程中被調用的額外的隨機cellForRowAtIndexPath可能做了奇怪的事情。
  3. 完全刪除背景上下文。這很有趣。如果我在主環境中執行所有UI更新和JSON解析,它仍然會發生。

編輯4:

現在它變得有趣。

我嘗試刪除我的表視圖中的隨機組件,如刷新控制。也嘗試擺脫我使用estimatedHeightForRowAtIndexPath,這意味着只提供一個靜態行高度,而不是使用autolayout來確定動態行高度。這兩個都沒有出現。我也嘗試完全擺脫我的自定義單元格,並使用基本的表格視圖單元格。

工作。

我試了一個帶有小標題的基本表格視圖單元格。

工作。

我嘗試了一個帶有小標題和圖像的基本表格視圖單元格。

工作。

靠近所有這些動畫相關項目的堆棧軌跡的頂部開始變得更有意義。看起來這是與自動佈局相關的。

+0

我正在遇到同樣的問題,當我向上或向下滾動時不斷插入更多行。它只是在iPhone 4下發生,而不是在5以上。這讓我瘋狂,你有沒有解決你的錯誤? – amb

+0

您是否找到解決方案?沒有崩潰評論'estimatedHeightForRowAtIndexPath'嗎?我有同樣的問題,擺脫'estimatedHeightForRowAtIndexPath',但我的應用程序有時仍然崩潰。而且很難再現,在調試模式下從未發生過。 – Spail

+0

我遇到這個問題,所以肯定會有興趣看到一個解決方案。目前我會盡量擺脫估計的高度,但顯然這不是一個長期的解決方案。 – Rupert

回答

3

從Apple技術支持工程師:

爲了保護數據存儲的完整性,核心數據捕捉一些 例外它的操作過程中發生的。有時這意味着 ,如果核心數據通過委託方法調用您的代碼,核心數據 可能最終捕捉您的代碼拋出的異常。

多線程錯誤是神祕的核心數據問題最常見的原因。

在這種情況下,核心數據通過您的controllerDidChangeContent:方法捕獲到異常,這是由於嘗試使用insertObject:atIndex引起的。

最可能的解決方法是確保您所有的NSManagedObject代碼被封裝在performBlock:performBlockAndWait:調用中。

在iOS 8和OSX Yosemite中,核心數據可以檢測並報告違反其併發模型的能力。它的工作原理是每當應用程序訪問託管對象上下文或來自錯誤調度隊列的託管對象時拋出異常。您可以通過在Xcode的Scheme Editor中將-com.apple.CoreData.ConcurrencyDebug 1傳遞給您的應用程序來啓用斷言。

CoreData ConcurrencyDebug

奧萊Begemann有great writeup of the new feature

相關問題