Какова семантика ссылочного владения ReactiveCocoa?
Когда я создаю сигнал и переношу его в область действия функции, его эффективное количество сохранений будет равно 0 для соглашений Cocoa:
RACSignal *signal = [self createSignal];
Когда я подписываюсь на сигнал, он сохраняет подписчика и возвращает одноразовое устройство, которое, согласно соглашениям Какао, также имеет счет сохранения нуля.
RACDisposable *disposable = [signal subscribeCompleted:^ {
doSomethingPossiblyInvolving(self);
}];
В большинстве случаев подписчик закрывается и ссылается self
или его ивары, или какая-то другая часть ограждающей области. Таким образом, когда вы подписываетесь на сигнал, у него есть ссылка на владельца, а у подписчика есть ссылка на вас. А одноразовый, который вы получаете взамен, имеет ссылку на сигнал.
disposable -> signal -> subscriber -> calling scope
Предположим, что вы удерживаете одноразовое использование, чтобы в какой-то момент вы могли отменить свою подписку (например, если сигнал извлекает данные из веб-службы и пользователь уходит с экрана, отменяя свое намерение просмотреть извлекаемые данные).
self.disposeToCancelWebRequest = disposable;
На данный момент у нас есть круговая ссылка:
calling scope -> disposable -> signal -> subscriber -> calling scope
Ответственное, что нужно сделать, это убедиться, что цикл прерывается при отмене запроса или после его завершения.
[self.disposeToCancelWebRequest dispose]
self.disposeToCancelWebRequest = nil;
Обратите внимание, что вы не можете сделать это, когда self
освобождается, потому что это никогда не произойдет из-за цикла сохранения! Кое-что также кажется сомнительным в нарушении цикла сохранения во время обратного вызова подписчику, поскольку сигнал может быть потенциально освобожден, пока его реализация все еще находится в стеке вызовов.
Я также заметил, что реализация сохраняет глобальный список активных сигналов (на момент, когда я первоначально задавал этот вопрос).
Как я должен думать о владении при использовании RAC?
2 ответа
Честно говоря, управление памятью в ReactiveCocoa довольно сложное, но достойный конечный результат заключается в том, что вам не нужно сохранять сигналы для их обработки.
Если бы структура требовала, чтобы вы сохраняли каждый сигнал, его было бы гораздо более громоздким, особенно для однократных сигналов, которые используются как фьючерсы (например, сетевые запросы). Вам нужно будет сохранить любой долгоживущий сигнал в свойстве, а затем обязательно очистить его, когда вы закончите с ним. Не смешно.
Подписчики
Прежде чем идти дальше, я должен отметить, что subscribeNext:error:completed:
(и все его варианты) создают неявного подписчика, используя заданные блоки. Поэтому любые объекты, на которые ссылаются эти блоки, будут сохранены как часть подписки. Как и любой другой объект, self
не будет сохранено без прямой или косвенной ссылки на него.
(Исходя из формулировки вашего вопроса, я думаю, вы уже знали об этом, но это может быть полезно для других.)
Конечные или недолговечные сигналы
Наиболее важным принципом управления памятью RAC является то, что подписка автоматически завершается после завершения или ошибки, и подписчик удаляется. Чтобы использовать пример круговой ссылки:
calling scope -> disposable -> signal -> subscriber -> calling scope
... это означает, что signal -> subscriber
отношения разрушаются, как только signal
заканчивается, нарушая цикл сохранения.
Это часто все, что вам нужно, потому что жизнь RACSignal
в памяти будет естественно соответствовать логическому времени жизни потока событий.
Бесконечные Сигналы
Однако бесконечные сигналы (или сигналы, которые живут так долго, что могут быть бесконечными) никогда не будут разрушаться естественным путем. Это где одноразовые изделия сияют.
Удаление подписки приведет к удалению связанного подписчика и, как правило, просто очистит все ресурсы, связанные с этой подпиской. Для этого единственного абонента, это как если бы сигнал был завершен или допущен с ошибкой, за исключением того, что окончательное событие не передается по сигналу. Все остальные подписчики останутся нетронутыми.
Однако, как правило, если вам нужно вручную управлять жизненным циклом подписки, возможно, есть лучший способ сделать то, что вы хотите. Методы как -take:
или же -takeUntil:
будет заниматься удалением для вас, и вы получите абстракцию более высокого уровня.
Сигналы, полученные от self
Здесь все еще есть немного сложный средний случай. Каждый раз, когда время жизни сигнала привязано к объему вызова, вам будет гораздо сложнее разорвать цикл.
Это обычно происходит при использовании RACAble()
или же RACAbleWithStart()
на ключевом пути, который относительно self
, а затем применяя блок, который должен захватывать self
,
Самый простой ответ здесь просто захватить self
слабо
__weak id weakSelf = self;
[RACAble(self.username) subscribeNext:^(NSString *username) {
id strongSelf = weakSelf;
[strongSelf validateUsername];
}];
Или после импорта включенного заголовка EXTScope.h:
@weakify(self);
[RACAble(self.username) subscribeNext:^(NSString *username) {
@strongify(self);
[self validateUsername];
}];
(Заменить __weak
или же @weakify
с __unsafe_unretained
или же @unsafeify
соответственно, если объект не поддерживает слабые ссылки.)
Однако, вероятно, есть лучший шаблон, который вы могли бы использовать вместо этого. Например, приведенный выше пример может быть записан так:
[self rac_liftSelector:@selector(validateUsername:)
withObjects:RACAble(self.username)];
или же:
RACSignal *validated = [RACAble(self.username) map:^(NSString *username) {
// Put validation logic here.
return @YES;
}];
Как и в случае с бесконечными сигналами, обычно вы можете избежать ссылок self
(или любой объект) из блоков в цепочке сигналов.
Приведенная выше информация - это действительно все, что вам нужно для эффективного использования ReactiveCocoa. Тем не менее, я хочу затронуть еще один момент, просто для технически любопытных или для тех, кто заинтересован в содействии RAC:
Я также заметил, что реализация сохраняет глобальный список активных сигналов.
Это абсолютно верно.
Задача разработки "не сохранять необходимое" напрашивается на вопрос: как мы узнаем, когда сигнал должен быть освобожден? Что, если он только что был создан, вышел из пула автоматического выпуска и еще не был сохранен?
Реальный ответ - нет, но обычно мы можем предположить, что вызывающая сторона сохранит сигнал в текущей итерации цикла выполнения, если они захотят его сохранить.
Как следствие:
- Созданный сигнал автоматически добавляется в глобальный набор активных сигналов.
- Сигнал будет ожидать одного прохода основного цикла выполнения, а затем удалит себя из активного набора, если у него нет подписчиков. Если сигнал не был сохранен каким-либо образом, он будет освобожден в этой точке.
- Если что-то подписалось в этой итерации цикла выполнения, сигнал остается в наборе.
- Позже, когда все подписчики ушли, #2 снова срабатывает.
Это может иметь неприятные последствия, если цикл выполнения запускается рекурсивно (как в модальном цикле событий в OS X), но это значительно облегчает жизнь потребителю платформы в большинстве или во всех других случаях.
Я пытаюсь разгадать тайну управления памятью в ReactiveCocoa 2.5
RACSubject* subject = [RACSubject subject];
RACSignal* signal = [RACSignal return:@(1)];
NSLog(@"Retain count of RACSubject %ld", CFGetRetainCount((__bridge CFTypeRef)subject));
NSLog(@"Retain count of RACSignal %ld", CFGetRetainCount((__bridge CFTypeRef)signal));
Первая строка вывода 1
и вторая строка вывода 2
, Кажется, что RACSignal
будет сохранено где-то, а RACSubject
не является. Если вы не сохраните явно RACSubject
, он будет освобожден, когда программа выйдет из текущей области.