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

Передача блоков в Objective-C

При написании метода, который принимает блок как аргумент, мне нужно сделать что-нибудь особенное, например, скопировать блок в кучу до его выполнения? Например, если бы у меня был следующий метод:

- (void)testWithBlock:(void (^)(NSString *))block {
    NSString *testString = @"Test";
    block(testString);
}

Должен ли я что-либо делать с block перед его вызовом или при вводе метода? Или приведенный выше способ использования переданного блока? Кроме того, следующий способ вызова метода правильный, или мне нужно что-то сделать с блоком перед его передачей?

[object testWithBlock:^(NSString *test){
    NSLog(@"[%@]", test);
}];

Где мне нужно скопировать блок? И как это было бы иначе, если бы я не использовал ARC?

4b9b3361

Ответ 1

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

typedef void (^IDBlock) (id);
@implementation MyClass{
    IDBlock _completionBlock;
}

Однако вам также нужно скопировать его, если вы собираетесь добавить его в любой класс коллекции, например NSArray или NSDictionary. В противном случае вы получите ошибки (скорее всего, EXC_BAD_ACCESS) или, возможно, повреждение данных при попытке выполнить блок позже.

Когда вы выполняете блок, важно сначала проверить, если блок nil. Objective-c позволит вам передать nil в параметр метода блока. Если этот блок равен нулю, вы получите EXC_BAD_ACCESS при попытке выполнить его. К счастью, это легко сделать. В вашем примере вы должны написать:

- (void)testWithBlock:(void (^)(NSString *))block {
    NSString *testString = @"Test";
    if (block) block(testString);
}

В копировальных блоках существуют соображения производительности. По сравнению с созданием блока в стеке, копирование блока в кучу не является тривиальным. Это не серьезная сделка в целом, но если вы используете блок итеративно или используете кучу блоков итеративно и копируете их при каждом выполнении, это создаст хит производительности. Поэтому, если ваш метод - (void)testWithBlock:(void (^)(NSString *))block; был в каком-то виде, копирование этого блока может повредить вашу производительность, если вам не нужно его копировать.

Еще одно место, где вам нужно скопировать блок, - это если вы намереваетесь называть этот блок сам по себе (рекурсия блока). Это не все так часто, но вы должны скопировать блок, если вы намереваетесь это сделать. См. Мой вопрос/ответ на SO здесь: Рекурсивные блоки В Objective-C.

Наконец, если вы собираетесь хранить блок, вам нужно быть очень осторожным в создании циклов сохранения. Блоки будут сохранять любой объект, переданный в него, и если этот объект является переменной экземпляра, он сохранит класс переменной экземпляра (self). Я лично люблю блоки и использую их все время. Но есть причина, по которой Apple не использует/хранит блоки для своих классов UIKit и вместо этого придерживается шаблона target/action или delegate. Если вы (класс, создающий блок) сохраняете класс, который получает/копирует/хранит блок, и в этом блоке вы ссылаетесь либо на свою, либо на ANY переменную экземпляра класса, вы создали цикл сохранения (classA → classB → block → classA). Это замечательно легко сделать, и это то, что я делал слишком много раз. Более того, "Утечки" в "Инструментах" этого не поймают. Метод обойти это легко: просто создайте временную переменную __weak (для ARC) или __block (не-ARC), и блок не сохранит эту переменную. Так, например, следующее будет циклом сохранения, если "объект" копирует/сохраняет блок:

[object testWithBlock:^(NSString *test){
    _iVar = test;
    NSLog(@"[%@]", test);
}];

Однако, чтобы исправить это (используя ARC):

__weak IVarClass *iVar = _iVar;
[object testWithBlock:^(NSString *test){
    iVar = test;
    NSLog(@"[%@]", test);
}];

Вы также можете сделать что-то вроде этого:

__weak ClassOfSelf _self = self;
[object testWithBlock:^(NSString *test){
    _self->_iVar = test;
    NSLog(@"[%@]", test);
}];

