編集7:
これが私の保存方法です。それはかなりボイラープレートです。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 が登場し、この問題が再発しました。私は幸運。以前は、問題をテーブル ビューで推定される行の高さを使用するように絞り込んでいました (ちなみに、問題を完全に修正したことはありません。推定された行の高さの使用をやめただけです)。現在、さまざまな状況でこの問題が再び発生しています。ナビゲーション/タブバーを半透明にした数日前からのコミットまで追跡しました。これには、ストーリーボードの「スクロール ビュー インセットの調整」を無効にし、ボックスをチェックして、ビューをトップ バーとボトム バーの下に表示することが含まれます。それを実現するために必要な一連の手順がありますが、そのように構成されたストーリーボードで毎回再現できます。そのコミットを元に戻すと、それはもう起こりません。
「もう起こらない」と言っていますが、私が本当に思っているのは、これが起こりにくくなっているということです。このバグは絶対的なものです。私の直観的な反応は、これは iOS のバグだということです。これをバグレポートに変えるために何ができるかわかりません。それは狂気です。
編集5:
私の悲惨な状況をすべて読みたい場合は、この投稿を最後まで読み進めてください。この問題に直面していて、助けが必要な場合は、次のことを確認してください。
私の最後の編集では、基本的なテーブル ビュー セルを使用すると、すべてが正常に機能したことがわかりました。私の次の行動方針は、新しいカスタムセルを1つずつ構築し、それが台無しになった場所を確認することでした. というわけで、古いカスタム セル コードを再度有効にしたところ、問題なく動作しました。うーん?待って、まだestimatedHeightForRowAtIndexPath
コメントアウトしています。それらのコメントを削除して有効estimatedHeightForRowAtIndexPath
にすると、再びお粗末になりました。面白い。
API ドキュメントでそのメソッドを調べたところ、 という定数について言及されていましたUITableViewAutomaticDimension
。私が見積もっていた値は、実際には一般的なセルの高さの 1 つに過ぎないので、その定数に切り替えても問題はありません。その定数に切り替えた後、正常に動作しています。報告すべき奇妙な例外/グラフィックの不具合はありません。
元の投稿
バックグラウンドで Web サービスからデータをフェッチし、テーブル ビューにデータを表示する、かなり標準的な iPhone アプリがあります。バックグラウンド更新作業には、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)
私のアプリは、endUpdates への呼び出しで、controllerDidChangeContent で実際に例外をヒットしています。私は基本的にこれと同じことを見ています( NSFetchedResultsController は nil オブジェクトを挿入しようとしていますか?)、しかし、私はより多くの情報と再現可能なケース a を持っています。私のマージイベントはすべて挿入です。マージ中、バックグラウンド コンテキストでの保留中の挿入、削除、または更新はないようです。WWDC ビデオから performBlock と performBlockAndWait の違いを知るまで、私は当初、あらゆる場所で performBlockAndWait を使用していました。performBlock に切り替えたところ、少し改善されました。最初はスレッドの問題としてこれに取り組みましたが、ブロックを完全に理解していないために発生する奇妙なメモリの問題である可能性に分岐し、今では競合状態に戻っています。欠けているピースが 1 つだけあるようです。起こらない方法は2つあります...
(1) コンテキストに登録すると、通知が保存され、取得時に FRC デリゲートが nil アウトされ、マージ後にデリゲートが元に戻されます。これは FRC をまったく使用しないことにはほど遠いので、回避策の選択肢にはなりません。
(2) 競合状態が発生しないように、メイン スレッドを十分に長くブロックすることを行います。たとえば、テーブル ビュー デリゲートに多くのデバッグ ログ メッセージを追加すると、それが発生しないほど遅くなります。
以下は、重要なコード部分であると私が信じているものです (このすでに大きな投稿を縮小するために、特定の場所を短くしています)。
スクロール中のさまざまなポイントの後、View Controller は、これを含む関数を呼び出して、より多くのデータを要求します...
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];
}
保存とマージの実装は、興味深いところです。Save は、適切な 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);
}
}
}
保存すると、マージ通知がトリガーされます。バックグラウンドからメインにのみマージしているので、マージ呼び出しをインライン化できるかどうか、または 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];
}
興味深いかもしれないもう 1 つのコード。テーブル ビュー セルには autolayout を使用し、動的なセルの高さには新しい 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;
}
これでほぼ1週間立ち往生しています。その過程で多くのことを学びましたが、私は次に進む準備ができています. どんな提案でも大歓迎です。
編集
I put together a pretty big debug log to try to tell the story of what's going on with the udpates. I'm seeing something really strange. I'm updating the table with 50 rows at a time, so I'll only include the interesting part of my debug output. Every time a cell gets configured I'm printing out what the title was for the cell that I just dequeued as well as what the new title will be. When I hit the last cell in the table view, I make a query to the web service for more data. This output is related to the final update before I hit the exception...
// 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
In the final update it's attempting to insert at s5 r17, but I already had a cell at that row. It also attempts to insert at s5 r22, but I also already had a cell at that row. Lastly it inserts a row at s5 r25, which actually is a new row. It seems to me as though considering r17 and r22 as inserts is leaving a gap in the table. Shouldn't the previous cells at those indexes have events to be moved to r23 and r24?
My fetched results controller is using a sort descriptor that sorts by date and start time. Maybe the existing events that were at r17 and r22 aren't getting move events because there weren't any changes related to their NSManagedObjects. Essentially, they are required to move because of my sort descriptor for events earlier than them and not because their data changed.
Edit 2:
Looks like those inserts do just trigger the existing cells to shift down :(
Edit 3:
Things I tried today...
- Made AFNetworking success block waits for the merge to complete before it returns
- Made cellForRowAtIndexPath return a stale cell (essentially dequeue it and return it right away) if the fetched results controller is in the middle of beginUpdates/endUpdates. Thinking that extra random cellForRowAtIndexPath that gets called during the update may have been doing weird things.
- Removing the background context altogether. This is interesting. If I do all of the UI updates AND JSON parsing on the main context, it still happens.
Edit 4:
Now it's getting interesting.
I tried removing random components in my table view such as the refresh control. Also tried getting rid of my use of estimatedHeightForRowAtIndexPath, which meant just supplying a static row height instead of using autolayout to determine dynamic row height. Both of those turned up nothing. I also tried getting rid of my custom cell entirely, and just using a basic table view cell.
That worked.
I tried a basic table view cell with subtitle.
That worked.
I tried a basic table view cell with subtitle and image.
That worked.
The top of my stack trace being near all of those animation related items is starting to make more sense. It's looking like this is auto-layout related.