Подтвердить что ты не робот

Должен ли я копировать /Block _copy блоки под ARC?

Я только что наткнулся на следующую тему SO: Зачем нам копировать блоки, а не сохранять?, которая имеет следующее предложение:

Однако с iOS 6 они рассматриваются как обычные объекты, поэтому вам не нужно беспокоиться.

Я был очень смущен этим утверждением, поэтому я спрашиваю: действительно ли это утверждение подразумевает, что разработчикам Objective-C не нужно

@property (copy) blockProperties или

[^(...){...) {} copy]

скопировать блоки и их содержимое из стека в кучу?

Я надеюсь, что описание, которое я сделал, ясно.

Пожалуйста, будьте подробными.


Похожие вопросы

В ARC, блоки автоматически копируются при назначении ivar непосредственно?.

4b9b3361

Ответ 1

ARC автоматически скопирует блок. From clang Objective-C Автоматический подсчет ссылок:

За исключением сохранений, выполняемых как часть инициализации переменной параметра __strong или чтения переменной __weak, всякий раз, когда эти семантики требуют сохранения значения типа блока-указателя, он имеет эффект Block_copy, Оптимизатор может удалить такие копии, когда видит, что результат используется только в качестве аргумента для вызова.

Таким образом, блоки, используемые только как аргументы для вызовов функций или методов, могут оставаться блоками стека, но в любом случае, где ARC сохраняет блок, он будет копировать блок. Это реализовано компилятором, испускающим вызов objc_retainBlock(), , для которого:

id objc_retainBlock(id x) {
    return (id)_Block_copy(x);
}

По-прежнему рекомендуется объявлять свойства блока как имеющие семантику копирования, поскольку на самом деле будет скопирован блок, назначенный сильному свойству. Apple рекомендует это:

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

Обратите внимание, что, поскольку эта функция копирования-на-удержания предоставляется ARC, она зависит только от доступности ARC или ARCLite и в противном случае не требует конкретной версии ОС или OS_OBJECT_USE_OBJC.

Ответ 2

Edit:

Оказалось, что рассмотрение адресов "захваченных" переменных трудно интерпретировать, и не всегда уместно выяснить, был ли блок скопирован в кучу или все еще находится в куче. Хотя спецификация блоков, приведенная здесь СПЕЦИФИКАЦИЯ БЛОКА РЕАЛИЗАЦИИ, достаточно подробно описывает факты, я пробую совершенно другой подход:

Что такое блок?

Это сводка официальных спецификаций СПЕЦИФИКАЦИЯ БЛОКА РЕАЛИЗАЦИИ:

Блок существует кода (как функция) и структуры, содержащей несколько строк данных, флагов и указателей функций И ​​раздел переменной длины для "захваченных переменных".

Обратите внимание, что эта структура является частной и реализована.

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

Блок может "импортировать" другие ссылки блоков, другие переменные и __block измененные переменные.

Когда блок ссылается на другие переменные, они будут импортированы:

  • Локальная (автоматическая) переменная стека будет "импортирована" посредством создания "const copy".

  • Измененная переменная A __block будет импортирована путем присвоения указателю адреса этой переменной, заключенной в другую структуру.

  • Глобальные переменные будут просто привязаны (не "импортированы" ).

Если переменная будет импортирована, "захваченная переменная" будет находиться в вышеупомянутой структуре в разделе переменной длины. То есть, "аналог" автоматической переменной (которая живет за пределами блока) имеет хранилище в структуре блока.

Поскольку эта "захваченная переменная" доступна только для чтения, компилятор может применять несколько оптимизаций: например, нам действительно нужен только один экземпляр захваченной переменной в куче, если нам когда-либо понадобится копия блока.

Значение захваченных переменных будет установлено, когда будет выражено выражение литерала блока. Это также подразумевает, что хранение захваченных переменных должно быть доступно для записи (то есть, нет секции кода).

Время жизни захваченных переменных - это функция: каждый вызов этого блока потребует новой копии этих переменных.

Захваченные переменные в блоках, которые живут в стеке, уничтожаются, когда программа оставляет составную инструкцию блока.

Захваченные переменные в блоках, которые живут в куче, уничтожаются, когда блок будет уничтожен.

В соответствии с API-интерфейсом блока версии 3.5:

Изначально, когда создается блок-литерал, эта структура будет существовать в стеке:

