简体   繁体   中英

Why block type is different in array?

I put some block in an array, then I print the type of block, it different, besides, the code runs well in main function, but crash in custom class.

I haven't found any information to explain the problem.

It runs well in main function:

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        NSInteger a = 1;
        int b = 1;
        NSArray *arr = [NSArray arrayWithObjects:^{NSLog(@"%ld",a);}, ^{NSLog(@"first~~~%d",b);}, nil];
        id c = arr[0];
        id d = arr[1];
        NSLog(@"%@, %@",[c class],[d class]);
    }
    return 0;
}

It print ' NSMallocBlock , NSStackBlock '.

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        NSInteger a = 1;
        int b = 1;
        NSArray *arr = [NSArray arrayWithObjects:^{;}, ^{NSLog(@"first~~~%d",b);}, nil];
        id c = arr[0];
        id d = arr[1];
        NSLog(@"%@, %@",[c class],[d class]);
    }
    return 0;
}

It print ' NSGlobalBlock , NSStackBlock ', and it crash in custom class:

- (instancetype)init {
    if (self = [super init]) {
        int a = 1;
        int b = 1;
        NSArray *arr = [NSArray arrayWithObjects:^{NSLog(@"%ld",a);}, ^{NSLog(@"first~~~%d",b);}, nil];
        id c = arr[0];
        id d = arr[1];
        NSLog(@"%@, %@",[c class],[d class]);
    }
    return self;
}

I want to know why did it print three type of block, ' NSGlobalBlock , NSMallocBlock , NSStackBlock ' and why did it crash.

A block that doesn't capture any variables (like your ^{;} ) will be implemented as an NSGlobalBlock . Basically, for a block that doesn't capture any variables, all instances of that block are the same, so just one instance is allocated statically (like a global variable) for the lifetime of the program, and re-used for all instances of that block.

A block that captures variables (ie a closure) will be either an NSStackBlock or an NSMallocBlock . Since blocks may need to outlive the function scope they are created in, they may need to be put on the heap as an NSMallocBlock . However, since sometimes blocks don't need to outline the function scope, as an optimization, to potentially save an allocation, blocks start out on the stack, like local variables, in which case they are NSStackBlock . When a stack block is copied, it is moved from the stack to the heap (becoming an NSMallocBlock ) and retained as a regular memory-managed object.

Note that -retain does not move a block from the stack to the heap, since -retain should always return the same pointer it is called on; only -copy can move a block from stack to the heap, since it can return a different pointer. When code needs to store an object in a place that will outlive the function, it usually just needs to retain it; but for blocks, that is not enough -- if code needs to a store a block in a place that will outlive the scope, it must copy it. ARC will automatically copy a value of block type that is stored to a strong reference variable.

If you pass a block to a function that might store the block, who is responsible for copying it? If the function takes a parameter of block type, then the function itself should copy it if it needs to store it, so the calling function does not need to copy the block before passing it. However, if the called function takes a parameter of regular object type (like id ), it will just retain (not copy) the object if it needs to store it. So the calling function needs to copy it before passing it. The compiler will defensively copy a block that is passed to a parameter of non-block type just in case.

In this case, your blocks are passed to the method +[NSArray arrayWithObjects:] , which takes id parameters, so the compiler should defensively copy the blocks before passing them, so you should end up with NSMallocBlock s. But you see some NSStackBlock s. So what's going on? Actually, only the first parameter of +[NSArray arrayWithObjects:] has type id . +[NSArray arrayWithObjects:] is a variadic function, with only the first parameter explicitly specified, and ... for the rest. Varargs in C do not specify a type for the "variable" arguments in the ... . (Consider +[NSString stringWithFormat:] which takes an NSString * , followed by an undetermined number of arguments which can be different types.) So for the second and following arguments of +[NSArray arrayWithObjects:] , the compiler doesn't know what type the method will treat it as, and it doesn't do the defensive copying. (Arguably, if they want to be safe, they should always copy a block passed too ... , but they don't do that now.)

Having stack blocks in an NSArray is dangerous, because a stack block object is only valid within the scope where the block was created, and the NSArray may outlive that, causing undefined behavior. You should explicitly copy the blocks you pass to +[NSArray arrayWithObjects:] to avoid this problem. (It doesn't hurt to copy a block multiple times. Copying a block that's already on the heap doesn't move it.)

Your crash can be attributed to undefined behavior from having a stack block in your NSArray . If you want a more specific explanation of why it happens not to crash in your first example and why it happens to crash in your third example, the Foundation implementation of [NSArray arrayWithObjects:...] happens to autorelease the array before returning it. That means the array will be retained until the end of the current autorelease pool, at which point it will be released. In your first example, you have an @autoreleasepool block in your main function, which causes the release of the array object, and consequent dealloc of the array object, inside main . When the array is deallocated, it releases all its elements, including the stack block. Since this is still in main , in the same function where the block was created, the block object may still be valid, and a release on it does not crash. On the other hand, in your third example, there is no @autoreleasepool block in your init method, so by the time it gets to the end of the autorelease pool, which is after init returns, the stack block is no longer valid, and calling release on what is basically a dangling pointer crashes. If you put an @autoreleasepool block in your init method, you will see that it happens not to crash also, but that doesn't fix the underlying problem.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM