I just "solved" what appears to be a deadlock or synchronization issue with:
[NSThread sleepForTimeInterval:0.1];
in an app that attaches MPMediaItem (music/images) property references from the IPOD library to object instances, and those objects are back-stored via CoreData. My interest here is to understand exactly what's going on and what is the best practice in this situation. Here goes:
The recipe to replicate this every time is as follows:
User creates a new project.
doc = [[UIManagedDocument alloc] initWithFileURL:docURL];
if (![[NSFileManager defaultManager] fileExistsAtPath:[docURL path]]) {
[doc saveToURL:docURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
if (success) {
completionBlock(doc);
}
else {
DLog(#"Failed document creation: %#", doc.localizedName);
}
}];
Later the managedObjectContext is used to associate the object instances and hydrate the CoreData model
TheProject *theProject = [TheProject projectWithInfo:theProjectInfo
inManagedObjectContext:doc.managedObjectContext];
The user later creates a "CustomAction" object, adds a "ChElement" to it and associates a "MusicElement" with the ChElement. (These are pseudonyms for the CoreData model objects). The MusicElement is added via the IPOD library.
#define PLAYER [MPMusicPlayerController iPodMusicPlayer]
The user saves this project, then switches to an existing project that already has one CustomAction object created, with a ChElement and a MusicElement.
The user selects that ChElement from a tableView and navigates to a detailView.
When navigating away from the ChElementTVC (a subclass of a CoreData TableViewController class similar to that found in Apple docs), this is required:
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
self.fetchedResultsController.delegate = nil;
}
In the detail View, the user changes an attribute of the ChElement object and saves the project. The detailView calls its delegate (ChElementTVC) to do the saving. The save is to the UIManagedDocument instance that holds the NSManagedObject.
#define SAVEDOC(__DOC__) [ProjectDocumentHelper saveProjectDocument:__DOC__]
// Delegate
- (void)chAddElementDetailViewController:(ChDetailViewController *)sender didPressSaveButton:(NSString *)message
{
SAVEDOC(THE_CURRENT_PROJECT_DOCUMENT);
[self.navigationController popViewControllerAnimated:YES];
}
// Helper Class
+ (void)saveProjectDocument:(UIManagedDocument *)targetDocument
{
NSManagedObjectContext *moc = targetDocument.managedObjectContext;
[moc performBlockAndWait:^{
DLog(#" Process Pending Changes before saving : %#, Context = %#", targetDocument.description, moc);
[moc processPendingChanges];
[targetDocument saveToURL:targetDocument.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL];
}];
}
Since the delegate (ChElementTVC) popped the detailView off the navigation stack,
its viewWillAppear is called, and the fetchedResultsController.delegate is restored.
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (!self.fetchedResultsController.delegate) {
DLog(#"Sleep Now %#", self);
//http://mobiledevelopertips.com/core-services/sleep-pause-or-block-a-thread.html
[NSThread sleepForTimeInterval:0.1];
DLog(#"Wake up %#", self);
[self fetchedResultsControllerWithPredicate:_savedPredicate]; // App Hangs Here ... This is sending messages to CoreData objects.
[self.tableView reloadData];
}
Without the [NSThread sleepForTimeInterval:0.1]; the app hangs. When I send a SIGINT via Xcode, I get the debugger and reveal the following:
(lldb) bt
* thread #1: tid = 0x1c03, 0x30e06054 libsystem_kernel.dylib semaphore_wait_trap + 8, stop reason = signal SIGINT
frame #0: 0x30e06054 libsystem_kernel.dylib semaphore_wait_trap + 8
frame #1: 0x32c614f4 libdispatch.dylib _dispatch_thread_semaphore_wait$VARIANT$mp + 12
frame #2: 0x32c5f6a4 libdispatch.dylib _dispatch_barrier_sync_f_slow + 92
frame #3: 0x32c5f61e libdispatch.dylib dispatch_barrier_sync_f$VARIANT$mp + 22
frame #4: 0x32c5f266 libdispatch.dylib dispatch_sync_f$VARIANT$mp + 18
frame #5: 0x35860564 CoreData _perform + 160
(lldb) frame select 5
frame #5: 0x35860564 CoreData _perform + 160
CoreData _perform + 160:
-> 0x35860564: add sp, #12
0x35860566: pop {r4, r5, r7, pc}
CoreData -[NSManagedObjectContext(_NestedContextSupport) executeRequest:withContext:error:]:
0x35860568: push {r4, r5, r6, r7, lr}
0x3586056a: add r7, sp, #12
(lldb) disassemble -f
CoreData _perform:
0x358604c4: push {r4, r5, r7, lr}
... snipped ...
0x35860560: blx 0x35938bf4 ; symbol stub for: dispatch_sync_f
-> 0x35860564: add sp, #12
0x35860566: pop {r4, r5, r7, pc}
Another work-around is possible. Coding the fetchedResultsController.delegate restoration in -[ChElementTVC viewDidAppear:] also effectively delays this setting on the main queue.
An additional work-around is to execute the nav pop in a completion block after the project saving is finished:
#define SAVEDOCWITHCOMPLETION(__DOC__,__COMPLETION_BLOCK__)[ProjectDocumentHelper saveProjectDocument:__DOC__ completionHandler:__COMPLETION_BLOCK__]
void (^completionBlock)(BOOL) = ^(BOOL success) {
[self.navigationController popViewControllerAnimated:YES];
};
SAVEDOCWITHCOMPLETION(THE_CURRENT_PROJECT_DOCUMENT, completionBlock);
I think the save operation runs in the background concurrently with the delegate restoration on the main queue, but I do not know how to examine/prove/disprove that theory.
So, with that, can someone explain what's going on and what is the best practice in this situation? Also, references for study are appreciated.
I ended up implementing the third method, that is, saving the document with a completion block to serialize the transactions interacting with the CoreData store.
Related
I have a situation where I am trying to resolve these Crashlytics issues and I have this crash log
Thread : Crashed: com.apple.main-thread
0 libobjc.A.dylib 0x34217f46 objc_msgSend + 5
1 UIKit 0x29a2d5a3 -[UIWebView webView:decidePolicyForNavigationAction:request:frame:decisionListener:] + 182
2 CoreFoundation 0x2630cad4 __invoking___ + 68
3 CoreFoundation 0x26239645 -[NSInvocation invoke] + 300
4 CoreFoundation 0x2623d0c7 -[NSInvocation invokeWithTarget:] + 50
5 WebKitLegacy 0x326d9261 -[_WebSafeForwarder forwardInvocation:] + 224
6 CoreFoundation 0x2630b62f ___forwarding___ + 354
7 CoreFoundation 0x2623d008 _CF_forwarding_prep_0 + 24
8 CoreFoundation 0x2630cad4 __invoking___ + 68
9 CoreFoundation 0x26239645 -[NSInvocation invoke] + 300
10 WebCore 0x31c02729 HandleDelegateSource(void*) + 100
11 CoreFoundation 0x262cefbf __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 14
12 CoreFoundation 0x262ce461 __CFRunLoopDoSources0 + 364
13 CoreFoundation 0x262cca35 __CFRunLoopRun + 772
14 CoreFoundation 0x2621a3b1 CFRunLoopRunSpecific + 476
15 CoreFoundation 0x2621a1c3 CFRunLoopRunInMode + 106
16 GraphicsServices 0x2d801201 GSEventRunModal + 136
17 UIKit 0x2988443d UIApplicationMain + 1440
18 abc 0x0030dcd7 main (main.m:14)
I can understand that its some callback on webview delegate and bad excess has occurred, so to rectify this I handled the delegates via
[self.webview stopLoading];
self.webview.delegate =nil;
in all the classes, yet I can see this crash. Can you enlighten me what's possibly going wrong and some approach to rectify this?
The following might be the case here
The user is presented a screen with UIWebView
The UIViewController sets self as the delegate Web page starts downloading
The User quits screen
UIViewController gets deallocated UIWebView finishes loading and sends I am finished loading message to its delegate
or
some other delegate method gets called when the webview object is no more.i.e dangling pointer effect
1.Always make sure you stop loading the webView and remove the delegate before leaving the view
Before releasing an instance of UIWebView for which you have set a
delegate, you must first set its delegate property to nil. This can
be done, in your dealloc method
Here is the reference
// If ARC is used
- (void)dealloc {
[_webView setDelegate:nil];
[_webView stopLoading];
}
// If ARC is not used
- (void)dealloc {
[webView setDelegate:nil];
[webView stopLoading];
[webView release];
[super dealloc];
}
// ARC - Before iOS6 as its deprecated from it.
- (void)viewWillUnload {
[webView setDelegate:nil];
[webView stopLoading];
}
2.Make sure you are not stopLoading and setDelegate to nil in viewWillDisappear
if the ViewController is a child of a another ViewController, u can
trigger the removal of the ViewController's view from the parent
ViewController's view with an animation. At the same time, u can
remove the ViewController from its parent and nil out its reference.
at this point ViewController will be nil and viewWillDisappear
will never be called, meaning the WebView delegate will never be
cleaned up
Use dealloc and ensure that your WebView is always cleaned up.
3.Make sure you set the ContentOffset of the subviews of webview to CGPointZero without animation
In iPad in some versions while webview is scrolling if you close the parent
viewcontroller without setting ContentOffset to CGPointZero this
kind of problems will come
so its better to you call the following code of in parent viewcontroller before closing it
for (id subview in webView.subviews){
if ([[subview class] isSubclassOfClass: [UIScrollView class]]){
[subview setContentOffset:CGPointZero animated:NO];
}
}
Hope this helps.Feel free to ask your doubts.
4.Generally speaking You should not embed UIWebView objects in UIScrollView objects. If you do so, unexpected behavior can result because touch events for the two objects can be mixed up and wrongly handled.
Here is the reference
Try disabling your UIWebView's scrolling behaviour before the ViewController deallocs it
for (id subview in webView.subviews){
if ([[subview class] isSubclassOfClass: [UIScrollView class]]){
[subview setContentOffset:CGPointZero animated:NO];
}
}
p.s. Dipen Chudasama's approach is correct, according to Apple's documentation, you should really set the delegate property to nil before releasing the webview, assuming you have released the webview correctly inside dealloc function but not viewWillDisappear
UIWebViews delegate uses assign and not weak. So you need to nullify the delegate when the webView's controller gets deallocated.
Example:
- (void)dealloc
{
self.webview.delegate =nil;
}
Are you adding a web view within your subclass of a web view? Generally that's the problem and if so, changing the superclass to a UIView would solve the problem.
Use visa versa means first nil your delegate and then after stop loading web view may be help you.
like this.
[_webView setDelegate:nil];
[_webView stopLoading];
As per apple document : Important Before releasing an instance of UIWebView for which you have set a delegate, you must first set its delegate property to nil.
Use [listener use] instead, to tell your UIWebView to handle clicked URL.
-(void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id )listener
{
[listener use]
}
I'm having a weird issue with UIViews and manual memory management.
I have a view (contentView) which is the main view of a view controller.
After a long press on the contentView, another view is supposed to fade in (on top of it).
When the gestures ends, the additional view fades out.
The issue is:
When the contentView receives a long press, I create the auxiliary view, add it to the contentView, and then release it, which is/was the common practice back in the pre-ARC days.
It works okay on the iPhone, but it crashes on the iPad!
The crashy line is:
[ZPNowPlayingItemInfoView dealloc]
...which gets triggered when I remove the auxiliary view from the contentView.
Any clues on why this happens?
If I comment out the release line (see my comment in the code), it works flawlessly on both devices, but it feels bad.
Here's the code:
-(void)longPressDetected:(UILongPressGestureRecognizer*)longPressGR
{
//Content view of the view controller I'm in
UIView *contentView = MSHookIvar<UIView*>(self, "_contentView");
if (longPressGR.state == UIGestureRecognizerStateBegan) {
id item = MSHookIvar<MPAVItem*>(self, "_item");
ZPNowPlayingItemInfoView *infoView =
[[ZPNowPlayingItemInfoView alloc] initWithFrame:
CGRectMake(0,0,contentView.frame.size.width,contentView.frame.size.height)
item:item];
//infoView retain count: 1
[infoView setAlpha:0.f];
[contentView addSubview:infoView];
//infoView retain count: 3 (???)
//iPad goes berserk on this line
//Commented - Works both on iPhone and iPad
//Uncommented - Works only on iPhone
//[infoView release];
//infoView retain count: 2 (if release is uncommented)
[UIView animateWithDuration:0.35f animations:^{
[infoView setAlpha:1.0f];
} completion:^(BOOL finished) {
//infoView retain count: 3
}];
} else if (longPressGR.state == UIGestureRecognizerStateEnded) {
ZPNowPlayingItemInfoView* infoView = nil;
for (UIView *subview in contentView.subviews) {
if ([subview isKindOfClass:[ZPNowPlayingItemInfoView class]]) {
infoView = (ZPNowPlayingItemInfoView*)subview;
break;
}
}
[UIView animateWithDuration:0.35f animations:^{
[infoView setAlpha:0.f];
} completion: ^(BOOL finished){
[infoView removeFromSuperview];
}];
}
P.S. I need to use manual memory management. This is a tweak for jailbroken devices.
Stack trace:
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0 libobjc.A.dylib 0x195287bdc 0x19526c000 + 0x1bbdc // objc_msgSend + 0x1c
1 + Musix.dylib 0x10015b19c 0x100154000 + 0x719c // -[ZPNowPlayingItemInfoView dealloc] + 0x48
2 libsystem_blocks.dylib 0x19590d90c 0x19590c000 + 0x190c // _Block_release + 0xfc
3 UIKit 0x188ef8590 0x188eb0000 + 0x48590 // -[UIViewAnimationBlockDelegate dealloc] + 0x44
4 CoreFoundation 0x1845f1374 0x1845ec000 + 0x5374 // CFRelease + 0x208
5 CoreFoundation 0x184601004 0x1845ec000 + 0x15004 // -[__NSDictionaryI dealloc] + 0x8c
6 libobjc.A.dylib 0x19528d720 0x19526c000 + 0x21720 // (anonymous namespace)::AutoreleasePoolPage::pop(void*) + 0x230
7 CoreFoundation 0x1845f4f90 0x1845ec000 + 0x8f90 // _CFAutoreleasePoolPop + 0x18
8 CoreFoundation 0x1846c774c 0x1845ec000 + 0xdb74c // __CFRunLoopRun + 0x5d8
9 CoreFoundation 0x1845f51f0 0x1845ec000 + 0x91f0 // CFRunLoopRunSpecific + 0x188
10 GraphicsServices 0x18d7575a0 0x18d74c000 + 0xb5a0 // GSEventRunModal + 0xa4
11 UIKit 0x188f26780 0x188eb0000 + 0x76780 // UIApplicationMain + 0x5cc
12 Music (*) 0x10006ee28 0x100064000 + 0xae28 // 0x0000adac + 0x7c
13 libdyld.dylib 0x1958e2a04 0x1958e0000 + 0x2a04 // start + 0x0
ZPNowPlayingItemInfoView:
#interface ZPNowPlayingItemInfoView()
#property (nonatomic, retain) MPAVItem* item;
#property (nonatomic, retain) MPUSlantedTextPlaceholderArtworkView *artworkView;
#property (nonatomic, retain) UILabel *artistLabel;
#property (nonatomic, retain) UILabel *albumLabel;
#property (nonatomic, retain) UILabel *songLabel;
#end
ZPNowPlayingItemInfoView dealloc:
-(void)dealloc
{
[super dealloc];
[self.item release];
[self.artworkView release];
[self.artistLabel release];
[self.songLabel release];
}
You have some problem in ZPNowPlayingItemInfoView class. When this problem happens? Only when the object gets deallocated. When you comment [infoView release] out, your object is never deallocated and the problem doesn't arise - you will have a memory leak though.
Inspect what ZPNowPlayingItemInfoView does, especially its dealloc method. Are you sure you are constructing it correctly? Is item always a valid object?
After seeing the ZPNowPlayingItemInfoView dealloc method, the problem is quite clear - [super dealloc] must always be the last call, not the first one. Once you have deallocated the object, accessing its properties is an undefined operation.
When commenting out the release is a working workaround, that indicates that you have released it once too often. It may well be the very one release that you commented out.
removeFromSuperview does reduce the retain count by 1.
I suggest re-visiting the full life cycle of the view object. This can be tricky though. Each retain needs to have exactly one corresponding release or autorelease. Assigning the view to a property using its getter (self.myView = subview) does retain it and re-assigning another view to the property (self.myView = someOhterview) releases subview.
On the contrary accessing the iVar directly (myView = subview) does not maintain the release/retain-cycle.
There is more than that. Adding the view and removing it from an array, set or dictionary will change the retain count accordingly.
So go and have a deeper look at it. Use instruments to observe the retain count.
I'm getting the following error suddenly every time I run my app:
CoreData: FATAL ERROR: The persistent cache of section information does not match the current configuration. You have illegally mutated the NSFetchedResultsController's fetch request, its predicate, or its sort descriptor without either disabling caching or using +deleteCacheWithName:
And here's the first ten items in the call stack:
*** First throw call stack:
(
0 CoreFoundation 0x04b835e4 __exceptionPreprocess + 180
1 libobjc.A.dylib 0x022c68b6 objc_exception_throw + 44
2 CoreData 0x020a1b41 -[NSFetchedResultsController performFetch:] + 913
3 Syllable 0x00036aa9 -[RootViewController fetchedResultsController] + 777
4 Syllable 0x0003772b -[RootViewController tableView:numberOfRowsInSection:] + 91
5 UIKit 0x00d1d240 -[UISectionRowData refreshWithSection:tableView:tableViewRowData:] + 2510
6 UIKit 0x00d20b3d -[UITableViewRowData numberOfRows] + 98
7 UIKit 0x00ba94c2 -[UITableView noteNumberOfRowsChanged] + 120
8 UIKit 0x00ba8e6f -[UITableView reloadData] + 814
9 UIKit 0x10378ed1 -[UITableViewAccessibility(Accessibility) reloadData] + 50
10 UIKit 0x00bac8d3 -[UITableView _reloadDataIfNeeded] + 65
There's a lot more it gives, but it seems to all stem from that first error I posted. If they're helpful, tell me and I'll post them.
The issue is really weird, though. It seems to have came out of nowhere. Even if I use git to revert back to a previously working version it still crashes, so I have no idea what caused it.
There's a solution posted on Stackoverflow but it's a little unnerving. Will setting the cache to nil remove caching ability from my app/make it slower? What rammifications will it bring about?
I'm working on a new update to my app and priority one is to make sure this doesn't crash for my pre-existing/future users.
EDIT: I believe this is what caused the issue (though I've since deleted it and the issue persists):
I have the following code in my applicationDidFinishLaunching in my AppDelegate, which basically serves to create a special object in Core Data on the app's first launch. Right after adding this the issue seemed to occur, despite deleting the code shortly thereafter. Should I not be manipulating Core Data at this point (is it too early)?
// If it's the first time launching, create and add a sample article for an introduction
if ([[[NSUserDefaults standardUserDefaults] objectForKey:#"IsFirstTimeLaunching"] isEqualToString:#"YES"]) {
NSManagedObjectContext *context = self.managedObjectContext;
Article *article = [NSEntityDescription insertNewObjectForEntityForName:#"Article" inManagedObjectContext:context];
article.source = #"text";
article.body = #"some text";
article.timeStamp = [NSDate date];
NSError *error;
[context save:&error];
[[NSUserDefaults standardUserDefaults] setObject:#"NO" forKey:#"IsFirstTimeLaunching"];
}
I also have this block in my AppDelegate that has been there for quite awhile, but to be honest I'm not sure what it does exactly, I may have just wrote it and forgot to finish it...
- (void)applicationDidEnterBackground:(UIApplication *)application
{
NSManagedObjectContext *context = self.managedObjectContext;
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Article" inManagedObjectContext:context];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entity];
}
You have these options:
Set the cache to nil. This is actually OK if you do not experience performance problems. If the fetches take very long a FRC cache can really help a lot with speed. First try this solution.
Devise a scheme with different cache names. Your FRC could determine which fetch it has to perform during lazy instantiation and use the appropriate cache name. For example, if you have a search controller, you could use the state of the scope buttons.
For this solution (and if you use only one cache name) it is important that you do not call performFetchon the fetched results controller if you have changed the FRC's fetchRequest. That is exactly what causes this crash. Rather you can set the FRC to nil and let it create itself lazily.
Delete the cache with deleteCacheWithName: at the appropriate time.
Edit 7:
Here's my save method. It's pretty boilerplate. The DEBUG_LOG() macros are only executed if it's a debug build.
- (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);
}
}
Edit 6:
iOS 8 is here and this problem is back. Lucky me. Previously I had narrowed the problem down to using estimatedRowHeight on table views (btw, I never fully fixed the problem. I just stopped using estimatedRowHeight). Now I'm seeing this problem again under different circumstances. I tracked it down to a commit from a few days ago when I made my nav/tab bars translucent. This included disabling 'Adjust Scroll View Insets' in storyboard and checking the boxes to have my views display under top bars and bottom bars. There's a series of steps I need to do to make it happen, but I can reproduce it every time with my storyboard configured that way. If I revert that commit it no longer happens.
While I say, "it no longer happens," what I really think is that this is just making it less likely to happen. This bug is an absolute b****. My gut reaction now is that this is an iOS bug. I just don't know what I can do to turn this into a bug report. It's madness.
Edit 5:
If you want to read the entirety of my misery, please continue all the way through this post. If you're running into this problem and you just want some help, here's something to look into.
My last edit noted that when I used a basic table view cell, everything worked fine. My next course of action was going to be to try from scratch building a new custom cell piece by piece and seeing where it messed up. For the hell of it, I re-enabled my old custom cell code and it worked just fine. Uhhh? Oh wait, I still have estimatedHeightForRowAtIndexPath commented out. When I removed those comments and enabled estimatedHeightForRowAtIndexPath, it got crappy again. Interesting.
I looked up that method in the API doc, and it mentioned something about a constant called UITableViewAutomaticDimension. The value I was estimating was really just one of the common cell heights, so it wouldn't hurt to switch to that constant. After switching to that constant it's working properly. No weird exceptions/graphical glitches to report.
Original post
I have a pretty standard iPhone app that fetches data from a web service in the background and displays data in a table view. The background updating work has its own managed object context configured for NSPrivateQueueConcurrencyType. My table view's fetched results controller has its own managed object context configured for NSMainQueueConcurrencyType. When the background context parses new data it passes that data to the main context via mergeChangesFromContextDidSaveNotification. Sometimes during the merge, my app hits an exception here...
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
Here's the exception I see...
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)
My app is actually hitting the exception in controllerDidChangeContent, at my call to endUpdates. I'm basically seeing the same thing as this (NSFetchedResultsController attempting to insert nil object?), but I've got more info and case a that's reproducible. All of my merge events are inserts. During the merge, there doesn't seem to be any pending inserts, deletes, or updates on the background context. I was initially using performBlockAndWait all over the place until I learned about the difference between performBlock and performBlockAndWait from the WWDC video. I switched to performBlock, and that made it a little bit better. Initially I approached this as a threading issue, diverged into the possibility of it being a weird memory problem caused by not fully understanding blocks, and now I'm back to it being a race condition. It seems like there's just one piece that I'm missing. There are two ways it doesn't happen...
(1) Register for the context will save notification, nil out the FRC delegate when I get it, and set the delegate back after the merge. This isn't far from not using an FRC at all, so this really isn't an option for a workaround.
(2) Do things that block the main thread long enough, so the race condition doesn't happen. For example, when I add a lot of debug log messages to my table view delegate, that slows it down enough for it not to happen.
Here are what I believe to be the important pieces of code (I've shortened certain spots to shrink this already large post).
After various points during scrolling, the view controller will request more data by calling a function that has this in it...
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];
For the most part, parseJSON doesn't really have anything interesting in it...
- (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];
}
The implementation for save and merge are where it might get interesting. Save expects to be called from within the appropriate performBlock already, so it doesn't do anything with the 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);
}
}
}
Upon saving, the merge notification gets triggered. I'm only merging from background to main, so I pretty much just want to know if I can inline the merge call or if I need to do it inside of performBlock.
- (void)backgroundManagedObjectContextDidSave:(NSNotification *)notification
{
if (![NSThread isMainThread]) {
[mainMOC performBlock:^ {
[self.mainMOC mergeChangesFromContextDidSaveNotification:notification];
}];
} else {
[mainMOC mergeChangesFromContextDidSaveNotification:notification];
}
}
My fetched results controller delegate methods are pretty boiler plate stuff...
- (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];
}
One other piece of code that might be of interest. I'm using autolayout for my table view cells, and the new estimatedHeightForRowAtIndexPath API for dynamic cell height. What this means is that during the call to [self.tableView endUpdates], the last step actually reaches down into some managed objects, whereas the other calls for number of sections/rows only need to know counts from the 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;
}
Been stuck on this for almost a week now. Learned a ton in the process, but geez I'm ready to move on. Any suggestions would be greatly appreciated.
Edit
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.
From an Apple Technical Support Engineer:
To protect the integrity of the datastore, Core Data catches some
exceptions that happen during its operations. Sometimes this means
that if Core Data calls your code through a delegate method, Core Data
may end up catching exceptions your code threw.
Multi-threading errors are the most common cause of mysterious Core Data issues.
In this case, Core Data caught an exception through your controllerDidChangeContent: method, caused by trying to use insertObject:atIndex.
The most likely fix is to ensure that all your NSManagedObject code is encapsulated inside performBlock: or performBlockAndWait: calls.
In iOS 8 and OSX Yosemite, Core Data gains the ability to detect and report violations of its concurrency model. It works by throwing an exception whenever your app accesses a managed object context or managed object from the wrong dispatch queue. You enable the assertions by passing -com.apple.CoreData.ConcurrencyDebug 1 to your app on the command line via Xcodeʼs Scheme Editor.
Ole Begemann has a great writeup of the new feature.
I am implementing the AQRecorder class from Apple's SpeakHere example into my project using ARC. To get it to compile, I had to create a class (AQRecorderController) that controls the AQRecorder instance (equivalent to the SpeakHereController in the example). AQRecorderController is connected through the nib of my main view controller and implemented as a property. The problem occurs whether or not the property is strong or weak.
My problem is that shortly after loading the view controller, the AQRecorderController is released, but only when tested on device. In the simulator, this does not occur. It occurs for iPad and iPhone, iOS 5 and iOS 6. I need to maintain this reference throughout the lifetime of my view controller for recording purposes (you can't delete the recorder while recording and expect to have a finished file).
Has anyone run into this or anything similar? If the AQRecorderController property is strong, I get a bad access error when trying to use it, if its weak, I just get a nil, and its unusable.
Any help would be greatly appreciated.
formViewController.h:
#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
#class AQRecorderController;
#interface formViewController : UIViewController <UIActionSheetDelegate, UITableViewDelegate, UIGestureRecognizerDelegate> {
IBOutlet AQRecorderController *aqRecorderController;
}
#property (nonatomic, weak) IBOutlet AQRecorderController *aqRecorderController;
#end
AQRecorderController.h
#import <Foundation/Foundation.h>
#import "AQRecorder.h"
#interface AQRecorderController : NSObject
{
AQRecorder *aqRecorder;
}
#property (readonly) AQRecorder* aqRecorder;
#property (nonatomic, assign) bool isRecording;
#property (nonatomic, strong) NSString* fileName;
-(bool)startRecording;
-(bool)pauseRecording;
-(bool)stopRecording;
-(bool)initializeRecordSettingsWithCompression:(bool)compressionEnabled;
#end
formView.xib:
Here is the stack trace after the AQRecorderController has been released:
2012-10-23 10:34:09.600 TestApp[510:907] (
0 TestApp 0x000f32ab
-[AQRecorderController dealloc] + 138
1 CoreFoundation 0x32247311 CFRelease + 100
2 CoreFoundation 0x3225195d <redacted> + 140
3 libobjc.A.dylib 0x31ad5489 <redacted> + 168
4 CoreFoundation 0x32249441 _CFAutoreleasePoolPop + 16
5 Foundation 0x37303a7f <redacted> + 466
6 CoreFoundation 0x322db5df <redacted> + 14
7 CoreFoundation 0x322db291 <redacted> + 272
8 CoreFoundation 0x322d9f01 <redacted> + 1232
9 CoreFoundation 0x3224cebd CFRunLoopRunSpecific + 356
10 CoreFoundation 0x3224cd49 CFRunLoopRunInMode + 104
11 GraphicsServices 0x32fb52eb GSEventRunModal + 74
12 UIKit 0x34e92301 UIApplicationMain + 1120
13 TestApp 0x00081a9d main + 48
14 TestApp 0x0005aa68 start + 40
)
This is where the recorder is instantiated.
AQRecorderController.mm:
- (void)awakeFromNib
{
aqRecorder = new AQRecorder();
}
This is where the recorder is used. By this point, the AQRecorderController has been released and this code never executes (it causes a crash, because the AQRecorderController has been deallocated).
-(bool)startRecording
{
if (aqRecorder->IsRunning())
{
[self stopRecording];
}
else // If we're not recording, start.
{
#try
{
// Start the recorder
CFStringRef filenameString = (CFStringRef)CFBridgingRetain(self.fileName);
aqRecorder->StartRecord(filenameString);
}
#catch(NSException *ex)
{
NSLog(#"Error: %#", [ex description]);
return NO;
}
[self setFileDescriptionForFormat:aqRecorder->DataFormat() withName:#"Recorded File"];
}
[self checkIfRecording];
return YES;
}
Here is where the AQRecorderController is instantiated.
formViewController.mm:
//this is called in viewDidAppear
-(void)initializeAQRecorder: (NSString*)soundFileName
{
aqRecorderController = [[AQRecorderController alloc] init];
NSLog(#"AQRecorderController is being initialized for file %#",soundFileName);
NSArray *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDir = [documentPaths objectAtIndex:0];
NSString *soundFilePath =[[NSString alloc] initWithFormat:#"%#",[documentsDir stringByAppendingPathComponent:soundFileName]];
[aqRecorderController setFileName:soundFilePath];
[aqRecorderController initializeRecordSettingsWithCompression:NO];
}
My problem is that shortly after loading the view controller, the
AQRecorderController is released...I need to maintain this reference
throughout the lifetime of my view controller
Mark your property strong instead of weak. weak means that the object pointed to by aqRecorderController won't be retained by the setter; strong will cause it to be retained.
If the AQRecorderController property is strong, I get a bad access
error when trying to use it, if its weak, I just get a nil, and its unusable.
That sounds like the property is being set to some invalid value somewhere in your program. Since you can't manually retain the object under ARC and you've marked the property weak, it may be released very early on. I'm not sure why you'd have a problem if you mark it strong... it'd help to see the code where you set the variable or property.
You're never setting the AQRecorderController to your formViewController from what I see. You need to do self.aqRecorderController = aqRecorderController, I believe it's just disappearing as soon as you leave the scope where you create the controller.
I got it working for now. I haven't completely fixed it, but I can record without it crashing. I commented out every line having to do with AQRecorderController until it stopped being released, then slowly added them back until I found out where it happens. It looks like the audio session setup code somehow provokes it to release the controller. This is the code that causes it (but no errors are thrown here):
From AQRecorderController.mm:
-(void)initializeRecordSettingsWithCompression:(bool)compressionEnabled
{
OSStatus error = AudioSessionInitialize(NULL, NULL, interruptionListener, (__bridge void*)self);
if (error) printf("ERROR INITIALIZING AUDIO SESSION! %d\n", (int)error);
else
{
UInt32 category = kAudioSessionCategory_PlayAndRecord;
error = AudioSessionSetProperty(kAudioSessionProperty_AudioCategory, sizeof(category), &category);
if (error) printf("couldn't set audio category!");
error = AudioSessionAddPropertyListener(kAudioSessionProperty_AudioRouteChange, propListener, (__bridge void*)self);
if (error) printf("ERROR ADDING AUDIO SESSION PROP LISTENER! %d\n", (int)error);
UInt32 inputAvailable = 0;
UInt32 size = sizeof(inputAvailable);
// we do not want to allow recording if input is not available
error = AudioSessionGetProperty(kAudioSessionProperty_AudioInputAvailable, &size, &inputAvailable);
if (error) printf("ERROR GETTING INPUT AVAILABILITY! %d\n", (int)error);
// we also need to listen to see if input availability changes
error = AudioSessionAddPropertyListener(kAudioSessionProperty_AudioInputAvailable, propListener, (__bridge void*)self);
if (error) printf("ERROR ADDING AUDIO SESSION PROP LISTENER! %d\n", (int)error);
error = AudioSessionSetActive(true);
if (error) printf("AudioSessionSetActive (true) failed");
}
}
So far, this isn't necessary for the functioning of my app, but I am curious as to why it would cause the AQRecorderController instance to release.