0

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?

Majster
  • 3,611
  • 5
  • 38
  • 60

2 Answers2

2

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.

Ian MacDonald
  • 13,472
  • 2
  • 30
  • 51
  • I do not want the invocation to maintain a strong reference since those references are references to view controllers most of the time. I want to check if the object the invocation is targeting still exists and if not I just want to "forget the whole thing". – Majster Feb 19 '15 at 17:27
  • 1
    And you're sure it's specifically `target` that's giving you the `EXC_BAD_ACCESS`? Accessing any other property on `invocation` at that same line is fine? – Ian MacDonald Feb 19 '15 at 18:06
  • This was an excellent point but I'm afraid that other things seem to work fine. I tried logging the selector using `NSLog(@"%@", NSStringFromSelector(invocation.selector));` and it works perfectly fine. I used this log statement just before the `if (!invocation.target)`. – Majster Feb 19 '15 at 18:11
  • So an `NSLog(@"%@", invocation.target);` also causes the `EXC_BAD_ACCESS`? – Ian MacDonald Feb 19 '15 at 18:25
  • It's not `weak` (as in ARC zeroing weak references). It's just plain not memory managed, which is called `unsafe_unretained` in ARC. – newacct Feb 20 '15 at 01:50
  • @Majster: There is no way to check whether an object still exists; it is undefined behavior to do anything to a deallocated object in any way. You must hold a strong reference to something, and if that thing is no longer used (e.g. no longer shown on screen), then calling something on it doesn't matter. – newacct Feb 20 '15 at 01:53
  • Thank you, cal you please provide a link with more on this topic? I will have to hold a strong reference to it as it seems yes. I do have a question if I could make a workaround, something like KVO when the object gets released and then remove my invocation from the dictionary, how's that? Any other ideas? – Majster Feb 20 '15 at 07:01
  • Any ideas? I can't seem to figure out a way to fix this. – Majster Feb 22 '15 at 16:29
0

Since NSInvocation wants to retain the target, but you essentially want it to keep a weak reference, use something like TPDWeakProxy. The proxy takes a reference and holds it with a weak pointer, but the proxy can be held strongly.

Here's how I did it in OCMockito in an NSInvocation category method:

- (void)mkt_retainArgumentsWithWeakTarget
{
    if (self.argumentsRetained)
        return;
    TPDWeakProxy *proxy = [[TPDWeakProxy alloc] initWithObject:self.target];
    self.target = proxy;
    [self retainArguments];
}

This replaces the target with what's essentially a weak target.

Jon Reid
  • 20,545
  • 2
  • 64
  • 95