Understanding Memory Leaks with RACObserve and RACSubject in ReactiveCocoa
In ReactiveCocoa, using RACObserve inside a flattenMap or applying map to a hot RACSubject can create retain cycles because the generated blocks capture self and the subject retains its subscribers, so employing @weakify/@strongify and ensuring signals send a completed or error event breaks these hidden memory leaks.
ReactiveCocoa is a popular functional‑reactive programming framework used extensively in Meituan's iOS app. While its API is convenient, hidden details can cause retain cycles and memory leaks, especially when combined with MVVM.
RACObserve and retain cycles
The macro
#define RACObserve(TARGET, KEYPATH) \ ({ \ _Pragma("clang diagnostic push") \ _Pragma("clang diagnostic ignored \"-Wreceiver-is-weak\"") \ __weak id target_ = (TARGET); \ [target_ rac_valuesForKeyPath:@keypath(TARGET, KEYPATH) observer:self]; \ _Pragma("clang diagnostic pop") \ })captures self inside the generated block. When the macro is used inside a flattenMap block, the block retains self, and the resulting signal is stored in a property of self, forming a strong reference cycle.
Example (simplified):
- (void)viewDidLoad {
[super viewDidLoad];
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
MTModel *model = [[MTModel alloc] init];
[subscriber sendNext:model];
[subscriber sendCompleted];
return nil;
}];
self.flattenMapSignal = [signal flattenMap:^RACStream *(MTModel *model) {
return RACObserve(model, title);
}];
[self.flattenMapSignal subscribeNext:^(id x) {
NSLog(@"subscribeNext - %@", x);
}];
}When the view controller is popped, the signal still holds a reference to self, preventing deallocation.
Fix with @weakify/@strongify
Wrap the block with @weakify(self) and re‑acquire it with @strongify(self) before using RACObserve:
- (void)viewDidLoad {
[super viewDidLoad];
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
MTModel *model = [[MTModel alloc] init];
[subscriber sendNext:model];
[subscriber sendCompleted];
return nil;
}];
@weakify(self);
self.signal = [signal flattenMap:^RACStream *(MTModel *model) {
@strongify(self);
return RACObserve(model, title);
}];
[self.signal subscribeNext:^(id x) {
NSLog(@"subscribeNext - %@", x);
}];
}RACSubject map and hidden leaks
Using RACSubject as a hot signal and applying map without sending a completion event also creates a retain cycle. The subject retains its subscribers; the bind implementation (used by map and flattenMap) stores the source signal in an internal signals array, which in turn retains the subject. Because the subject also retains the subscriber, a circular reference is formed.
When the subject finally receives sendCompleted, the internal completeSignal block removes the subject from the signals array and disposes the disposables, breaking the cycle.
Key code excerpts:
- (instancetype)map:(id (^)(id value))block {
NSCParameterAssert(block != nil);
Class class = self.class;
return [[self flattenMap:^(id value) {
return [class return:block(value)];
}] setNameWithFormat:@"[%@] -map:", self.name];
}
- (instancetype)flattenMap:(RACStream * (^)(id value))block {
Class class = self.class;
return [[self bind:^{
return ^(id value, BOOL *stop) {
id stream = block(value) ?: [class empty];
return stream;
};
}] setNameWithFormat:@"[%@] -flattenMap:", self.name];
}
- (RACSignal *)bind:(RACStreamBindBlock (^)(void))block {
// ... creates NSMutableArray *signals = [NSMutableArray arrayWithObject:self];
// ... subscribes to self and adds each inner signal to the array
}Because RACSignal (cold signal) does not retain its subscribers, the same map operation on a plain RACSignal does not leak.
Best practices
Always use @weakify/@strongify (or other weak‑reference patterns) when a block captures self and the block is retained by a signal.
When working with hot signals such as RACSubject, ensure that the signal sends a completed or error event after its work is done.
Apply the same rule to other operators that internally use bind (e.g., filter, merge, combineLatest, flattenMap).
Following these guidelines prevents hidden memory leaks in ReactiveCocoa‑based iOS projects.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Meituan Technology Team
Over 10,000 engineers powering China’s leading lifestyle services e‑commerce platform. Supporting hundreds of millions of consumers, millions of merchants across 2,000+ industries. This is the public channel for the tech teams behind Meituan, Dianping, Meituan Waimai, Meituan Select, and related services.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
