4

Trying to get to the bottom of an issue I've been seeing in production builds and FINALLY was able to reproduce it while testing. Using RestKit v0.23.1, when doing an RKManagedObjectRequestOperation using the following code (while plugged into instruments) I get "An Objective-C message was sent to a deallocated 'NSError' object (zombie)" and the app crashes every time there's objects in the response JSON - if the response is something like "objects = ();" there's no crash - so I'm guessing it's somewhere in the RestKit/Core Data mapping or storage?

    RKManagedObjectRequestOperation *objectRequestOperation = [_objectManager managedObjectRequestOperationWithRequest:request managedObjectContext:_objectManager.managedObjectStore.mainQueueManagedObjectContext success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
        DDLogInfo(@"INSIDE SUCCESS BLOCK");
    } failure:^(RKObjectRequestOperation *operation, NSError *error) {
        DDLogInfo(@"INSIDE ERROR BLOCK");
    }];

    [objectRequestOperation setWillMapDeserializedResponseBlock:^id(id deserializedResponseBody) {
        DDLogInfo(@"Response JSON: %@", deserializedResponseBody);

        return deserializedResponseBody;
    }];

    objectRequestOperation.savesToPersistentStore = YES;
    [objectRequestOperation start];

The raw JSON is properly logged inside the setWillMapDeserializedResponseBlock, but the logs inside the success and error block are never reached. Here is the stack trace I get back from crashlytics:

Thread : Crashed: NSOperationQueue Serial Queue
0  libobjc.A.dylib                0x37dd4626 objc_msgSend + 5
1  Foundation                     0x2df5802d -[NSError dealloc] + 60
2  libobjc.A.dylib                0x37dd9b6b objc_object::sidetable_release(bool) + 174
3  libobjc.A.dylib                0x37dda0d3 (anonymous namespace)::AutoreleasePoolPage::pop(void*) + 358
4  CoreFoundation                 0x2d569501 _CFAutoreleasePoolPop + 16
5  Foundation                     0x2df69999 -[__NSOperationInternal _start:] + 1064
6  Foundation                     0x2e00d745 __NSOQSchedule_f + 60
7  libdispatch.dylib              0x382b8cbd _dispatch_queue_drain + 488
8  libdispatch.dylib              0x382b5c6f _dispatch_queue_invoke + 42
9  libdispatch.dylib              0x382b95f1 _dispatch_root_queue_drain + 76
10 libdispatch.dylib              0x382b98dd _dispatch_worker_thread2 + 56
11 libsystem_pthread.dylib        0x383e4c17 _pthread_wqthread + 298
Mike
  • 9,765
  • 5
  • 34
  • 59
  • So you're running this on a background thread but using the main thread context? – Wain May 10 '14 at 13:35
  • This is only ever called from the main thread. – Mike May 11 '14 at 17:03
  • Is there something based on the stacktrace or something that leads you to think its on the background thread, or is that based on the conversation we had last week in a prior thread? – Mike May 12 '14 at 14:44
  • Sorry I don't remember the previous conversation, but the stack trace starts with lots of dispatch queue and worker thread stuff. If you start the operation explicitly on the main thread then it should run on the main thread, though it's likely to create other operations (for mapping) which run on a (private) operation queue. Sucks that you can't replicate running from Xcode. Can you show the mappings and sample JSON that results in a crash. – Wain May 12 '14 at 17:59
  • This is not running on the main thread. Your problem is that NSError is being autoreleased, and then sent a message. Without looking at RestKit's code, it likely has to do with the memory management of that "failure block". Designed-in retain cycles are teh suck. – quellish Jun 03 '14 at 19:53
  • The reskit code I provided is running on the main thread, I presume you're talking about what is shown in the stack trace? – Mike Jun 03 '14 at 20:22
  • For now I've moved to using RKObjectManager's postObject function instead... Seems to have the same request/mapping functionality... – Mike Jun 04 '14 at 19:04
  • One thing that you might consider doing is making available a small sample application that reproduces the problem. – ericg Jun 09 '14 at 19:11
  • I would if I could - this issue occurs very infrequently so its virtually impossible to replicate. Given a million+ sessions this has occurred ~1k times. I've only ever gotten it to happen once on an ad hoc build on a test device and after plugging it into xcode the problem went away. – Mike Jun 09 '14 at 19:41
  • @Mike Did you ever come round solving this issue? I am currently experiencing the same thing and I don't really know how to fix it, since I cannot really reproduce it. – Sebastian Borggrewe Jul 14 '14 at 14:31
  • Not yet - I've made a few changes that I doubt will fix the issue as they are longshots, but if I do I'll be sure to let you know. – Mike Jul 14 '14 at 15:19
  • @Mike Thank you. Sadly this problem does not only appear 1k in a million times for me. Will let you know if I am able to fix it. Did this problem occur after an app update? For us it did. – Sebastian Borggrewe Jul 14 '14 at 20:02
  • Unfortunately, it seems to not have been caused by an update. Do you do anything to the newly mapped objects in the success block - like save new values on them? – Mike Jul 14 '14 at 22:48
  • @Mike I am currently not even sure where this problem occurs. I am seeing the same stack trace as you do, but I am not sure why it happens and I can't reproduce it. – Sebastian Borggrewe Jul 15 '14 at 11:21
  • Are you doing a managed object request operation? If not, what restkit methods are you using? – Mike Jul 15 '14 at 14:41

