Как избежать захвата себя в блоках при реализации API?

222

У меня есть работающее приложение, и я работаю над его преобразованием в ARC в Xcode 4.2. Одно из предупреждений перед проверкой включает в себя selfсильный захват в блоке, приводящем к циклу сохранения. Я сделал простой пример кода, чтобы проиллюстрировать проблему. Мне кажется, я понимаю, что это значит, но я не уверен, что это «правильный» или рекомендуемый способ реализации сценария такого типа.

  • self является экземпляром класса MyAPI
  • приведенный ниже код упрощен, чтобы показать только взаимодействия с объектами и блоками, относящимися к моему вопросу
  • Предположим, что MyAPI получает данные из удаленного источника, а MyDataProcessor работает с этими данными и производит вывод
  • процессор сконфигурирован с блоками для передачи информации о ходе и состоянии

Пример кода:

// code sample
self.delegate = aDelegate;

self.dataProcessor = [[MyDataProcessor alloc] init];

self.dataProcessor.progress = ^(CGFloat percentComplete) {
    [self.delegate myAPI:self isProcessingWithProgress:percentComplete];
};

self.dataProcessor.completion = ^{
    [self.delegate myAPIDidFinish:self];
    self.dataProcessor = nil;
};

// start the processor - processing happens asynchronously and the processor is released in the completion block
[self.dataProcessor startProcessing];

Вопрос: что я делаю "неправильно" и / или как это следует изменить, чтобы соответствовать соглашениям ARC?

XJones
источник

Ответы:

509

Короткий ответ

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

__block MyDataProcessor *dp = self;
self.progressBlock = ^(CGFloat percentComplete) {
    [dp.delegate myAPI:dp isProcessingWithProgress:percentComplete];
}

Эти __blockключевые слова , знаки переменных , которые могут быть изменены внутри блока (мы не делаем) , но и они не сохраняются автоматически , когда блок сохраняется (если вы не используете ARC). Если вы сделаете это, вы должны быть уверены, что больше ничего не будет пытаться выполнить блок после освобождения экземпляра MyDataProcessor. (Учитывая структуру вашего кода, это не должно быть проблемой.) Узнайте больше о__block .

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

Длинный ответ

Допустим, у вас был такой код:

self.progressBlock = ^(CGFloat percentComplete) {
    [self.delegate processingWithProgress:percentComplete];
}

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

Этот случай можно легко исправить, выполнив вместо этого:

id progressDelegate = self.delegate;
self.progressBlock = ^(CGFloat percentComplete) {
    [progressDelegate processingWithProgress:percentComplete];
}

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

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

self.dataProcessor.progress = ^(CGFloat percentComplete) {
    [self.delegate myAPI:self isProcessingWithProgress:percentComplete];
};

Здесь вы передаете selfнепосредственно делегату в вызове метода, поэтому вы должны получить его где-нибудь. Если у вас есть контроль над определением типа блока, лучше всего передать делегат в блок в качестве параметра:

self.dataProcessor.progress = ^(MyDataProcessor *dp, CGFloat percentComplete) {
    [dp.delegate myAPI:dp isProcessingWithProgress:percentComplete];
};

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

Если вы не можете изменить блок, вы можете с этим справиться . Причина сохранения цикла является предупреждением, а не ошибкой, в том, что они не обязательно означают гибель для вашего приложения. Если MyDataProcessorудастся освободить блоки после завершения операции, прежде чем родительский объект попытается освободить ее, цикл будет прерван, и все будет очищено должным образом. Если бы вы могли быть уверены в этом, то правильнее всего было бы использовать a #pragmaдля подавления предупреждений для этого блока кода. (Или используйте флаг компилятора для каждого файла. Но не отключайте предупреждение для всего проекта.)

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

