Objective-C: how blocks work

    In this article, I will talk about the location of blocks (__NSStackBlock __ / __ NSGlobalBlock __ / __ NSMallocBlock__), how variables are captured and how this relates to what the block is compiled into.

    At the moment, the use of blocks in Objective-C begins almost from the first days of learning this language. But in most cases, developers do not think about how the blocks work inside. There will be no magic, I’ll just tell you more about it.

    Let's start from the very beginning what a block looks like in Objective-C


    What are the blocks used for, I will not paint, this is not about that, so let's immediately look at interesting points in practice.

    What is a block? First of all, a block is an object.
    id thisIsBlock = ^{
    };
    

    For understanding, consider what an object is.
    struct objc_object {
        Class isa  OBJC_ISA_AVAILABILITY;
    };
    /// A pointer to an instance of a class.
    typedef struct objc_object *id;
    

    And the block has a method
    - (Class)class
    
    which isa returns We will

    use what we now know and see what classes the block has in what situation
    __NSStackBlock__
        int foo = 3;
        Class class = [^{
            int foo1 = foo + 1;
        } class];
        NSLog(@"%@", NSStringFromClass(class));
    

    2015-11-29 22: 30: 21.054 block_testing [99727: 13189641] __NSStackBlock__

    __NSMallocBlock__
        int foo = 3;
        Class class = [[^{
            int foo1 = foo + 1;
        } copy] class];
        NSLog(@"%@", NSStringFromClass(class));
    

    2015-11-29 22: 33: 45.026 block_testing [99735: 13190778] __NSMallocBlock__

    __NSGlobalBlock__
        Class class = [^{
        } class];
        NSLog(@"%@", NSStringFromClass(class));
    

    2015-11-29 22: 34: 49.645 block_testing [99743: 13191389] __NSGlobalBlock__


    But consider another option __NSMallocBlock__
    ARC
        int foo = 3;
        id thisIsBlock = ^{
            int foo1 = foo + 1;
        };
        Class class = [thisIsBlock class];
        NSLog(@"%@", NSStringFromClass(class));
    

    2015-11-29 22: 37: 27.638 block_testing [99751: 13192462] __NSMallocBlock__

    As you can see, if the block does not capture external variables, then we get __NSGlobalBlock__
    Experiments
        Class class = [[^{
        } copy] class];
        NSLog(@"%@", NSStringFromClass(class));
    

        id thisIsBlock = ^{
        };
        Class class = [thisIsBlock class];
        NSLog(@"%@", NSStringFromClass(class));
    

    __NSGlobalBlock__


    If the block captures external variables, then the __NSStackBlock__ block (on the stack). However, if you send the block 'copy', the block will be copied to the heap (__NSMallocBlock__).

    ARC helps us with this, and when assigning a block to a variable (__strong), the block will be copied to the heap, which can be seen in the example above. In general, let’s say thanks again to ARC, because in MRC we could get extremely unpleasant bugs
    object.h
    /*!
     * @typedef dispatch_block_t
     *
     * @abstract
     * The type of blocks submitted to dispatch queues, which take no arguments
     * and have no return value.
     *
     * @discussion
     * When not building with Objective-C ARC, a block object allocated on or
     * copied to the heap must be released with a -[release] message or the
     * Block_release() function.
     *
     * The declaration of a block literal allocates storage on the stack.
     * Therefore, this is an invalid construct:
     * 
     * dispatch_block_t block;
     * if (x) {
     *     block = ^{ printf("true\n"); };
     * } else {
     *     block = ^{ printf("false\n"); };
     * }
     * block(); // unsafe!!!
     * 
     *
     * What is happening behind the scenes:
     * 
     * if (x) {
     *     struct Block __tmp_1 = ...; // setup details
     *     block = &__tmp_1;
     * } else {
     *     struct Block __tmp_2 = ...; // setup details
     *     block = &__tmp_2;
     * }
     * 
     *
     * As the example demonstrates, the address of a stack variable is escaping the
     * scope in which it is allocated. That is a classic C bug.
     *
     * Instead, the block literal must be copied to the heap with the Block_copy()
     * function or by sending it a -[copy] message.
     */
    typedef void (^dispatch_block_t)(void);
    



    Why then are proprieties designated as 'copy'?
    Note: You should specify copy as the property attribute, because a block needs to be copied to keep track of its captured state outside of the original scope. This isn't something you need to worry about when using Automatic Reference Counting, as it will happen automatically, but it's best practice for the property attribute to show the resultant behavior. For more information, see Blocks Programming Topics.
    link
    Therefore, continue to write property copy for blocks.

    Also consider another small example about __NSStackBlock__
    What happens if you pass __NSStackBlock__ to a function?
    typedef void(^EmptyBlock)();
    void fooFunc(EmptyBlock block) {
        Class class = [block class];
        NSLog(@"%@", NSStringFromClass(class));
    }
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            int foo = 1;
            fooFunc(^{
                int foo1 = foo + 1;
            });
        }
        return 0;
    }
    

    2015-11-29 22: 52: 16.905 block_testing [99800: 13197825] __NSStackBlock__


    Why __NSGlobalBlock__ is needed
    About __NSStackBlock__ and __NSMallocBlock__ there was a lot of information, what is the use of __NSGlobalBlock__? Inside such a block there is no capture of external variables, which means that violations cannot occur due to memory visibility, as when using __NSStackBlock__. And you do not need to create a block on the stack first, then transfer it to the heap if necessary, which means that you can solve this situation much easier.


    The article has said so many times about the capture of external variables, but we still have not said how this happens. The clang documentation will help us with this . We will not paint it completely, we select only the necessary ideas.

    The block turns into a structure, and the grips turn into fields.

    This is easy to verify in practice.
    footnote for connoisseurs
    clang -rewrite-objc -ObjC main.m -o out.cpp

    and you can see the whole process in C ++ code

        MyObject *myObject = [[MyObject alloc] init];
        NSLog(@"object %p, ptr myObject %p", myObject, &myObject);
        ^{
            NSLog(@"object %p, ptr myObject %p", myObject, &myObject);
        }();
    

    29/11/2015 23: 12: 37.297 block_testing [99850: 13203592] 0x100111e10 object, ptr myObject 0x7fff5fbff798
    11/29/2015 23: 12: 37.298 block_testing [99850: 13203592] object 0x100111e10, ptr myObject 0x7fff5fbff790

    As can be seen, the pointer outside myObject and inside the block points to the same memory area, however, the pointer itself is different.

    The block creates constant local variables and pointers of what it captures within itself. That when using objects, it will increase the reference counter, according to the usual logic for ARC (when copying a block to a heap).

    However, when using __block, inout will occur, so the reference count will not increase (however, you should not use it always and everywhere, const is a good word).

    To consolidate, consider a small example
    the code
    #import 
    typedef NSInteger (^IncrementBlock)();
    IncrementBlock createIncrementBlock(const NSInteger start, const NSInteger incrementValue) {
        __block NSInteger acc = start;
        return ^NSInteger{
            acc += incrementValue;
            return acc;
        };
    }
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            IncrementBlock incrementBlock = createIncrementBlock(0, 2);
            NSLog(@"%ld", incrementBlock());
            NSLog(@"%ld", incrementBlock());
            NSLog(@"%ld", incrementBlock());
            IncrementBlock incrementBlock1 = createIncrementBlock(0, 2);
            NSLog(@"%ld", incrementBlock1());
            NSLog(@"%ld", incrementBlock1());
            NSLog(@"%ld", incrementBlock1());
        }
        return 0;
    }
    

    2015-11-29 23: 31: 24.027 block_testing [99910: 13209611] 2
    2015-11-29 23: 31: 24.028 block_testing [99910: 13209611] 4
    2015-11-29 23: 31: 24.028 block_testing [99910: 13209611] 6
    2015-11-29 23: 31.028 block_testing [99910: 13209611] 2
    2015-11-29 23: 31: 24.028 block_testing [99910: 13209611] 4
    2015-11-29 23: 31: 24.029 block_testing [99910: 13209611 ] 6

    The IncrementBlock block was turned into a structure by the compiler, when the block was returned from the function, the current area was copied, and thereby we got a structure that has a field in which the battery is stored. And for every call to the createIncrementBlock function, we get a new instance.

    I will also focus on the case of using self inside a block ( what is self ). After reading the article, it should become clear that using self inside __NSMallocBlock__ will increase the reference count, but it does not mean at all retain cycle. retain cycle is when an object holds a block, and a block holds an object that holds a block ...

    Paranoia is not needed to use __weak __strong everywhere, for us a block is an object. Just an object that can be saved, transferred, used, with which the usual rules for managing memory apply.

    Also popular now: