3

Got a really hard crash to solve. My iOS app (iOS version 6+, Xcode 5.1.1) crashed when the user logs out from his account, but only when it is forgrounded and backgrounded just before.

This is the crashlog from Testflight:

SIGSEGV
APP_NAME copy__destroy_helper_block_ 
in CAPServiceManager.m on Line 0

#   Binary Image Name   Address Symbol
0   APP_NAME copy   0x0010b61c  testflight_backtrace
1   APP_NAME copy   0x0010ae5c  TFSignalHandler
2   libsystem_platform.dylib    0x33d0087a  _sigtramp
3   APP_NAME copy   0x000f180c  __destroy_helper_block_ in CAPServiceManager.m on Line 0
4   libsystem_blocks.dylib  0x33bdbae0  _Block_release
5   Foundation  0x27268eb8  
6   libobjc.A.dylib 0x33650d5e  
7   Foundation  0x272ff372  
8   libdispatch.dylib   0x33ba295e  
9   libdispatch.dylib   0x33ba5ba6  _dispatch_main_queue_callback_4CF
10  CoreFoundation  0x2655fbd8  
11  CoreFoundation  0x2655e2d8  
12  CoreFoundation  0x264ac610  CFRunLoopRunSpecific
13  CoreFoundation  0x264ac422  CFRunLoopRunInMode
14  GraphicsServices    0x2da060a8  GSEventRunModal
15  UIKit   0x29bf6484  UIApplicationMain
16  APP_NAME copy   0x0009587a  main in main.m on Line 16
17  libdyld.dylib   0x33bc0aae  

In Xcode it is however crashing as a EXC_BAD_ACCESS in the AppDelegate file. (not giving more details). Enabling NSZombie does not work either as it prevents the app from crashing while this is on.

The code in CAPServiceManager related to blocks are these ones:

- (void)executeService:(WCServiceType)service
                   pin:(NSString *)pin
               payLoad:(id)payload
            usingBlock:(void (^) (NSError *error))block
{
    [self addStartingServiceStatusForService:service];

    [[WCWebService sharedWebService]
     postAuthTokenForService:service
     pin:pin
     vehicle:_vehicle
     target:self
     usingBlock:^(NSError *error, id response) {
         if (!error) {
             [self executeService:service token:response payLoad:payload usingBlock:block];
         }
         else {
             if (error.code == kWCHTTPStatusCodeUnauthorized) {
                 _wrongPinCounter++;
             }

             [self failStartingServiceStatusForServiceType:service error:error serviceAlreadyStarted:NO];
             block(error);
         }
     }];
}

- (void)executeService:(WCServiceType)service
                 token:(NSString *)token
               payLoad:(id)payload
            usingBlock:(void (^) (NSError *error))block
{
    [self addStartingServiceStatusForService:service];

    [[WCWebService sharedWebService]
     postStartServiceWithTarget:self
     service:service
     payLoad:payload
     token:token
     vehicle:_vehicle
     usingBlock:^(id target, NSError *error, WCServiceStatus *serviceStatus) {

         if (!error) {
             WCServiceStatus *startingServiceStatus = [self serviceStatusForService:service];

             NSManagedObjectContext *moc = _vehicle.managedObjectContext;
             [moc performBlockAndWait:^{
                 startingServiceStatus.sentPayload = payload;
                 [startingServiceStatus updateWithServiceStatus:serviceStatus];
             }];

             block(nil);
         }
         else {
             if (error.localizedDescription && [error.localizedDescription isEqualToString:@"Service is already started"]) {
                 [self serviceIsAlreadyStartedForServiceType:service block:^(NSError *error) {
                     if (error) {
                         [self failStartingServiceStatusForServiceType:service error:error serviceAlreadyStarted:YES];
                     }
                     block(error);
                 }];
             }
             else {
                 [self failStartingServiceStatusForServiceType:service error:error serviceAlreadyStarted:NO];
                 block(error);
             }
         }
     }];
}

I first thought that the error was related to self. But testing to store self as this does not work either:

__weak CAPServiceManager *weakSelf = self;

did not work either. I also tried __block as a modifier.

The blocks are passed on as you can see. The are then stored in an instance variable in WCWebServiceRequest.m like this:

_block = [block copy];

...where _block is defined as

@interface WCWebServiceRequest : NSObject <NSURLConnectionDelegate, NSURLConnectionDataDelegate> {
@protected
    id _block;
    //...

...and later called upon with this code:

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    NSOperationQueue *queue = [NSOperationQueue new];
    queue.name = [NSString stringWithFormat:@"%s Queue", __PRETTY_FUNCTION__];