Когда оценивается выражение Block literal, структура на основе стека инициализируется следующим образом:

  • Статическая структура дескриптора объявляется и инициализируется следующим образом:

    а. Указатель функции вызова устанавливается в функцию, которая принимает структуру блока в качестве своего первого аргумента, а остальные аргументы (если есть) в блок и выполняет оператор составной части блока.

    б. Поле размера установлено в размере следующей структуры блока.

    с. Указатели функции copy_helper и dispose_helper устанавливаются соответствующими вспомогательными функциями, если они требуются литералом Block.

  • Создана и инициализируется структура стек (или глобальная) блока данных литерала следующим образом:

    а. Поле isa устанавливается на адрес внешнего _NSConcreteStackBlock, который является блоком неинициализированной памяти, поставляемой в libSystem, или _NSConcreteGlobalBlock, если это статический или файловый литерал блока.

    б. Поле flags устанавливается равным нулю, если в блок не импортированы переменные, которым требуются вспомогательные функции для операций Block_copy() и Block_release() программного уровня, и в этом случае устанавливается бит бит (1 < 25).

Обратите внимание, что это для Блочных литералов.

Согласно Objective-C Расширения к блокам, компилятор будет обрабатывать объекты Blocks как.

Наблюдения

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

  • _Block_copy_internal и

  • malloc (который должен быть включен только после того, как была удалена первая точка останова)

а затем запустите соответствующий тестовый код (например, ниже) и проверьте, что происходит:

В следующем фрагменте кода мы создаем литерал блока и передаем его как параметр функции, которая его вызывает:

typedef void (^block_t)(void);

void func(block_t block) {
    if (block) {
        block();
    }
}

void foo(int param)
{
    int x0 = param;
    func(^{
        int y0 = x0;
        printf("Hello block 1\n");
        printf("Address of auto y0: %p\n", &y0);
        printf("Address of captured x0: %p\n", &x0);
    });
}  

Выход выглядит следующим образом:

Hello block 1
Address of auto y0: 0x7fff5fbff8dc
Address of captured x0: 0x7fff5fbff940

Адрес "захваченной" переменной x0 сильно указывает, что он живет в стеке.

Мы также установили точку останова при _Block_copy_internal - однако она не будет удалена. Это указывает на то, что блок не был скопирован в кучу. Еще одно доказательство может быть сделано с помощью инструментов, в которых не отображаются распределения в функции foo.

Теперь, если мы создадим и инициализируем блочную переменную, кажется, структура блоков данных исходного литерала блока, которая изначально создана в стеке, будет скопирована в кучу:

int capture_me = 1;
dispatch_block_t block = ^{ int y = capture_me; };

enter image description here

Это выше копирует блок, который изначально был создан в стеке. Это может произойти просто из-за ARC и того факта, что у нас есть блок-буква справа, а блочной переменной block в левой руке будет назначен блок-литерал, что приведет к операции Block_copy. Это делает блоки похожими на обычные объекты Objective-C.

Распределение, прослеживаемое с помощью инструментов в этом аналогичном коде ниже

void foo(int param)
{
    dispatch_queue_t queue = dispatch_queue_create("queue", 0);

    int x0 = param;
    dispatch_block_t block = ^{
        int y0 = x0;
        printf("Hello block 1\n");
        printf("Address of auto y0: %p\n", &y0);
        printf("Address of captured x0: %p\n", &x0);
    };

    block();
}    

показать, что блок действительно будет скопирован:

enter image description here

Обращает на себя внимание

  • Когда блок не фиксирует какие-либо переменные, он похож на обычную функцию. Тогда имеет смысл применять оптимизацию, где операция Block_copy фактически ничего не делает, поскольку копировать нечего. clang реализует это, делая такие блоки "глобальным блоком".

  • При отправке copy в блочную переменную и присвоении результата другой блок-переменной, например:

    dispatch_block_t block = ^{
        int y0 = capture_me;
    };
    dispatch_block_t otherBlock = [block copy];
    

    copy будет довольно дешевой операцией, так как блок block уже выделил хранилище для структуры блоков, которые могут совместно использоваться. Таким образом, copy не нужно снова выделять хранилище.

Вернуться к вопросу

Чтобы ответить на вопрос, нужно ли явно копировать блок в определенных обстоятельствах, например:

@property (copy) block_t completion

[^{...} copy]

