Как избежать захвата себя в блоках при реализации 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?
8 ответов
Короткий ответ
Вместо доступа 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
больше нигде не используется).
Что лучше, будет зависеть от того, какой код вы можете изменить, а что нет. Но, надеюсь, это дало вам некоторые идеи о том, как поступить.
Также есть возможность подавить предупреждение, когда вы уверены, что цикл будет прерван в будущем:
#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.
Для общего решения у меня есть эти определения в заголовке прекомпиляции. Избегает захвата и все еще позволяет помощь компилятора, избегая использования id
#define BlockWeakObject(o) __typeof(o) __weak
#define BlockWeakSelf BlockWeakObject(self)
Тогда в коде вы можете сделать:
BlockWeakSelf weakSelf = self;
self.dataProcessor.completion = ^{
[weakSelf.delegate myAPIDidFinish:weakSelf];
weakSelf.dataProcessor = nil;
};
Я считаю, что решение без 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];
Объединяя несколько других ответов, я сейчас использую это для типизированного слабого "я" в блоках:
__typeof(self) __weak welf = self;
Я установил это как фрагмент кода XCode с префиксом завершения "welf" в методах / функциях, который срабатывает после ввода только "мы".
warning => "захват себя внутри блока может привести к циклу сохранения"
когда вы ссылаетесь на себя или его свойство внутри блока, который сильно сохраняет себя, чем показано выше, предупреждение.
поэтому, чтобы избежать этого, мы должны сделать это недельный реф
__weak typeof(self) weakSelf = self;
поэтому вместо использования
blockname=^{
self.PROPERTY =something;
}
мы должны использовать
blockname=^{
weakSelf.PROPERTY =something;
}
примечание: сохранение цикла обычно происходит, когда некоторые как два объекта ссылаются друг на друга, с помощью которых оба имеют счетчик ссылок =1 и их метод delloc никогда не вызывается.
Новый способ сделать это с помощью @weakify и @strongify marco
@weakify(self);
[self methodThatTakesABlock:^ {
@strongify(self);
[self doSomething];
}];
Если вы уверены, что ваш код не создаст цикл сохранения или что цикл будет прерван позже, то самый простой способ заставить предупреждение замолчать:
// 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), и предупреждение о цикле сохранения не генерируется.