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?
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.