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

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

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


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

Что такое блок? В первую очередь, блок — это объект.
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:
 * <code>
 * dispatch_block_t block;
 * if (x) {
 *     block = ^{ printf("true\n"); };
 * } else {
 *     block = ^{ printf("false\n"); };
 * }
 * block(); // unsafe!!!
 * </code>
 *
 * What is happening behind the scenes:
 * <code>
 * if (x) {
 *     struct Block __tmp_1 = ...; // setup details
 *     block = &__tmp_1;
 * } else {
 *     struct Block __tmp_2 = ...; // setup details
 *     block = &__tmp_2;
 * }
 * </code>
 *
 * 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 0x100111e10, ptr myObject 0x7fff5fbff798
2015-11-29 23:12:37.298 block_testing[99850:13203592] object 0x100111e10, ptr myObject 0x7fff5fbff790

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

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

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

Для закрепления рассмотрим небольшой пример
код
#import <Foundation/Foundation.h>


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 не нужна, для нас блок — это объект. Просто объект, который можно сохранить, передать, использовать, с которым действуют обычные правила управления памятью.

Комментарии (7)


  1. InstaRobot
    30.11.2015 21:18
    +1

    Статья отличная. Спасибо


  1. KamiSempai
    30.11.2015 23:07

    Я не совсем понял в чем существенная разница между __NSStackBlock__ и __NSMallocBlock__. Вроде бы оба хранят ссылки на внешние объекты, тем самым увеличивая счетчик ссылок. В чем тогда смысл вызова copy?

    И еще по поводу copy. Вы писали:

    Однако если же послать блоку 'copy', то блок переместится в кучу (__NSMallocBlock__)
    Он точно переместится? Вроде бы copy должен копировать?


    1. ajjnix
      01.12.2015 04:44

      typedef void (^EmptyBlock)();
      void foo(EmptyBlock block) {
          NSLog(@"my class:%@, my ptr %p", [block class], &block);
          block = [block copy];
          NSLog(@"my class:%@, my ptr %p", [block class], &block);
      }
      
      int main(int argc, const char * argv[]) {
          @autoreleasepool {
              int a = 1;
              foo(^{
                  int b = a;
              });
          }
          return 0;
      }
      

      2015-12-01 07:36:25.293 block_testing[580:13262049] my class:__NSStackBlock__, my ptr 0x7fff5fbff758
      2015-12-01 07:36:25.294 block_testing[580:13262049] my class:__NSMallocBlock__, my ptr 0x7fff5fbff758

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

      Тоесть если внутри блока используются объекты, то retain будет отправлен именно при перемещении в кучу


    1. ajjnix
      01.12.2015 05:07

      извините, не о том подумал в ответе

      typedef int (^EmptyBlock)();
      void foo(EmptyBlock block) {
          EmptyBlock mallocBlock = block;
          EmptyBlock mallocBlock1 = block;
          
          NSLog(@"my class:%@, ptr block = %p", [mallocBlock class], mallocBlock);
          NSLog(@"my class:%@, ptr block = %p", [mallocBlock1 class], mallocBlock1);
      }
      
      int main(int argc, const char * argv[]) {
          @autoreleasepool {
              int a = 1;
              foo(^int{
                  return a+5;
              });
          }
          return 0;
      }
      

      2015-12-01 08:03:08.840 block_testing[683:13269753] my class:__NSMallocBlock__, ptr block = 0x100111d00
      2015-12-01 08:03:08.841 block_testing[683:13269753] my class:__NSMallocBlock__, ptr block = 0x100111d30

      по тексту сейчас поправлю


  1. AlexIzh
    01.12.2015 13:49
    +1

    Заранее извиняюсь, если своим комментарием я оскорблю ваши чувства.
    Данный комментарий является сугубо моим личным мнением.

    Мне кажется, или существует какой-то негласный закон, что статьи об одном и том же без новой информации должны публиковаться каждые пол года/год?
    Подобных статей на хабре уже достаточно(Что бы не быть голословным: Хабрапоиск). Да и на остальных ресурсах есть куча статей про блоки. Вы не внесли никакой новой информации в свою статью.
    Так же по заголовку статьи, я ожидал увидеть о том, как работают блоки, а не как с ними работать. Слишком поверхностно описали блоки.

    Например, сразу же возникают следующие вопросы: Что же есть стек хранения блоков, где он находится? Когда умирают блоки в стеке и какой их жизненный цикл в общем? Что есть куча? Какое её отличие от стека? Когда умирают глобальные блоки? Какой их жизненный цикл?


    1. ajjnix
      01.12.2015 14:52

      Спасибо большое за замечание и особенно за вопросы (не сарказм)
      Постараюсь написать продолжение (по вопросам) через пол года/год в таком случае и обещаю чаще пользоваться хабропоиском перед публикацией )


  1. aspcartman
    01.12.2015 22:21

    Blocks ABI опушен, а это самый сок. Тема не раскрыта :) ведь блоки нужны для того, чтобы их вызывать, а как они вызываются вы не описали.

    ЗЫ: советую помимо документации силанга посмотреть исходники, там больше информации можно почерпнуть.