0

I have an iPhone/iPad app where one of the main functions is to arrange objects on a plot. I use Core Data to manage the objects' relationships; for this example, I'll talk about Units, which have a to-one relationship to a Plot (and a to-many inverse). Units' properties include positionX, positionY, and angle.

Each Unit (inherits from NSManagedObject) is paired with a UnitViewController (inherits from UIViewController). The Unit has a property .viewController and the UnitViewController has a property .object, so in different uses they can refer to each other. These are set when the Plot is opened or new Units are added (or re-added from Undo, etc).

Each UnitViewController has a UIPanGestureRecognizer for its view, and when that gesture occurs, the UnitViewController changes its .object's positionX and positionY values. When that happens, the UnitViewController then observes those changes through KVO and re-positions the view.

This may seem convoluted, but the reason I did it this way is that I can also change the position numerically in a UITableView. Here's an abbreviated version of the code, showing the crucial bits of the path. Some of these methods exist in my UIViewController+LD category, hence the general names.

- (IBAction)dragObject:(UIPanGestureRecognizer *)gesture
{
    // GESTURE BEGAN
    if ([gesture state] == UIGestureRecognizerStateBegan) {
        [self beginGesture:gesture];
        if (!_selected) [self setSelected:YES];
    }

    // turn on registration before last pass
    if ([gesture state] == UIGestureRecognizerStateEnded || [gesture state] == UIGestureRecognizerStateCancelled) {
        [[NSNotificationCenter defaultCenter] postNotificationName:ENABLE_UNDO_REGISTRATION object:nil];
    }

    // MOVE
    [self dragUnitWithGesture:gesture];

    // turn off registration after first pass
    if ([gesture state] == UIGestureRecognizerStateBegan) {
        [[NSNotificationCenter defaultCenter] postNotificationName:DISABLE_UNDO_REGISTRATION object:nil];
    }

    // GESTURE ENDED
    if ([gesture state] == UIGestureRecognizerStateEnded ||
        [gesture state] == UIGestureRecognizerStateCancelled) {
        [self endGesture];
    }
}

- (void)dragUnitWithGesture:(UIPanGestureRecognizer *)gesture
{
    CGPoint translation = [gesture translationInView:self.view.superview];
            [self saveNewObjectCenterWithTranslation:translation];
}

- (void)saveNewObjectCenterWithTranslation:(CGPoint)translation
{
    [self saveNewObjectCenter:CGPointMake(initialCenter.x + translation.x, initialCenter.y + translation.y)];
}

- (void)saveNewObjectCenter:(CGPoint)center
{
    CGPoint dataPoint = [Converter dataPointFromViewPoint:center];
    self.object.positionX = [NSNumber numberWithFloat:dataPoint.x];
    self.object.positionY = [NSNumber numberWithFloat:dataPoint.y];
}

- (void)beginGesture:(UIGestureRecognizer *)gesture
{
    [[NSNotificationCenter defaultCenter] postNotificationName:BEGIN_UNDO_GROUPING object:nil];

    self.initialCenter = self.view.center;
}

- (void)endGesture
{
    NSError *error = nil;
    [self.object.managedObjectContext save:&error];

    [[NSNotificationCenter defaultCenter] postNotificationName:END_UNDO_GROUPING object:nil];
}

My issue comes from crash reports obtained through Crashlytics, because I cannot replicate the crash on my devices. There have been multiple reports that all occur with the stack trace:

_UIGestureRecognizerSendActions
-[UnitViewController dragObject:]
-[UnitViewController dragUnitWithGesture:]
-[UnitViewController saveNewObjectCenterWithTranslation:]
-[UIViewController(LD) saveNewObjectCenter:]
_sharedIMPL_setvfk_core + 110
-[NSObject(NSKeyValueObserverNotification) willChangeValueForKey:] + 180
NSKeyValueWillChange + 474
NSKeyValuePushPendingNotificationPerThread + 214

This particular one ended with:

Crashed: com.apple.main-thread
EXC_BAD_ACCESS KERN_INVALID_ADDRESS at 0x00000000

But I've also seen:

Fatal Exception: NSInternalInconsistencyException
An -observeValueForKeyPath:ofObject:change:context: message was received but not handled (keyPath: positionX)

Fatal Exception: NSObjectInaccessibleException
Core Data could not fulfill a fault for ‘0x00000000 <x-coredata://xxxxxx/Unit/p116>'

So my question: is there a known issue with this type of data modification? Is it simply too fast for the Core Data framework to handle? Or is there something else I could be doing wrong? This problem is only one of the ways Core Data issues have manifested in my app, and I'd love to get to the heart of the matter to make my app more stable.

Update: I don't have enough reputation to post images, but here's a link to the full stack trace: stackTrace

Marco
  • 6,692
  • 2
  • 27
  • 38
ccombe
  • 1
  • 3

3 Answers3

1

As much as I agree with Marcus on the spirit of reducing the number of save: calls, I'm not convinced that save: frequency is the source of this particular problem because it's only called when the pan gesture ends.

Instead, the crash-log screenshot looks very much like a KVO observer has been deallocated without first being removed as an observer.

Review your code and make sure that all addObserver:forKeyPath:context: calls are balanced by removeObserver:forKeyPath:context: and that it's not possible for your observer(s) to be deallocated without first removing their observances.

If you can't spot where it might be going wrong then perhaps edit your question to include the KVO-related code so we can take a look at it.

As for the could not fulfill a fault exception, that's a different issue and probably deserves a different stack overflow question, complete with the full stack trace.

Trevor Squires
  • 531
  • 4
  • 7
  • Trevor, it's interesting that you mention KVO observers. Each Core Data object can have two possible observers: its view on the Plot, and a "detail view" which would be called UnitDetailTableViewController (subclass of UITableViewController). This detail view can be shown or hidden on the side of the screen (as a popover on the iPad or as a modal view on the iPhone). I add it as an observer in its viewWillAppear method and remove it in its viewWillDisappear method. – ccombe Feb 09 '14 at 13:09
  • What's very interesting is that I also have errors for each detail view controller: cannot remove an observer because it is not registered as an observer. So somewhere, I'm getting one too many calls to viewWillDisappear. I don't know how a view could disappear without appearing first, but it seems to be happening. Perhaps these issues are linked? – ccombe Feb 09 '14 at 13:11
  • There is no doubt in my mind: they are linked. You bear the sole responsibility for adding and removing the KVO observer so you shouldn't rely on things outside your control (like appearance callbacks) to be the authority on when (and whether) to remove your observer. I recommend that you use a boolean property in the observer as a marker for whether or not it's observing. In the observer's appearance callbacks *and* dealloc method, check that marker to verify whether you should add or remove the observer. – Trevor Squires Feb 09 '14 at 23:01
  • Did you ever track down the problem? I'm curious to know if it was a KVO observer issue or if it really was related to save frequency. – Trevor Squires Mar 06 '14 at 17:46
0

Core Data can handle a surprising amount of data at just about any pace you want. However your code may be doing more work than is needed.

First, you appear to be saving on each move. That is completely unnecessary. Core Data works the same whether the entity is saved or not. However, saving to disk is expensive. So if you are saving on every movement/change you can be causing a problem. Removing those saves will only help. Consider saving only when the user expects a pause: leaving the app, changing views, etc.

Second, what thread is this crash coming from? If you have a multi-threaded situation here and you are receiving notifications on the wrong thread you can get some odd crashes. The UI expects/demands that you interact with it only on the main thread (thread 0). Core Data has some rules around threading as well. I cannot tell from your snippet of the stack trace if this is an issue. Posting more of the stack trace will help.

Update

Saving that often hurts the user experience because you are blocking the main thread on each save. While it should not directly cause a crash it could in theory cause notifications to get "queued up" waiting for the writes to be done. Better to save every X seconds or something than block on each notification firing.

Can you post the full stack trace? The snippet you showed has removed a great deal of information that will help to diagnose the problem.

As for not duplicating, that tends to also point towards a threading issue. Not every CPU is identical. A threading issue can show up on a small subset of devices and not duplicatable at all on others. I once had a threading issue show up on a single device.

Update

Further reflection. Wiring up User Events to saving in Core Data is just bad imho. There is no guarantee that the user is going to do anything sane. You would be far better off having the user event kick off a timer. If the timer is already present then kick it out a bit further. When the timer fires, you save.

That will give you a bit of disconnect and protect you from user behavior.

Marcus S. Zarra
  • 46,571
  • 9
  • 101
  • 182
  • Marcus, I save on each move for data security; while in theory, saving only on app quitting or entering the background is fine, I don't quite trust it enough to not crash (obviously). I'd rather it do a little more work, if it means a crash only loses the last move the user made. Is it possible that could make the app crash, though? Trying to modify a Core Data structure while it's saving? I think all crashes are coming from the main thread, but maybe I'm reading the report wrong. I've been aware of Core Data's issues with multithreading, and I only use other threads to save images to disk. – ccombe Feb 07 '14 at 20:38
  • The thing that really frustrates me is that I can't duplicate these crashes. Is there something in the Xcode debugger that prevents it from happening? I've had a number of multithreaded Core Data crashes during development, and it had no problem seeing those... – ccombe Feb 07 '14 at 20:39
  • Thanks for the advice. I'm currently in touch with Crashlytics about their threading notation. If it's multithreading, I'll just have to find that split and fix it. – ccombe Feb 07 '14 at 21:24
  • I'll play with the idea of a timed save rather than a user action based save. Or perhaps there are other actions that happen less frequently that could trigger a save... – ccombe Feb 07 '14 at 21:24
0

Answered

After a lot of digging and fixing of other bugs, I think I've figured out the problem. I haven't fixed it yet, but I'm able to repeat crashing behavior, so it's only a matter of how many hours it'll take to sort it out.

The problem lies in an ill-conceived undo grouping, which un-does part of an action but not all of it, and when that grouping is undone, a number of different crashes can occur, depending on what action is performed next. Basically, I'm left with a faulted NSManagedObject subclass instance that doesn't exist anymore, and the view controller that reacts to these other actions tries to call it from various places. When that happens, the app obviously crashes, but since none of the actions in the stack trace itself indicate the actual cause of the problem, it was hard to track down.

So this has nothing to do with save frequency, which is good. I'm still curious to see if it prevents the Core Data crashes that I believe were occurring on another thread.

Thanks to everyone who answered!

UPDATE

The actual problem was elsewhere. See my other question here to see the answer and the solution: make NSUndoManager ignore a property of an NSManagedObject

Community
  • 1
  • 1