__weak MyDataProcessor *dp = self; // OK for iOS 5 only
__unsafe_unretained MyDataProcessor *dp = self; // OK for iOS 4.x and up
__block MyDataProcessor *dp = self; // OK if you aren't using ARC
self.progressBlock = ^(CGFloat percentComplete) {
    [dp.delegate myAPI:dp isProcessingWithProgress:percentComplete];
}

Все три из вышеперечисленного дадут вам ссылку без сохранения результата, хотя все они ведут себя немного по-разному: __weakбудут пытаться обнулить ссылку, когда объект будет освобожден; __unsafe_unretainedоставит вас с неверным указателем; __blockфактически добавит еще один уровень косвенности и позволит вам изменить значение ссылки изнутри блока (не имеет значения в этом случае, так dpкак больше нигде не используется).

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

benzado
источник
1
Отличный ответ! Спасибо, я гораздо лучше понимаю, что происходит и как все это работает. В этом случае я контролирую все, поэтому я буду перестроять некоторые объекты по мере необходимости.
XJones
18
О_О Я просто проходил мимо с немного другой проблемой, застрял в чтении и теперь покидаю эту страницу, чувствуя себя хорошо осведомленным и крутым. Спасибо!
Орк JMR
правильно, что если по какой-то причине в момент исполнения блока dpбудет освобожден (например, если это был контроллер представления и он был выдвинут), то строка [dp.delegate ...вызовет EXC_BADACCESS?
peetonn
Должно ли свойство, содержащее блок (например, dataProcess.progress) быть strongили weak?
djskinner
1
Вы могли бы взглянуть на libextobjc, который предоставляет два вызываемых удобных макроса @weakify(..)и @strongify(...)который позволяет вам использовать selfв блоке без сохранения.
25

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

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-retain-cycles"

self.progressBlock = ^(CGFloat percentComplete) {
    [self.delegate processingWithProgress:percentComplete];
}

#pragma clang diagnostic pop

Таким образом , вы не должны обезьяны вокруг с __weak, selfсглаживание и явного Ивар префиксов.

Zoul
источник
8
Похоже, очень плохая практика, которая занимает более 3 строк кода, который можно заменить на __weak id weakSelf = self;
Бен Синклер
3
Там часто больший блок кода, который может извлечь выгоду из подавленных предупреждений.
Зул
2
За исключением того, что __weak id weakSelf = self;имеет принципиально иное поведение, чем подавление предупреждения. Вопрос начался с «... если вы уверены, что цикл сохранения будет нарушен»
Тим
Слишком часто люди слепо делают переменные слабыми, не понимая их последствий. Например, я видел, как люди ослабляют объект, а затем, в блоке, который они делают: [array addObject:weakObject];если слабый объект был выпущен, это вызывает сбой. Очевидно, что это не является предпочтительным по сравнению с циклом сохранения. Вы должны понять, действительно ли ваш блок живет достаточно долго, чтобы оправдать ослабление, а также хотите ли вы, чтобы действие в блоке зависело от того, действует ли слабый объект.
Махбудз
14

Для общего решения у меня есть эти определения в заголовке прекомпиляции. Избегает захвата и все еще позволяет помощь компилятора, избегая использованияid

#define BlockWeakObject(o) __typeof(o) __weak
#define BlockWeakSelf BlockWeakObject(self)

Тогда в коде вы можете сделать:

BlockWeakSelf weakSelf = self;
self.dataProcessor.completion = ^{
    [weakSelf.delegate myAPIDidFinish:weakSelf];
    weakSelf.dataProcessor = nil;
};
Дэмиен Понтифекс
источник
Согласен, это может вызвать проблемы внутри блока. У ReactiveCocoa есть еще одно интересное решение этой проблемы, которое позволяет вам продолжать использовать selfвнутри своего блока @weakify (self); id block = ^ {@strongify (self); [self.delegate myAPIDidFinish: self]; };
Дэмиен Понтифекс
@dmpontifex - это макрос из libextobjc github.com/jspahrsummers/libextobjc
Elechtron
11

Я считаю, что решение без ARC также работает с ARC, используя __blockключевое слово:

РЕДАКТИРОВАТЬ: Согласно примечаниям к переходу на ARC , объект, объявленный с __blockхранилищем, все еще сохраняется. Используйте __weak(предпочтительно) или __unsafe_unretained(для обратной совместимости).

// code sample
self.delegate = aDelegate;

self.dataProcessor = [[MyDataProcessor alloc] init];

// Use this inside blocks
__block id myself = self;

self.dataProcessor.progress = ^(CGFloat percentComplete) {
    [myself.delegate myAPI:myself isProcessingWithProgress:percentComplete];
};

self.dataProcessor.completion = ^{
    [myself.delegate myAPIDidFinish:myself];
    myself.dataProcessor = nil;
};

// start the processor - processing happens asynchronously and the processor is released in the completion block
[self.dataProcessor startProcessing];
Тони
источник
Не понимал, что __blockключевое слово избегало сохранения его референта. Спасибо! Я обновил свой монолитный ответ. :-)
Бензадо
3
Согласно документам Apple: «В режиме ручного подсчета ссылок __block id x; эффект не сохраняется x. В режиме ARC __block id x; по умолчанию сохраняется x (как и все другие значения)».
XJones
11

