Objective-C: как работают блоки

В этой статье я расскажу о расположении блоков (__NSStackBlock__/__NSGlobalBlock__/__NSMallocBlock__), о том, как происходит захват переменных и как это связано с тем, во что компилируется блок.

В данный момент, применение блоков в Objective-C начинается практически с первых дней изучения этого языка. Но в большинстве случаев разработчики не задумываются, как блоки работают внутри. Здесь не будет магии, я просто расскажу об этом подробнее.

Начнем с самого начала, как выглядит блок в Objective-C

3a4d0cc31edc48d5aca9095dd3be8b4b.jpg

Для чего применяются блоки, я расписывать не буду, речь идет не об этом, поэтому давайте сразу рассмотрим интересные моменты на практике.
Что такое блок? В первую очередь, блок — это объект.

id thisIsBlock = ^{
};


Для понимания рассмотрим, что такое объект

struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;


А у блока есть метод

- (Class)class

который возвращает isa

Воспользуемся тем, что знаем теперь, и посмотрим, какие классы имеет блок в какой ситуации

__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__

Но рассмотрим еще один вариант __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__


Как видно, если блок не захватывает внешние переменные, то мы получаем __NSGlobalBlock__

Эксперементы
    Class class = [[^{
    } copy] class];
    NSLog(@"%@", NSStringFromClass(class));

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


__NSGlobalBlock__

Если же блок захватывает внешние переменные, то блок __NSStackBlock__ (на стеке). Однако если же послать блоку 'copy', то блок будет скопирован в кучу (__NSMallocBlock__).

ARC нам в этом помогает, и при присваивание блока в переменную (__strong) произойдет копирование блока в кучу, что можно заметить на примере выше. В общем, еще раз скажем ARC спасибо, ведь в MRC мы могли получить крайне неприятные баги

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);


Для чего тогда проперти обозначаются как '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.

ссылка
Поэтому продолжайте и дальше писать property copy для блоков.

Также рассмотрим еще один небольшой пример о __NSStackBlock__

Что будет, если передать __NSStackBlock__ в функцию?
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__
Зачем нужен __NSGlobalBlock__

О __NSStackBlock__ и __NSMallocBlock__ было достаточно много информации, какой толк от __NSGlobalBlock__? Внутри такого блока нет захвата внешний переменных, а значит не может произойти нарушений из-за видимости памяти, как при использовании __NSStackBlock__. И не нужно сперва создавать блок на стеке, потом при необходимости переносить в кучу, а значит, можно разрешить эту ситуацию намного проще.

В статье столько раз было сказано о захвате внешних переменных, однако мы все еще не сказали, как это происходит. В этом нам поможет документация clang. Не будем ее расписывать полностью, выделим лишь нужные идеи.

Блок превращается в структуру, а захваты превращаются в поля.

Это несложно проверить практически

сноска для ценителей

clang -rewrite-objc -ObjC main.m -o out.cpp


и можно посмотреть весь процесс в C++ коде
    MyObject *myObject = [[MyObject alloc] init];
    NSLog(@"object %p, ptr myObject %p", myObject, &myObject);
    ^{
        NSLog(@"object %p, ptr myObject %p", myObject, &myObject);
    }();


2015–11–29 23:12:37.297 block_testing[99850:13203592] object 0×100111e10, ptr myObject 0×7fff5fbff798
2015–11–29 23:12:37.298 block_testing[99850:13203592] object 0×100111e10, ptr myObject 0×7fff5fbff790

Как видно, указатель myObject снаружи и внутри блока указывает на одну и ту же область памяти, однако сам указатель отличается.

Блок создает внутри себя константные локальные переменные и указатели того, что захватывает. Что при использовании объектов увеличит счетчик ссылок, по обычной для ARC логике (при копировании блока в кучу).

Однако при использовании __block произойдет inout, поэтому счетчик ссылок не увеличится (однако не нужно использовать всегда и везде, const — это хорошее слово).

Для закрепления рассмотрим небольшой пример

код
#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:24.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


Блок IncrementBlock был превращен компилятором в структуру, при возврате блока из функции произошло копирование текущего области, и тем самым мы получили структуру, у которой есть поле, в которой хранится аккумулятор. А на каждый вызов функции createIncrementBlock мы получаем новый экземпляр.

Остановлю так же внимание на случай использования self внутри блока (что такое self). После прочтения статьи должно стать понятно, что использование self внутри __NSMallocBlock__ приведет к увеличению счетчика ссылок, однако вовсе не означает retain cycle. retain cycle это когда объект держит блок, а блок держит объект, который держит блок…

Параноя везде использовать __weak __strong не нужна, для нас блок — это объект. Просто объект, который можно сохранить, передать, использовать, с которым действуют обычные правила управления памятью.

© Habrahabr.ru