Обратите внимание, что многим людям это не нравится, потому что они считают его хрупким, но он является допустимым способом доступа к переменным. Обновление. Текущий компилятор теперь предупреждает, если вы попытаетесь получить прямой доступ к переменной с помощью "- > ". По этой причине (а также причинам безопасности) лучше всего создать свойство для переменных, к которым вы хотите получить доступ. Поэтому вместо _self->_iVar = test; вы должны использовать: _self.iVar = test;.

ОБНОВЛЕНИЕ (подробнее)

Как правило, лучше всего рассмотреть метод, который получает блок как ответственный за определение того, нужно ли копировать блок, а не вызывающий. Это связано с тем, что метод приема может быть единственным, который знает, как долго блок должен быть сохранен или он должен быть скопирован. Вы (как программист), очевидно, узнаете эту информацию при написании вызова, но если вы мысленно рассмотрите вызывающего и приемного устройств на отдельных объектах, вызывающий абонент получит блок и выполнит его. Поэтому ему не нужно знать, что делается с блоком после его ухода. С другой стороны, вполне возможно, что вызывающий может уже скопировать блок (возможно, он сохранил блок и теперь передает его другому методу), но получатель (который также намерен хранить блок) должен все же скопировать блок (хотя блок уже скопирован). Получатель не может знать, что блок уже скопирован, а некоторые блоки, которые он получает, могут быть скопированы, а другие могут отсутствовать. Поэтому приемник должен всегда копировать блок, который он намерен хранить? Имеют смысл? Это, по сути, хорошая объектно-ориентированная практика проектирования. В принципе, тот, кто имеет информацию, несет ответственность за ее обработку.

Блоки широко используются в Apple GCD (Grand Central Dispatch), чтобы легко включить многопоточность. В общем, вам не нужно копировать блок, когда вы отправляете его на GCD. Как ни странно, это немного противоречит интуиции (если вы думаете об этом), потому что если вы отправляете блок асинхронно, часто метод, который был создан в блоке, будет возвращен до того, как блок выполнит, что обычно означает, что блок истечет, поскольку он объект стека. Я не думаю, что GCD копирует блок в стек (я читал это где-то, но не смог найти его снова), вместо этого я думаю, что жизнь потока расширяется, ставя на другой поток.

Майк Эш имеет обширные статьи о блоках, GCD и ARC, которые могут оказаться полезными:

Ответ 2

Все выглядит хорошо. Хотя, возможно, вы захотите дважды проверить параметр блока:

@property id myObject;
@property (copy) void (^myBlock)(NSString *);

....

- (void)testWithBlock: (void (^)(NSString *))block
{
    NSString *testString = @"Test";
    if (block)
    {
        block(test);
        myObject = Block_copy(block);
        myBlock = block;
    }
}

...

[object testWithBlock: ^(NSString *test)
{
    NSLog(@"[%@]", test);
}];

Должно быть хорошо. И я считаю, что они даже пытаются поэтапно отказаться от Block_copy(), но они еще не сделали.

Ответ 3

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

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

В случае, когда вы описываете, вы можете в принципе думать о блоке как о простом параметре для своего метода, как если бы это был int или другой примитивный тип. Когда метод вызывается, пространство в стеке будет выделено для параметров метода, и поэтому блок будет жить в стеке в течение всего выполнения вашего метода (как и все остальные параметры). Когда кадр стека выталкивается из верхней части стека при возврате метода, стек стека, выделенный блоку, будет освобожден. Таким образом, во время выполнения вашего метода, блок, как гарантируется, будет жив, поэтому нет управления памятью для работы здесь (как в ARC, так и в случае без ARC). Другими словами, ваш код в порядке. Вы можете просто вызвать блок внутри метода.

