iosobjective-cexc-bad-accessnsinvocation

NSInvocation get target causing EXC_BAD_ACCESS


I have a weird problem with NSInvocation. I'm using it as a return callback when a network operation completes. Let me explain the previous sentence in more detail:

I'm using a custom made network protocol which works over a TCP socket and I have a class which uses this protocol and serves as a connection to my server. Now the class has a method lets say performNetworkRequestWithDelegate: which is implemented like so:

- (void)performNetworkRequestWithDelegate:(id<MyClassDelegate>)delegate
{
    NSString *requestKey = [self randomUniqueString];
    id request = [self assembleRequestAndSoOnAndSoForth];
    [request setKey:requestKey];

    SEL method = @selector(callbackStatusCode:response:error:);
    NSMethodSignature *signature = [delegate methodSignatureForSelector:method];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    invocation.target = delegate;
    invocation.selector = method;

    delegateInvocationMap[requestKey] = invocation; //See below for an explanation what the delegateInvocationMap is

    [self sendRequest:request];
}

Okay so I know there are some things that need to be explained. First don't bother about anything related to the request except the requestKey. It works like so: the request key gets looped back to me when I get a response from the server. So its like setting a HTTP header field which gets looped back to you when you get a response from the server. That way I can identify which request was made. The delegateInvocationMap is a NSMutableDictionary which holds on to our invocations and we can get the correct one when we get the response and parse out the requestKey.

Now the handler for the response is like this:

- (void)processResponse:(id)response
{
    //Check for errors and whatnot

    NSString *requestKey = [response requestKey];
    if (!requestKey) return; //This never happens and is handled more correctly but keep it like this for the sake of simplicity

    NSInvocation *invocation = delegateInvocationMap[requestKey];
    if (!invocation) return; 

    [delegateInvocationMap removeObjectForKey:requestKey];

    if (!invocation.target) return; //THIS LINE IS THE PROBLEM

    [self setInvocationReturnParams:invocation fromResponse:response];
    [invocation invoke]; //This works when everything is fine
}

This function also works when there is a successful response return or when there are any errors I handle them correctly. Except one:

When the target of the invocation is dealloced I get an EXC_BAD_ACCESS when trying to check if there is a target for my invocation. The apple docs say:

The receiver’s target, or nil if the receiver has no target.

How can I check if the receiver was already deallocated? This is a huge pain.

EDIT: In the comments below I found that accessing a deallocated object is always unknown behaviour. I don't know if there is any official documentation specifying this (I didn't check yet) but I have a workaround idea. Would it be possible to observe the target of the invocation for a dealloc call via KVO?


Solution

  • NSInvocation's target property is not an ARC weak reference; it is defined as assign. If you do not hold any references to this object, it will be deallocated and you will start seeing EXC_BAD_ACCESS exceptions.

    @property(assign) id target
    

    ARC automatically converts assign properties to unsafe_unretained instead of weak. A weak property will be set to nil when the object is deallocated; an unsafe_unretained property will continue pointing at the memory address, which will be garbage.

    You can get around this by using the retainArguments method.

    [invocation retainArguments];
    

    From the documentation:

    If the receiver hasn’t already done so, retains the target and all object arguments of the receiver and copies all of its C-string arguments and blocks.