Does -[NSInvocation retainArguments] copy blocks?
Asked Answered
C

2

7

NSInvocation's -retainArguments method is useful for when you don't run the NSInvocation immediately, but do it later; it retains the object arguments so they remain valid during this time.

As we all know, block arguments should be copied instead of retained. My question is, does -retainArguments know to copy instead of retain an argument when it's of block type? The documentation does not indicate that it does, but it seems like an easy and sensible thing to do.

Update: The behavior seems to have changed in iOS 7. I just tested this, and in iOS 6.1 and before, -retainArguments didn't copy parameters of block type. In iOS 7 and later, -retainArguments does copy parameters of block type. The documentation of -retainArguments has been updated to say that it copies blocks, but it does not say when the behavior changed (which is really dangerous for people who support older OS's).

Cagliari answered 18/4, 2013 at 0:52 Comment(1)
Thanks for keeping this updated!Parahydrogen
C
4

It's certainly supposed to (albeit I haven't test it myself). According to the documentation:

retainArguments

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.

  • (void)retainArguments

Discussion

Before this method is invoked, argumentsRetained returns NO; after, it returns YES.

For efficiency, newly created NSInvocation objects don’t retain or copy their arguments, nor do they retain their targets, copy C strings, or copy any associated blocks. You should instruct an NSInvocation object to retain its arguments if you intend to cache it, because the arguments may otherwise be released before the invocation is invoked. NSTimer objects always instruct their invocations to retain their arguments, for example, because there’s usually a delay before a timer fires.

Cayuse answered 22/5, 2014 at 17:44 Comment(3)
Interesting. The documentation has changed: web.archive.org/web/20120826185131/http://developer.apple.com/… But it does not document if and when the behavior changed. Blocks were definitely not copied by retainArguments when I tried it in iOS 6 a year ago.Cagliari
I just tested it, and it is copied in iOS 7 but not in iOS 6.1, so it changed.Cagliari
They should've documented the change as an ios 7 update as opposed to making it seem like it's always been this way... silly Apple, tricks are for kids!Cayuse
Z
1

No.

Image if the answer is yes, where NSInvocation is smart enough to copy block, it should do something like this:

for (/*every arguments*/) {
    if (/*arg is object. i.e. @encode(arg) is '@'*/) {
        if ([arg isKindOfClss:[NSBlock class]]) {
            arg = [arg copy]; // copy block
        } else {
            [arg retain];
        }
    }
}

the problem is that arg is modified while copying the block, which should not happen because this means call retainArguments may change the arguments in the NSInvocation. this will break many assumptions that already made. (i.e. arguments get from NSInvocation should be same as arguments used to create the NSInvocation)


Update

just did the test to conform the answer is NO, but my previous point was incorrect though...

@interface Test : NSObject

@end

@implementation Test

- (void)testMethodWithBlock:(void (^)(void))block obj:(id)obj cstr:(const char *)cstr {
    NSLog(@"%p %p %p %@", block, obj, cstr, [block class]);
}

@end

@implementation testTests

- (void)test1 {
    __block int dummy;
    Test *t = [[Test alloc] init];
    NSMethodSignature *ms = [t methodSignatureForSelector:@selector(testMethodWithBlock:obj:cstr:)];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:ms];
    void (^block)(void) = ^ {
        dummy++;    // stop this become global block
    };
    id obj = @"object";
    char *cstr = malloc(5);
    strcpy(cstr, "cstr");


    NSLog(@"%@", [ms debugDescription]);

    NSLog(@"%p %p %p %@", block, obj, cstr, [block class]);

    [invocation setSelector:@selector(testMethodWithBlock:obj:cstr:)];
    [invocation setArgument:&block atIndex:2];
    [invocation setArgument:&obj atIndex:3];
    [invocation setArgument:&cstr atIndex:4];

    [invocation invokeWithTarget:t];

    [invocation retainArguments];

    [invocation invokeWithTarget:t];

    free(cstr);
}

@end

output, ARC disabled (and crashed):

2013-04-18 19:49:27.616 test[94555:c07] 0xbfffe120 0x70d2254 0x7167980 __NSStackBlock__
2013-04-18 19:49:27.617 test[94555:c07] 0xbfffe120 0x70d2254 0x7167980 __NSStackBlock__
2013-04-18 19:49:27.618 test[94555:c07] 0xbfffe120 0x70d2254 0x736a810 __NSStackBlock__

ARC enabled:

2013-04-18 19:51:03.979 test[95323:c07] 0x7101e10 0x70d2268 0x7101aa0 __NSMallocBlock__
2013-04-18 19:51:03.979 test[95323:c07] 0x7101e10 0x70d2268 0x7101aa0 __NSMallocBlock__
2013-04-18 19:51:03.980 test[95323:c07] 0x7101e10 0x70d2268 0xe0c1310 __NSMallocBlock__

as you can see, c string are copied by retainArguments but not blocks. but with ARC enabled, the problem should go away because ARC copied it for you at some point.

Zephaniah answered 18/4, 2013 at 1:14 Comment(7)
Copying a block doesn't change what the type system cares about, the block's signature. Sure, it may go from an __NSStackBlock__ to an __NSHeapBlock__, but that's the point of copying a block: moving it to the heap to extend its lifetime.Malonylurea
Good point. However, -retainArguments also copies C strings. And copying C strings also changes the arguments.Cagliari
@Malonylurea my point is block != [block copy] so it may cause problem if is copiedZephaniah
@Cagliari i never know that... can you give me reference? and i will try it when i have timeZephaniah
@xlc: yes, the documentation for the retainArguments method. developer.apple.com/library/mac/documentation/Cocoa/Reference/…Cagliari
"output, ARC disabled (and crashed)" you must be running some other code than what you have shown, because what you have shown should not crash. The block is in scope until the end of test1, including where the NSInvocation is invoked.Cagliari
@Cagliari that's all code and i am running it as unit test. it crashed because __NSStackBlock__ is added to autorelease pool, so the reference of the block is get extended beyond the scopeZephaniah

© 2022 - 2024 — McMap. All rights reserved.