Как следует из цитированного текста, единственный раз, когда вам нужно явно скопировать блок, вы хотите, чтобы он был доступен из-за пределов области, в которой он был создан (в вашем случае, за исключением срока службы фрейма стека вашего метод). В качестве примера предположим, что вам нужен метод, который извлекает некоторые данные из Интернета и запускает блок кода, когда выборка завершена. Ваша подпись метода может выглядеть так:

- (void)getDataFromURL:(NSURL *)url completionHandler:(void(^)(void))completionHandler;

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

@interface MyClass

@property (nonatomic, copy) void(^dataCompletion)(NSData *);

@end



@implementation MyClass
@synthesize dataCompletion = _dataCompletion;

- (void)getDataFromURL:(NSURL *)url completionHandler:(void(^)(NSData *fetchedData))completionHandler {
    self.dataCompletion = completionHandler;
    [self fetchDataFromURL:url]; 
}

- (void)fetchDataFromURL:(NSURL *)url {
    // Data fetch starts here 
}

- (void)finishedFetchingData:(NSData *)fetchedData {
    // Called when the data is done being fetched
    self.dataCompletion(fetchedData)
    self.dataCompletion = nil; 
}

В этом примере использование свойства с семантикой copy будет выполнять Block_copy() на блоке и скопировать его в кучу. Это происходит в строке self.dataCompletion = completionHandler. Таким образом, блок перемещается из фрейма стека метода -getDataFromURL:completionHandler: в кучу, что позволяет называть его позже в методе finishedFetchingData:. В последнем методе строка self.dataCompletion = nil аннулирует свойство и отправляет Block_release() в сохраненный блок, тем самым освобождая его.

Использование свойства таким образом является приятным, поскольку оно по существу будет обрабатывать все управление блочной памятью (просто убедитесь, что это свойство copy (или strong), а не просто retain), и будет работают как в случаях, не связанных с ARC, так и в ARC. Если вместо этого вы хотели использовать исходную переменную экземпляра для хранения вашего блока и работали в среде, отличной от ARC, вам нужно было бы вызвать Block_copy(), Block_retain() и Block_release() самостоятельно во всех необходимых местах, если вы захотите чтобы сохранить блок дольше, чем время жизни метода, в котором он передавался как параметр. Тот же самый код, написанный с использованием ivar вместо свойства, будет выглядеть так:

@interface MyClass {
    void(^dataCompletion)(NSData *);
}

@end



@implementation MyClass

- (void)getDataFromURL:(NSURL *)url completionHandler:(void(^)(NSData *fetchedData))completionHandler {
    dataCompletion = Block_copy(completionHandler);
    [self fetchDataFromURL:url]; 
}

- (void)fetchDataFromURL:(NSURL *)url {
    // Data fetch starts here 
}

- (void)finishedFetchingData:(NSData *)fetchedData {
    // Called when the data is done being fetched
    dataCompletion(fetchedData)
    Block_release(dataCompletion);
    dataCompletion = nil;
}

Ответ 4

Вы знаете, что есть два типа блоков:

  • блоки, хранящиеся в стеке, те, которые вы явно пишете как ^ {...}, и которые исчезают, как только функция, которую они создают в возвратах, так же, как и обычные переменные стека. Когда вы вызываете блок стека после возвращения функции, к которой он принадлежал, происходят плохие вещи.

  • блоки в куче, те, которые вы получаете при копировании другого блока, те, которые живут до тех пор, пока какой-то другой объект сохраняет ссылку на них, как обычные объекты.

Единственная причина, по которой вы копируете блок, - это когда вам предоставляется блок, который является или может быть блоком стека (явный локальный блок ^ {...} или аргумент метода, происхождение которого вы не знаете), И что вы хотите продлить срок службы из ограниченного одного из блоков стека и что компилятор еще не выполнил эту работу для вас.

Подумайте: сохраняя блок в переменной экземпляра.

Добавление блока в коллекцию, например NSArray.

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

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