    [queue addOperationWithBlock:^{
        NSError *error = nil;
        NSDictionary *attributes;
        __block WCServiceStatus *serviceStatus;

        if (_data) {
            attributes = [NSJSONSerialization JSONObjectWithData:_data options:0 error:&error];
        }

        if (self.response.statusCode == kWCHTTPStatusCodeAccepted || self.response.statusCode == kWCHTTPStatusCodeOK) {
            if ([attributes isKindOfClass:[NSDictionary class]]) {
                NSManagedObjectContext *moc = [WCStorage sharedStorage].moc;
                [moc performBlockAndWait:^{
                    serviceStatus = [WCServiceStatus makeServiceStatusWithAttributes:attributes moc:moc];
                }];
            }
            else {
                error = [NSError errorWithWebServiceErrorCode:kWCHTTPStatusCodeInvalidData];
            }
        }
        else {
            error = [NSError errorWithWebServiceErrorCode:self.response.statusCode errorInfo:[NSError errorInfoFromData:_data]];
        }

        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            if (!_cancelled) {
                WCWebServiceServiceStatusBlock_Invoke(_block, _target, error, serviceStatus);
            }

            [super connectionDidFinishLoading:connection];
        }];
    }];
}

... where WCWebServiceServiceStatusBlock_Invoke is defined as

#define WCWebServiceServiceStatusBlock_Invoke(block, target, error, serviceStatus) \
{ \
    WCWebServiceServiceStatusBlock block_ = (WCWebServiceServiceStatusBlock) block; \
    block_(target, error, serviceStatus); \
}

typedef void (^WCWebServiceServiceStatusBlock)              (id target, NSError *error, WCServiceStatus *serviceStatus);

...and _block deallocated like this:

- (void)dealloc
{
    if (_block) {
        _block = nil;
    }
}

Any idea what can be wrong or how to debug this further?

Edit: I am using ARC, and I can't answer why it is implemented this way, I took over an existing project.

Sunkas
  • 9,542
  • 6
  • 62
  • 102
  • 1.Are block from `(void (^) (NSError *error))block` is invoked in `WCWebServiceServiceStatusBlock_Invoke`? – Cy-4AH Sep 02 '14 at 11:27
  • 2.Why not define ivar _block as `WCWebServiceServiceStatusBlock`? – Cy-4AH Sep 02 '14 at 11:28
  • 3.If you use ARC, then you don't need `copy` and don't need dealloc. If not, then dealloc will be: `- (void) dealloc { [_block release]; [super dealloc];}` – Cy-4AH Sep 02 '14 at 11:30
  • Thank you for your reply. 1. Yes, it is called, see second last code section in my question. 2. I'll try that. 3. Will try removing dealloc and copy. However similar problems seems to be related to _not_ making a copy of your block. I.e. http://stackoverflow.com/questions/23573114/objective-c-crash-on-destroy-helper-block – Sunkas Sep 02 '14 at 11:48
  • Neither removing [block copy] or removing dealloc hade any effect on my problem. I cannot change type of _block as it is not always storing a WCWebServiceServiceStatusBlock (other classes inherit that class). – Sunkas Sep 02 '14 at 11:54
  • It's not always storing as `WCWebServiceServiceStatusBlock` but it's always invoked as `WCWebServiceServiceStatusBlock`, isn't it? – Cy-4AH Sep 02 '14 at 12:12
  • You can't invoke `(void (^) (NSError *error))` as `WCWebServiceServiceStatusBlock`. They have different signature. That's yours problem. – Cy-4AH Sep 02 '14 at 12:18
  • `_block` is not always `WCWebServiceServiceStatusBlock`, there are other types. Note that `_block` is not the same as `block`. `_block` is an instance variable stored in `WCWebServiceRequest`. `block` is a callback block from `CAPServiceManager` to either return an error or nil (when successful) – Sunkas Sep 02 '14 at 13:32

1 Answers1

2

This problem was related to many issues. Here are the steps I took to finally get rid of the problem:

  • Make sure all NSNotificationsCenter registrations where removed in dealloc
  • Made sure KVO listeners where "ignored" (via a a BOOL+check) before they where removed (to not trigger code that interfered with the logout)
  • Moved the unregistration code (including clearing CoreData and unregister notifications and network request) to separate queues:

Related new code:

- (void)userDidSignOut
{
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        [[NSNotificationCenter defaultCenter] removeObserver:self];
        [[WCWebService sharedWebService] signOut];

        [_weatherRefresher stop];
        [_TARefreshTimer invalidate];
        [CAPSVTMessageView hide];

        [[WCStorage sharedStorage].validV enumerateObjectsUsingBlock:^(WCV *obj, NSUInteger idx, BOOL *stop) {
            [self unregisterFromPushNotificationsForVehicle:obj];
            [obj.VHSRefresher kill];
            [obj.serviceManager kill];
        }];

        NSOperationQueue *queue = [NSOperationQueue new];
        queue.name = [NSString stringWithFormat:@"%s Queue", __PRETTY_FUNCTION__];

        [queue addOperationWithBlock:^{
            [[WCStorage sharedStorage] clearDatabase];
            [WCStorage sharedStorage].sessionPassword = nil;
            _signInFromBackround = NO;
        }];
    }];
}
Sunkas
  • 9,542
  • 6
  • 62
  • 102