Объединяя несколько других ответов, я сейчас использую это для типизированного слабого «я» в блоках:

__typeof(self) __weak welf = self;

Я установил это как фрагмент кода XCode с префиксом завершения «welf» в методах / функциях, который срабатывает после ввода только «мы».

Кендалл Хельмштеттер Гелнер
источник
Ты уверен? Эта ссылка и документы clang, кажется, думают, что обе могут и должны использоваться для сохранения ссылки на объект, но не ссылки, которая вызовет цикл сохранения: stackoverflow.com/questions/19227982/using-block-and-weak
Kendall Хельмштеттер Гелнер
Из документов clang: clang.llvm.org/docs/BlockLanguageSpec.html "В языках Objective-C и Objective-C ++ мы разрешаем спецификатор __weak для переменных __block типа объекта. Если сборка мусора не включена, этот квалификатор вызывает эти переменные должны быть сохранены без сохранения отправляемых сообщений. "
Кендалл Хельмштеттер Гелнер,
6

warning => "захват себя внутри блока может привести к циклу сохранения"

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

поэтому, чтобы избежать этого, мы должны сделать это недельный реф

__weak typeof(self) weakSelf = self;

поэтому вместо использования

blockname=^{
    self.PROPERTY =something;
}

мы должны использовать

blockname=^{
    weakSelf.PROPERTY =something;
}

примечание: сохранение цикла обычно происходит, когда некоторые, как два объекта, ссылающиеся друг на друга, с помощью которых оба имеют счетчик ссылок = 1, и их метод delloc никогда не вызывается.

Анураг Бхакуни
источник
-1

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

// code sample
self.delegate = aDelegate;

self.dataProcessor = [[MyDataProcessor alloc] init];

[self dataProcessor].progress = ^(CGFloat percentComplete) {
    [self.delegate myAPI:self isProcessingWithProgress:percentComplete];
};

[self dataProcessor].completion = ^{
    [self.delegate myAPIDidFinish:self];
    self.dataProcessor = nil;
};

// start the processor - processing happens asynchronously and the processor is released in the completion block
[self.dataProcessor startProcessing];

Причина, по которой это работает, заключается в том, что хотя точечный доступ к свойствам учитывается анализом XCode, и поэтому

x.y.z = ^{ block that retains x}

считается сохраняемым по x of y (слева от присвоения) и y y x (справа), вызовы методов не подвергаются такому же анализу, даже если они являются вызовами методов доступа к свойству которые эквивалентны точке доступа, даже когда эти методы доступа к свойству генерируются компилятором, поэтому в

[x y].z = ^{ block that retains x}

только правая сторона рассматривается как создающая сохранение (по y of x), и предупреждение о цикле сохранения не генерируется.

Бен Артин
источник