1 Answers1

3

This isn't a problem with RestKit. I've seen this problem frequently and it actually looks like the over-release actually happens in Apple's code. The problem happens when you try to save to a Core Data store and it fails. Core Data reports an error as it should, but that error is mishandled.

I had a few scenarios causing the save failures and this is how I fixed them:

The data store is inaccessible because of the Data Protection API.

Either busy wait and let your app fail to launch like this:

while(![[UIApplication sharedApplication] isProtectedDataAvailable]) {
        [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.5f]];
   }

Or disable protection if the data in your store isn't sensitive like this:

[_coordinator addPersistentStoreWithType:NSSQLiteStoreType
                               configuration:nil
                                         URL:url
            options:@{NSPersistentStoreFileProtectionKey:NSFileProtectionNone}
                                       error:&error];

The important thing is that you don't try to save until you can access the file. If you can re-structure you code to prevent accessing the database when it is inaccessible that is good too. You can use the Data Protection API application delegate methods to trigger that mechanism.

The data store is corrupted - The best thing to do here is to delete the store and start over. Here is a good way to detect a corrupted store using the sqlite library directly.

    #import <sqlite3.h>

    sqlite3 *dbConnection;
    if (sqlite3_open([[url absoluteString] UTF8String], &dbConnection) != SQLITE_OK) {
        NSLog(@"[SQLITE] Unable to open database!");
    }
    sqlite3_stmt *statement = nil;
    sqlite3_prepare_v2(dbConnection, "PRAGMA quick_check;", -1, &statement, NULL);
    NSString *result = nil;
    while (sqlite3_step(statement) == SQLITE_ROW) {
        for (int i=0; i<sqlite3_column_count(statement); i++) {
            int colType = sqlite3_column_type(statement, i);
            if (colType == SQLITE_TEXT) {
                const unsigned char *col = sqlite3_column_text(statement, i);
                result = [NSString stringWithFormat:@"%s", col];
            } else {
                NSLog(@"[SQLITE] UNKNOWN DATATYPE");
            }
        }
    }
    sqlite3_close(dbConnection);

This runs a sqlite PRAGMA query to perform an integrity check. I use quick_check, but you could also use integrity_check if you are willing to wait the extra time. You can tell things are good using [result isEqualToString:@"ok"]

Mike Gottlieb
  • 966
  • 6
  • 11
  • EXTREMELY interesting and useful. Unfortunately, I won't be able to check directly to see if this works, as I cannot replicate it locally, but I think the first step will be try out the file protection setting and see if the crash reports go away... In terms of 'protection' is this in terms of like how secure is the information, i.e. personal/confidential information as opposed to some random server objects that represent data? – Mike Jul 15 '14 at 22:23
  • "The important thing is that you don't try to save until you can access the file." - This could potentially be the key, as in my success block I **USED TO** take the objects that are mapped and set a new value on them and then save them each one at a time, which was not really the greatest thing to do. What I ended up doing was adding this to the managed object's willSave method instead, so I'm not manually saving any objects. Perhaps this (once I release the new build with this change) will solve the issue. – Mike Jul 15 '14 at 22:31
  • If you want to learn more about the "protection" bit this does a pretty good job of summing it up. http://erik.io/blog/2014/01/03/watch-that-cache-dropbox-evernote-insufficiently-protecting-ios-6-data/ also you can watch the WWDC session from 2012. To answer your question it has to do with whether or not the data gets encrypted on disk. If it is encrypted and the user sets a passcode then it can't be decrypted until the user enters the passcode. – Mike Gottlieb Jul 15 '14 at 23:00
  • FYI in one instance I solved this by just removing protection as the data was not sensitive. In another I wrapped all my db saves in operations that I submitted to an NSOperationQueue. I then toggled suspension on the queue based on whether or not I could access the DB. – Mike Gottlieb Jul 15 '14 at 23:02
  • If I have nothing configured for protection in my options dictionary, does that mean it's currently set to none? I looked in the docs for the "default" but didn't see what it was. I currently only use NSMigratePersistentStoresAutomaticallyOption: @(YES), NSInferMappingModelAutomaticallyOption: @(YES), – Mike Jul 15 '14 at 23:04
  • The default changed in iOS 7 from none to either NSFileProtectionCompleteUnlessOpen or NSFileProtectionCompleteUntilFirstUserAuthentication. I can't remember which. – Mike Gottlieb Jul 15 '14 at 23:08
  • Fantastic - I'm actually feeling like I might now have a better grasp on this issue. I'll take protection off and now that I've removed my explicit (redundant and unnecessary) saving of objects, I'm hopeful this will solve the issue. If it does, I'll owe you a beer. – Mike Jul 15 '14 at 23:14
  • Ahh yes, found it "The default value is NSFileProtectionCompleteUntilFirstUserAuthentication for all applications built on or after iOS v5.0." – Mike Jul 15 '14 at 23:20
  • @MikeGottlieb I will test this straight away once I am in the office. If this solves the issue, you have just saved me hours of work and will definitely be rewarded the bounty!!! – Sebastian Borggrewe Jul 16 '14 at 08:04
  • @MikeGottlieb Have you also observed the error in case there is a Model mismatch problem? I am not 100% sure whether the migration was successful, although while testing it, it always went through. – Sebastian Borggrewe Jul 16 '14 at 14:14
  • Yes a model mismatch will cause a failure to create the store and the error will likely be over-released. RestKit depends on a framework called MagicalRecord (or used to at least) which handled the model mismatch issues in Debug builds, but not Release builds. Sometimes that makes it harder to catch it. If you aren't already you should use auto-migration for the simple DB changes. – Mike Gottlieb Jul 16 '14 at 17:16
  • @MikeGottlieb Thanks Mike. I already use auto-migrations. I wasn't able to try the solution you offered, but I will award you the bounty anyway, since it showed me the right direction. – Sebastian Borggrewe Jul 18 '14 at 13:12
  • Alright, apparently the issue still sticks. I have seriously no idea how i can solve the issue... – Sebastian Borggrewe Aug 01 '14 at 17:15
  • Damn, I just released the new client with these fixes in there and the issue still persists. – Mike Aug 24 '14 at 02:02