Mobile Development 15 min read

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.

Meituan Technology Team
Meituan Technology Team
Meituan Technology Team
Understanding Memory Leaks with RACObserve and RACSubject in ReactiveCocoa

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

iOSMemoryLeakObjective‑CReactiveProgrammingReactiveCocoa
Meituan Technology Team
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.