Циклические ссылки в блоках (Objective-C)

Заключающая важная тема про блоки, в которой мы рассмотрим возможные ситуации, когда при создании блока, мы можем негласно создать циклическую ссылку на себя (self).

Из жизни. В процессе написания кода, меня иногда умиляет тот факт, что разработчики не знающие каким образом нужно предотвращать создание циклических ссылок, пренебрегает к простому правилу, делаем ВЕЗДЕ указатель на weak self, и бывает еще в придачу, создание внутри блока ВЕЗДЕ strong указателя на self. И если спросить, зачем? В ответ мы услышим: Так везде делают 🙂

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

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

Возможные случаи и их решения.


    self.myBlock = ^{ 
        [self doSomething];  // <--- strong reference cycle
    };

Без зануления self.myBlock объект никогда не сможет быть удален, поскольку блок ссылается на self. При включенном CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF компилятор выдаст предупреждение.


    __weak __typeof(self)weakSelf = self;
    self.myBlock = ^{ 
        [weakSelf doSomething];
    };

Теперь рассмотрим другой случай. Когда нам нужно вызвать два метода в блоке.


    __weak __typeof(self)weakSelf = self;
    self.myBlock = ^{ 
        [weakSelf doSomething];
        [weakSelf doSomethingElse];
    };

У нас возникает другая проблема, на момент вызова блока объект weakSelf либо существует, либо уже нет. Если объект уже не существует, weakSelf == nil, doSomething и doSomethingElse не вызовутся — казалось бы, все в порядке. Однако, может получиться так, что на момент вызова doSomething объект еще существует, а на момент вызова doSomethingElse уже нет. При этом doSomething вызовется, а doSomethingElse нет — такое поведение может быть неожиданным и неправильным. Это может произойти как при race condition в многопоточном приложении, либо если doSomething уменьшает количество ссылок на объект (например, удаляет объект из какой-нибудь коллекции). В случае включенных CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK и CLANG_WARN_OBJC_RECEIVER_WEAK компилятор выдает предупреждение для этого случая. Проблема с консистентностью вызовов методов внутри блока решается вот таким способом:


    __weak typeof(self)weakSelf = self;
    self.myBlock = ^{
         __strong typeof(self)strongSelf = weakSelf;
        [strongSelf doSomething];
        [strongSelf doSomethingElse];
    };

Немного отсебятины. При разработке мобильных приложений средней сложности, вероятность того, что ссылка на self будет уже nil при вызове второго метода, мало вероятна. Конечно это всё зависит от поставленных задач, но на моей практике таких случаев было очень мало. Обычно такие случаи где нам нужно вызывать два и больше методов в блоке бывают во вьюконтроллерах (UIViewController) или же когда мы выносим бизнес-логику в менеджере например или в модели для работы с бизнес-логикой, но в таких случаях ссылка всегда остается жить пока жизненный цикл контролера еще не завершен. Другими словами, создавать из weak self - strong self каждый раз не имеет никакого смысла, если мы точно уверены в том что по завершении работы блока self еще будет доступен. Этим мы можем подчеркивать тот момент, что мы понимаем, что мы делаем в данном случае, или при указании __strong мы тоже говорим, что именно вот в этом месте, такое может произойти. Таким образом, другому разработчику будет более понятней понять, как работает ваш код. Конечно если речь идет о ReactiveCocoa например, где колитов блоков на строчку кода просто зашкаливает, вероятность использования увеличивается в разы. 🙂

Но в данном решении наши проблемы, существует не большой подводный камень, про который я обязан упомянуть.


    __weak typeof(self)weakSelf = self;
    self.myBlock = ^{
        __strong typeof(self)strongSelf = weakSelf;
        [strongSelf doSomething];
        [strongSelf doSomethingElse];
        NSAssert(foo == bar, @"Assert!")
    };

Макросы, такие как NSAssert неявно используют self, и проблема с циклической ссылкой возвращается. Проблема решается переименованием strongSelf в self.


    __weak typeof(self)weakSelf = self;
    self.myBlock = ^{
        __strong typeof(self)self = weakSelf; // <-- strongSelf to self
        [self doSomething];
        [self doSomethingElse];
        NSAssert(foo == bar, @"Assert!")
    };

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


    @weakify(self); // self теперь новая локальная переменная с __weak
    self.myBlock = ^{
        @strongify(self); // self теперь новая локальная переменная с __strong
        [self doSomething];
        [self doSomethingElse];
        NSAssert(foo == bar, @"Assert!")
    };

Это почти оптимальное решение, но оно все еще не лишено нескольких недостатков: нужно не забыть поставить в нужные места @weakify и @strongify; использование self после @weakify безопасно, но компилятор может выдавать предупреждение. При этом все еще остается вероятность случайно захватить в блоке self по сильной ссылке:


    @weakify(self); // self теперь новая локальная переменная с  __weak
    self.myBlock = ^{
        @strongify(self); // self теперь новая локальная переменная с __strong
        [self doSomething];
        NSLog(@"Ivar value form object: %@", _ivar); // Сильная ссылка на self сохраняется неявно для доступа к  _ivar
        NSAssert(foo == bar, @"Assert!")
    };

Для того, что бы этого избежать нужно либо использовать только доступ через property (self.ivar), либо явно использовать переопределенный self:


    @weakify(self); // self теперь новая локальная переменная с  __weak
    self.myBlock = ^{
        @strongify(self); // self теперь новая локальная переменная с __strong
        [self doSomething];
        NSLog(@"Ivar value form object: %@", self->_ivar); // Явно используем свой переопределенный self для доступа к _ivar
        NSAssert(foo == bar, @"Assert!")
    };

Для того, что бы этого избежать нужно либо использовать только доступ через property (self.ivar), либо явно использовать переопределенный self:


    @weakify(self); // self теперь новая локальная переменная с  __weak
    self.myBlock = ^{
        @strongify(self); // self теперь новая локальная переменная с __strong
        [self doSomething];
        NSLog(@"Ivar value form object: %@", self->_ivar); // Явно используем свой переопределенный self для доступа к _ivar
        NSAssert(foo == bar, @"Assert!")
    };

При этом нужно помнить, что self может быть nil, и явное разыменование self->_ivar вызовет креш.

Думаю данного материала достаточно, чтобы не создавать циклических ссылок в коде.

И еще 🙂 этих две записи идентичные, так как значением по умолчанию является __strong.


    __strong typeof(self)strongSelf = weakSelf;
    typeof(self)strongSelf = weakSelf;

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

Поделиться

Оставить комментарий