Ну, как только блок был скопирован, независимо от того, когда ему будет присвоена целевая переменная (переменная блока), мы должны быть всегда безопасными без явного копирования блока, поскольку он уже находится в куче.

В случае свойства блока, это должно быть безопасно, если бы мы просто выступили:

@property dispatch_block_t completion;

а затем:

foo.completion = ^{ x = capture_me; ... };

Это должно быть безопасно, так как назначение блочного литерала (который живет в стеке) базовой блок-переменной _completion, скопирует блок в кучу.

Тем не менее, я бы по-прежнему рекомендовал использовать атрибут copy - поскольку он по-прежнему предлагается официальной документацией как лучшая практика, а также поддерживает более старые API, где блоки не ведут себя как обычные объекты Objective-C и где есть нет ARC.

Существующие системные API также будут заботиться о необходимости копирования блока:

dispatch_async(queue, ^{int x = capture_me;});

dispatch_async() сделает копию для нас. Поэтому нам не нужно беспокоиться.

Другие сценарии более тонкие:

dispatch_block_t block;
if (condition) {
    block = ^{ ... };
}
else {
    block = ^{ ... };
}
dispatch_sync(queue, block);

Но на самом деле это безопасно: литерал блока будет скопирован и назначен блочная переменная block.

Этот пример может выглядеть даже страшно:

int x0 = param;
NSArray* array = [NSArray arrayWithObject:^{
    int y0 = x0;
    printf("Hello block 1\n");
}];

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), queue, ^{
    block_t block = array[0];
    block();
});

Но, похоже, литерал блока будет правильно скопирован как эффект назначения аргумента (блока) параметру (a id) в методе arrayWithObject:, который копирует литерал блока в кучу:

enter image description here

Кроме того, NSArray сохранит объект, переданный как параметр в методе arrayWithObject:. Это вызывает другой вызов Block_copy(), однако, поскольку блок уже находится в куче, этот вызов не выделяет хранилище.

Это означает, что сообщение "сохранить", отправленное в блок-литерал где-то в реализации arrayWithObject:, также действительно скопирует блок.

Итак, когда нам действительно нужно явно скопировать блок?

Блок-литерал - например, выражение:

^{...}

создаст структуру блока в стеке.

Итак, нам действительно нужно сделать копию только в тех случаях, когда нам нужен блок в куче, и когда это не произойдет "автоматически". Однако практически нет сценариев, где это происходит. Даже в тех случаях, когда мы передаем блок как параметр методу, который не знает, что он является блочным литералом и требует сначала copy (например: arrayWithObject:), и, возможно, получатель отправляет только сообщение сохранения в объект, заданный в параметре, сначала будет скопирован блок в кучу.

Примеры, в которых мы могли бы явно использовать copy, - это то, где мы не назначаем блок-литерал блочной переменной или id, и поэтому ARC не может понять, что он должен сделать копию блочного объекта или должен отправьте "сохранить".

В этом случае выражение

[^{...} copy];  

даст автореализованный блок, структура которого находится в куче.

Ответ 3

Мой оригинальный ответ был неправильным. Отредактированный ответ - это не ответ, а больше "это действительно хороший вопрос".

Пожалуйста, см. ответ Matt для реальных ссылок, почему материал такой, как есть.

После тестирования следующего кода на iOS7:

int stackVar;
void (^someBlock)() = ^(){};
NSLog(@"int: %x\nblock: %x,\ncopied: %x", (unsigned int)&stackVar, (unsigned int)someBlock, (unsigned int)[someBlock copy]);

Я получил это:

int: bfffda70
block: 9d9948
copied: 9d9948

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

Это, однако, не подкреплено каким-либо официальным источником, поскольку они все еще утверждают, что блоки создаются в стеке при необходимости копировать "при передаче".


Часть ответа перед тестированием, указав, какие документы противоречат этому примеру.

Документ о переходе на ARC:

Блоки "просто работают", когда вы передаете блоки вверх в стек в режиме ARC, например, в возврате. Вам больше не нужно звонить Block_copy. Вам все равно нужно использовать [^{} copy] при передаче "вниз" стека в arrayWithObjects: и других методах, которые сохраняют.

И документы о блоках и свойствах говорят:

Вы должны указать copy как атрибут свойства, потому что необходимо скопировать блок, чтобы отслеживать его захваченное состояние вне исходной области.