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

У меня есть рабочее приложение, и я работаю над его преобразованием в 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?

Ответ 1

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

Вместо прямого доступа к 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 может освободить блоки, когда операция будет завершена, прежде чем ее родитель попытается ее выпустить, цикл будет сломан, и все будет очищено должным образом. Если бы вы были в этом уверены, правильная вещь - использовать #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 больше не используется).

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

Ответ 2

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

#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 сглаживания и явного префикса ivar.

Ответ 3

Для общего решения я определяю их в заголовке precompile. Предотвращает захват и позволяет включить помощь компилятора, избегая использования id

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

Затем в коде вы можете:

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

Ответ 4

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

EDIT: по Переход к заметкам о выпуске 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];

Ответ 5

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

__typeof(self) __weak welf = self;

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

Ответ 6

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

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

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

__weak typeof(self) weakSelf = self;

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

blockname=^{
    self.PROPERTY =something;
}

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

blockname=^{
    weakSelf.PROPERTY =something;
}

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

Ответ 7

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

// 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];

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

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

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

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

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