Як я можу уникнути захоплення самоврядування в блоки під час впровадження 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?

Відповіді:


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не використовується більше ніде).

Що найкраще буде залежати від того, який код ви можете змінити, а який ви не можете. Але, сподіваємось, це дало вам кілька ідей, як діяти далі.


1
Дивовижна відповідь! Дякую, я набагато краще розумію, що відбувається і як це все працює. У цьому випадку я маю контроль над усім, тож я перероблю деякі об’єкти за потребою.
XJones

18
O_O Я щойно проходив повз, дещо інша проблема, застрягла в читанні, і тепер залишаю цю сторінку, відчуваючи все обізнаною та крутою. Дякую!
Орк 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згладжування і явного Івар префіксів.


8
Здається, що це дуже погана практика, яка займає понад 3 рядки коду, які можна замінити на __weak id слабкіSelf = self;
Бен Сінклер

3
Часто існує більший блок коду, який може отримати користь від придушених попереджень.
zoul

2
За винятком того, що __weak id weakSelf = self;має принципово іншу поведінку, ніж придушення попередження. Питання почалося з "... якщо ви впевнені, що цикл збереження буде порушений"
Тім

Занадто часто люди сліпо роблять змінні слабкими, не розуміючи насправді наслідків. Наприклад, я бачив, як люди слабшають об'єктом, а потім у блоці, який вони роблять: [array addObject:weakObject];Якщо слабкийОб'єкт був випущений, це спричиняє збій. Очевидно, що це не є кращим протягом циклу утримування. Ви повинні зрозуміти, чи дійсно ваш блок живе достатньо довго, щоб вимагати послаблення, а також чи хочете, щоб дії в блоці залежали від того, чи слабкий об'єкт все-таки дійсний.
mahboudz

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 блок = ^ {@strongify (самоврядування); [self.delegate myAPIDidFinish: self]; };
Дамієн Понтіфекс

@dmpontifex - це макрос від libextobjc github.com/jspahrsummers/libextobjc
Elechtron

11

Я вважаю, що рішення без 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];

Не усвідомлював, що __blockключове слово уникає збереження референтного. Дякую! Я оновив свою монолітну відповідь. :-)
benzado

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 Helmstetter Gelner

З clang docs: clang.llvm.org/docs/BlockLanguageSpec.html "У мовах Objective-C і Objective-C ++ ми дозволяємо специфікатор __weak для змінних __block типу об'єкта. Якщо збирання сміття не ввімкнено, цей класифікатор викликає ці змінні слід зберігати, не зберігаючи повідомлення. "
Кендалл Гельмстеттер Гелнер


6

попередження => "Захоплення самості в блоці, ймовірно, призведе до циклу збереження"

коли ви посилаєтесь на себе або його властивості всередині блоку, який сильно зберігає себе, ніж це видно з попередження.

тож, щоб уникнути цього, ми мусимо зробити це за тиждень

__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 y (ліворуч від призначення) та y на x (на правій стороні), виклики методів не підлягають аналогічному аналізу, навіть коли вони є викликами методу доступу до власності які еквівалентні точковому доступу, навіть коли ці методи доступу до властивостей створюються компілятором, т. ін

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

тільки правий бік розглядається як створює фіксатор (по y з x), і не створюється попередження про збереження циклу.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.