Решение проблемы с циклическими ссылками в блоках ObjC

О блоках в ObjC и правильной работе с ними написано очень много, в том числе и на хабре. Вопрос о том, как правильно работать с self в блоках, чтобы избежать циклических ссылок, регулярно задается на собеседованиях. При использовании таких фреймворков, как ReactiveCocoa количество блоков в коде сильно возрастает, при этом увеличивается шанс допустить ошибку и потерять в памяти объекты. Про попытку окончательно решить эту проблему, метапрограммирование для с99 с экстеншнами и блоками + хипстерсткие макросы с @ под катом.Рассмотрим проблему и способы ее решения эволюционно. self.block = ^{ [self f1]; [self f2]; }; Этот код очевидно содержит проблему. Без зануления self.block объект никогда не сможет быть удален, поскольку блок ссылается на self. При включенном LANG_WARN_OBJC_IMPLICIT_RETAIN_SELF компилятор даже выдаст предупреждение.Улучшение 1: __weak __typeof (self)weakSelf = self; self.block = ^{ [weakSelf m1]; [weakSelf m2]; }; Проблема циклической ссылки решена, но возникает другая. На момент вызова блока объект weakSelf либо существует, либо уже нет. Если объект уже не существует, weakSelf == nil, m1 и m2 не вызовутся — казалось бы, все в порядке. Однако, может получиться так, что на момент вызова m1 объект еще существует, а на момент вызова m2 уже нет. При этом m1 вызовется, а m2 нет — такое поведение может быть неожиданным и неправильным. Это может произойти как при race condition в многопоточном приложении, либо если m1 уменьшает количество ссылок на объект (например, удаляет объект из какой-нибудь коллекции). В случае включенных CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK и CLANG_WARN_OBJC_RECEIVER_WEAK компилятор выдает предупреждение для этого случая.Улучшение 2: __weak typeof (self)weakSelf = self; self.block = ^{ __strong typeof (self)strongSelf = weakSelf; [strongSelf m1]; [strongSelf m2]; }; Проблема с консистентностью вызовов методов внутри блока решена. Но обнаруживается новая: __weak typeof (self)weakSelf = self; self.block = ^{ __strong typeof (self)strongSelf = weakSelf; [strongSelf m1]; [strongSelf m2]; NSAssert (foo == bar, @«Cool assert!») }; Макросы, такие как NSAssert и RACObserve, неявно используют self, и проблема с циклической ссылкой возвращается.Улучшение 3: __weak typeof (self)weakSelf = self; self.block = ^{ __strong typeof (self)self = weakSelf; [self m1]; [self m2]; NSAssert (foo == bar, @«Cool assert!») }; Теперь проблема с макросами использующими self решена, но при включенном GCC_WARN_SHADOW компилятор выдает предупрежнение.Улучшение 4: В библиотеке libextobjc есть макросы @weakify и @stongify которые убирают предупреждение компилятора и немного упрощают код. @weakify (self); // self теперь новая локальная переменная с __weak self.block = ^{ @strongify (self); // self теперь новая локальная переменная с __strong [self m1]; [self m2]; NSAssert (foo == bar, @«Cool assert!») }; Это почти оптимальное решение, но оно все еще не лишено нескольких недостатков: нужно не забыть поставить в нужные места @weakify и @strongify; использование self после @weakify безопасно, но компилятор может выдавать предупрежнение.При этом все еще остается вероятность случайно захватить в блоке self по сильной ссылке: @weakify (self); // self теперь новая локальная переменная с __weak self.block = ^{ @strongify (self); // self теперь новая локальная переменная с __strong [self m]; NSLog (@«Ivar value form object: %@», _ivar); // Сильная ссылка на self сохраняется неявно для доступа к _ivar NSAssert (foo == bar, @«Cool assert!») }; Для того, что бы этого избежать нужно либо использовать только доступ через property (self.ivar), либо явно использовать переопределенный self: @weakify (self); // self теперь новая локальная переменная с __weak self.block = ^{ @strongify (self); // self теперь новая локальная переменная с __strong [self m]; NSLog (@«Ivar value form object: %@», self→_ivar); // Явно используем свой переопределенный self для доступа к _ivar NSAssert (foo == bar, @«Cool assert!») }; При этом нужно помнить, что self может быть nil, и явное разыменование self→_ivar вызовет креш.С учетом всех этих проблем возникла идея написать макрос, который будет модифицировать не self, а сам блок таким образом, что:

self вне scope блока изменяться не должен, как в случае @weakify внутри блока self должен называться self, чтобы избежать неожиданностей с NSAssert и другими макросами до момента вызова блока объект, на который указывает self, хранится по слабой ссылке, а во время вызова блока — по сильной по возможности макрос должен помогать находить блоки, которые неявно захватили self через _ivar все проверки типов должны работать так же, как и без макроса минимизировать изменения в коде при использовании этого макроса overhead в runtime должен быть минимальный Макрос должен работать примерно как функция-декоратор в Python, принимать на вход блок и заворачивать его в новый блок-обертку совместимый по параметрам и возвращаемому значению. Для примера рассмотрим блок: self.block = ^(NSObject *obj) { NSLog (@»%@ %@», [self description], obj); return 0; }; Начнем модифицировать блок таким образом, чтобы self захватывался как слабая ссылка, по аналогии с кодом из «Улучшение 1». Для этого нам нужен новый scope в котором эта локальная ссылка будет объявлена. В качестве такого scope подойдет анонимный блок, который вызывается сразу после создания: self.block = ^{ __weak typeof (self) weakSelf = self; return ^(NSObject *obj) { NSLog (@»%@ %@», [weakSelf description], obj); return 0; }; }(); Компилятор автоматически выведет тип возвращаемого значения для внешнего безымянного блока, все остается типобезопасным.Теперь нужно каким-то образом сделать так, что бы в момент вызова внутри тела внутреннего блока self становился сильной ссылкой. Для этого придется разделить блок на 2 части: декларацию типа ^(NSObject *obj) и, собственно, само тело в {… }. Превратим тело нашего блока в блок без параметров и поместим его вызов в еще один блок, созданный с использованием декларации типа, который превратит self в сильную ссылку:

self.block = ^{ __weak typeof (self) weakSelf = self; return ^(NSObject *obj) { __strong typeof (self)self = weakSelf; return ^ (void) { NSLog (@»%@, %@», [self description], obj); return 0; }(); }; }(); Основной трюк — это замена исходного блока, эквивалентным ему, но который неявно захватывает weakSelf вместо self, а в момент вызова превращает его в strongSelf. return ^(NSObject *obj) { __weak typeof (self)self = weakSelf; return ^ (void) { NSLog (@»%@, %@», [self description], obj); return 0; }(); }; по сути то же самое, что и ^(NSObject *obj) { NSLog (@»%@ %@», [self description], obj); return 0; }; Итого вместо одного блока создается три. Поскольку самый внешний блок вызывается сразу после создания, от него можно избавиться воспользовавшись code block evaluation aka statement expressions extension: self.block = ({ __weak typeof (self) weakSelf = self; ^(NSObject *obj) { __strong typeof (self)self = weakSelf; return ^ (void) { NSLog (@»%@, %@», [self description], obj); return 0; }(); }; }); Осталось завернуть весь boilerplate в макрос, чтобы этим трюком было удобно пользоваться. Если оставить только общий код, то получится: ({ __weak typeof (self) weakSelf = self; /* ТИП БЛОКА */ { __strong typeof (self)self = weakSelf; return ^ (void) { /* ТЕЛО БЛОКА */ } (); }; }) Первой идеей было сделать макрос с двумя параметрами, для типа и тела, который бы вызывался так: self.block = weakself (^(NSObject *obj), { NSLog (@»%@ %@», [self description], obj); return 0; }); но, к сожалению, при препроцессинге макросы разворачиваются в одну строку, и, как следствие, нельзя поставить breakpoint на произвольную строчку в теле блока. Поэтому пришлось делать так: self.block = weakself (^(NSObject *obj)) { NSLog (@»%@ %@», [self description], obj); return 0; } weakselfend; такой вариант эквивалентен @weakify/@strongify из «Улучшение 4». Код макроса: #define weakself (ARGS) \ ({ __weak typeof (self) _private_weakSelf => self; \ ARGS { \ __strong typeof (_private_weakSelf) self __attribute__((unused)) = _private_weakSelf; \ return ^ (void) {

#define weakselfend } (); }; }) Одной из целей при создании макроса было обезопасить себя от неявного захвата self при доступе к ivar. К сожалению, как сделать это в compile time я так и не придумал. Единственный вариант — это assert/log для дебаг версии при создании блока (достаточно просто создать блок чтобы проверка сработала, не обязательно его вызывать). Тут стоит немного напомнить о том, как работает memory management для блоков и объектов, которые они захватывают. Существуют 3 типа блоков: NSGlobalBlock — блоки, созданные на верхнем уровне файла с исходным кодом, по сути аналогичны функциям с точки зрения memory management, переменные в scope не захватывают по этому интереса для нас не представляют. NSStackBlock — начальный тип для всех остальных созданных блоков, создаются на стеке, не увеличивают счетчики ссылок у объектов, которые захватывают, поскольку время жизни такого блока меньше либо равно времени жизни переменных из его лексического scope. NSMallocBlock — это NSStackBlock который был перенесен в heap явным вызовом copy/Block_copy или неявно компилятором. Один из случаев когда компилятор неявно вставляет Block_copy — это возврат блока как результат из функции/блока. В момент превращения NSStackBlock в NSMallocBlock и происходит увеличения счетчиков ссылок объектам, которые блок захватил в свой scope. Таким образом, для того, что бы проверить, захватывает ли блок сильную ссылку на self нужно сравнить счетчик ссылок на self, до того как блок был перенесен в heap, и после. Если счетчик увеличился, значит блок захватил self по сильной ссылке. Эта проверка не может быть надежной в 100% случаев, поскольку счетчик ссылок на self может изменяться из других потоков во время переноса блока в heap, однако в нормальной программе эта ситуация маловероятна, и для Debug-сборки вполне подходит.Для получения счетчика ссылок у объекта раньше можно было использовать метод retainCount, однако с ARC он больше не доступен, но CFGetRetainCount по-прежнему работает через toll-free bridging. Осталось только вставить вызовы этой функции с параметром self в нужные места и сравнить результаты.

self.block = {( __weak typeof (self) weakSelf = self; // Первый раз счетчик ссылок для self нужно получить здесь ^(NSObject *obj) { __strong typeof (self)self = weakSelf; return ^ (void) { NSLog (@»%@, %@», [self description], obj); return 0; }(); }; }) // второй раз здесь и сравнить. Но у нас нет доступа к переменным из statement expression Проблема в том, что результат statement expressions — это последняя строчка в нем. Поведение аналогично анонимному блоку, который вызывается сразу после объявления. Поскольку последняя строчка statement expression это декларация блока, то для того, чтобы этот блок оставался валидным, компилятор перенесет его в heap. Получается, мы можем сохранить вызов CFGetRetainCount для self в локальную переменную внутри statement expression, а второй вызов CFGetRetainCount нам нужно делать после последней строчки statement expression. Если бы речь шла про C++, мы бы могли создать объект на стеке, а в деструкторе объекта сделать все, что нам нужно, поскольку деструтор бы вызвался после выполнения последней строчки statement expression. К счастью, clang поддерживает gcc-extension который позволяет выставить cleanup-функцию (аналог деструктора) для любой переменной на стеке, которая будет вызвана в тот момент, когда переменая уйдет из области видимости. Через этот extension работает макрос @onExit из libextobjc.Для реализации проверки счетчика ссылок понадобится дополнительная структура:

struct RefCountCheckerData { CFTypeRef weakSelf; NSUInteger refCountBefore; }; И функция, которая будет выставлена как cleanup. static inline void vbr_CheckRefCountForWeakSelf (struct RefCountCheckerData *data) { const NSInteger refCountAfter = CFGetRetainCount (data→weakSelf); const NSInteger countOfSelfRefInBlock = refCountAfter — data→refCountBefore; if (countOfSelfRefInBlock > 0) { raise (SIGPIPE); } } Создаем структуру на стеке, выставляем cleanup функцию и инициализируем указатель на weakSelf и число ссылок на него. Cleanup функция вызовется когда переменная _private_refCountCheckerData уйдет из области видимости, а в этот момент наш блок уже в heap. self.block = {( __weak typeof (self) weakSelf = self; __attribute__((cleanup (vbr_CheckRefCountForWeakSelf), unused)) struct RefCountCheckerData _private_refCountCheckerData = { .weakSelf = (__bridge CFTypeRef)self, .refCountBefore = CFGetRetainCount ((__bridge CFTypeRef)self), }; ^(NSObject *obj) { __strong typeof (self)self = weakSelf; return ^ (void) { NSLog (@»%@, %@», [self description], obj); return 0; }(); }; }); С такой версией макроса сработает breakpoint в отладчике при попытке получить доступ к ivar не через self, например таком self.block = ^{ NSLog (@»%d», _ivarInteger); }; Перед тем как, представить финальный вариант макроса, нужно привести его в современный хипстерский вид. Для ObjC модно делать макросы начинающиеся, как и ключевые слова языка, с @, например: @strongify, @onExit. Но препроцессор не разрешает использовать @ как часть имени макроса. В extobjc для этого используют вставку в начало макроса autoreleasepool {} либо try {} catch (…) {}, символ @ таким образом приклеивается либо к try либо к autoreleasepool. После разворачивания макроса в коде появляется ненужный пустой autoreleasepool либо try catch, но это никого сильно не волнует. Однако такой подход не работает для макроса weakself, потому что результат weakself это выражение, а выражение не может содержать @autoreleasepool try {} catch (…) {} в начале.

self.block = @weakself (^(NSObject *obj)) { NSLog (@»%@ %@», [self description], obj); return 0; } @weakselfend; Когда речь идет о сложных выражениях в С на ум первым делом приходит тернарный оператор. Осталось понять, как его применить. Первым в голову пришло записать как-то так: self.block = @1? /* здесь код блоков */: nil; Для этого нужно всего лишь добавить 1? в начало weakself и : nil; в конец weakselfend. Но self.block = 1? /* здесь код блоков */: nil; вполне корректное выражение, поэтому @weakself и weakself будут работать.

Вариант self.block = @[]? /* здесь код блоков */: nil; не дает использовать @weakself без @, однако после проверки дизассемблера выяснилось, что оптимизатор не выбрасывает создание пустого массива, а это лишний overhead в runtime.

Наконец в голову пришла идея использовать особенности String Literal Concatenation в ObjC.

const char *s0 = «ABC» «DEF»; // это валидная C-строка «ABCDEF» NSString *s1 = @«ABC» @«DEF»; // это валидная ObjC-строка @«ABCDEF» NSString *s2 = @«ABC» «DEF»; // это тоже валидная ObjC-строка @«ABCDEF» NSString *s3 = «ABC» @«DEF»; //, а это ошибка компиляции Итак, финальный вариант макроса: #define weakself (ARGS) \ «weakself should be called as @weakself» @» ? \ ({ __weak typeof (self) _private_weakSelf = self; \ ARGS { \ __strong typeof (_private_weakSelf) self __attribute__((unused)) = _private_weakSelf; \ return ^ (void) {

#define weakselfnotnil (ARGS) \ «weakself should be called as @weakself» @» ? \ ({ __weak typeof (self) _private_weakSelf = self; \ ARGS { \ __strong typeof (_private_weakSelf) self __attribute__((unused)) = _private_weakSelf; \ return ^ (void) { if (self)

#define weakselfend \ try {} @finally {} } (); }; \ }) : nil @weakselfnotnil отличается тем, что если к моменту вызова блока self уже удален, то блок не вызовется. Подходит только для случаев, когда блок не имеет возвращаемого значения, иначе не понятно, что возвращать в случае если self уже удален. Сделан в основном для безопасного использования ivar через явное разыменованиее self: self.block = @weakselfnotnil (^) { NSLog (@»%d», self→_ivar); } @weakselfend; Производительность Сильно беспокоиться из-за производительности тут, пожалуй, не стоит, накладных расходов должно быть не много. Трюк для добавления @ в начало макроса полностью выбрасывается оптимизатором. С накладными расходами на вызов дополнительного блока дела обстоят интереснее. Для проверки как обстоят дела с накладными расходами рассмотрим 2 случая, с использованием макросов из libextobjc и нашего weakself:  — (void)m1 { @weakify (self); self.block = ^(NSObject * obj) { @strongify (self); NSLog (@»%@», [self description]); return 0; }; }

— (void)m2 { self.block = @weakself (^(NSObject * obj)) { NSLog (@»%@», [self description]); return 0; } @weakselfend; } Собираем с -O3, открываем в Hooper и смотрим псевдокод для обоих случаев function -[ViewController m1] { asm{ vst1.64 {d8, d9, d10, d11}, [r4:128]! }; asm{ vst1.64 {d12, d13, d14, d15}, [r4:128] }; r1 = *_NSConcreteStackBlock; *((sp — 0×40 & !0xf) — 0×50) = r1; var_4 = 0xc2000000; var_24 = ((sp — 0×40 & !0xf) — 0×50) + 0×14; asm{ stm.w r5, {r1, r2, r3} }; r5 = [r0 retain]; objc_initWeak (var_24, r5); [r5 release]; r0 = *__objc_personality_v0; r1 = *0xac24; var_52 = r0; var_56 = GCC_except_table0; var_60 = &var_12; var_68 = (sp — 0×40 & !0xf) — 0×50; var_64 = (r1×0x1) + 0xabc4; var_32 = 0×1; [r5 setBlock1:(sp — 0×40 & !0xf) — 0×50]; objc_destroyWeak (var_24); r0 = _Unwind_SjLj_Unregister (&var_28); asm{ vld1.64 {d8, d9, d10, d11}, [r4:128]! }; asm{ vld1.64 {d12, d13, d14, d15}, [r4:128] }; Pop (); Pop (); Pop (); return r0; }

function ___20-[ViewController m1]_block_invoke { r4 = objc_loadWeakRetained (r0 + 0×14); r0 = [r4 description]; r5 = [r0 retain]; NSLog (@»%@», r5); [r5 release]; [r4 release]; return 0×0; }

function -[ViewController m2] { r4 = r0; r0 = *_NSConcreteStackBlock; *(sp — 0×18) = r0; var_4 = 0xc2000000; asm{ stm.w r3, {r0, r1, r2} }; objc_initWeak ((sp — 0×18) + 0×14, r4); r5 = objc_retainBlock (sp — 0×18); objc_destroyWeak ((sp — 0×18) + 0×14); [r4 setBlock1: r5]; r0 = [r5 release]; return r0; }

function ___20-[ViewController m2]_block_invoke { r4 = objc_loadWeakRetained (r0 + 0×14); r0 = [r4 description]; r5 = [r0 retain]; NSLog (@»%@», r5); [r5 release]; [r4 release]; return 0×0; } Получается, что weakself эффективнее чем @weakify/strongify, внутренний дополнительный блок полностью заинлайнился и _block_invoke в обоих случаях выглядит одинаково. Но способ которым в extobjc «съедают» @ в начале макроса добавляет бесполезный код по обработке исключений в рантайме, что видно по _Unwind_SjLj_Unregister.В случае компиляции с -Os все не так хорошо, блок не инлайнится и вместо одного _block_invoke генерируется два function ___20-[ViewController m2]_block_invoke { r0 = objc_loadWeakRetained (r0 + 0×14); r1 = *_NSConcreteStackBlock; *(sp — 0×18) = r1; var_4 = 0xc2000000; asm{ stm.w r4, {r1, r2, r3} }; var_20 = r0; r4 = [r0 retain]; r5 = ___20-[ViewController m2]_block_invoke_2(sp — 0×18); [var_20 release]; [r4 release]; r0 = r5; return r0; }

function ___20-[ViewController m2]_block_invoke_2 { r0 = *(r0 + 0×14); r0 = [r0 description]; r4 = [r0 retain]; NSLog (@»%@», r4); [r4 release]; return 0×0; } К сожалению, clang пока не позволяет добавить атрибут always_inline к блоку.Полный исходный код и autocomplete для Xcode тут.

© Habrahabr.ru