惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

U
Unit 42
C
Cybersecurity and Infrastructure Security Agency CISA
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Know Your Adversary
Know Your Adversary
S
Securelist
I
Intezer
AWS News Blog
AWS News Blog
L
LINUX DO - 热门话题
P
Privacy International News Feed
Recent Announcements
Recent Announcements
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
博客园 - 聂微东
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Attack and Defense Labs
Attack and Defense Labs
N
News and Events Feed by Topic
The GitHub Blog
The GitHub Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
Schneier on Security
Schneier on Security
N
Netflix TechBlog - Medium
爱范儿
爱范儿
B
Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
C
CERT Recently Published Vulnerability Notes
Hacker News: Ask HN
Hacker News: Ask HN
Google DeepMind News
Google DeepMind News
Engineering at Meta
Engineering at Meta
Blog — PlanetScale
Blog — PlanetScale
WordPress大学
WordPress大学
S
Secure Thoughts
K
Kaspersky official blog
N
News | PayPal Newsroom
O
OpenAI News
Last Week in AI
Last Week in AI
C
Check Point Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Cyberwarzone
Cyberwarzone
Application and Cybersecurity Blog
Application and Cybersecurity Blog
T
Tor Project blog
大猫的无限游戏
大猫的无限游戏
Vercel News
Vercel News
D
Docker
Hugging Face - Blog
Hugging Face - Blog
T
Threat Research - Cisco Blogs
Cisco Talos Blog
Cisco Talos Blog
The Register - Security
The Register - Security
博客园 - 司徒正美
Martin Fowler
Martin Fowler
人人都是产品经理
人人都是产品经理
P
Palo Alto Networks Blog

美团技术团队

美团 · 技术团队 美团 · 技术团队 美团 · 技术团队 美团 · 技术团队 美团 · 技术团队 美团 · 技术团队 美团发布基于 N-gram 全新模型:嵌入扩展新范式,实现轻量化 MoE 高效进化 2025美团技术年货,「马」上到来 多维创新打造强泛化智能体模型,LongCat-Flash-Thinking-2601技术报告发布 美团 EvoCUA 刷新开源 SOTA,会用电脑还会持续进化的智能体! 美团 LongCat-Flash-Thinking-2601 发布,工具调用能力登顶开源 SOTA! KuiTest:基于大模型通识的 UI 交互遍历测试 AAAI 2026 | 美团技术团队学术论文精选 2025 | 美团技术团队热门技术文章汇总 美团 LongCat-Video-Avatar 正式发布,实现开源 SOTA 级拟真表现 大模型剪枝新范式:先浓缩,再剪枝——DenoiseRotator技术解读 LongCat 上线 AI 生图!精准高效,AI 创作不设限 美团发布 LongCat-Image 图像生成模型,编辑能力登顶开源 SOTA AI Coding与单元测试的协同进化:从验证到驱动 R-HORIZON:探索长程推理边界,复旦NLP&美团LongCat联合提出LRMs能力评测新框架 美团 LongCat 发布 AMO-Bench:突破 AIME 评测饱和困境,重新定义 LLM 数学上限 美团 LongCat Interaction 团队发布大模型交互系统技术报告 WOWService 美团 LongCat 团队发布全模态一站式评测基准 UNO-Bench 美团开源LongCat-Audio-Codec,高效语音编解码器助力实时交互落地 NeurIPS 2025 | 美团技术团队论文精选 LongCat-Flash-Omni正式发布并开源:开启全模态实时交互时代 美团 LongCat 团队发布 VitaBench:基于复杂生活场景的交互式 Agent 评测基准 LongCat-Video 视频生成模型正式发布,探索世界模型的第一步 ICCV 2025 | 美团论文精选及多模态推理竞赛冠军方法分享 从0到1建设美团数据库容量评估系统 可验证过程奖励在提升大模型推理效率中的探索与实践 LongCat-Flash-Thinking 正式发布,更强、更专业,保持极速! 开源 | InfiniteTalk:无限长虚拟人视频生成的新范式 美团正式发布并开源 LongCat-Flash-Chat,动态计算开启高效 AI 时代 美团 M17 团队开源 Meeseeks 评测集:揭秘大模型的“听话”能力 可信实验白皮书系列08:开放式分析引擎 | 附PDF合集 美团智能头盔研发实践系列02:软件功能篇 美团智能头盔研发实践系列01:硬件设计篇 ACL 2025 | 美团技术团队论文精选 美团开源OIBench与CoreCodeBench:揭示大模型编程能力的真实水平 可信实验白皮书系列07:高阶实验工具 开源 | MeiGen-MultiTalk:基于单张照片实现多人互动演绎 可信实验白皮书系列06:观察性研究 JDK高版本特性总结与ZGC实践 可信实验白皮书系列05:准实验 可信实验白皮书系列04:随机轮转实验 可信实验白皮书系列03:随机对照实验 可信实验白皮书系列02:AB实验基础 可信实验白皮书系列01:从0到1的方法论与实践指南 MTGR:美团外卖生成式推荐Scaling Law落地实践 OR算法+ML模型混合推理框架架构演进 ICLR&CVPR 2025美团技术团队论文精选 行为正则化与顺序策略优化结合的离线多智能体学习算法 预测技术在美团弹性伸缩场景的探索与应用 美团技术年货 | 600+页电子书,算法、工程、测试、数据、安全系列大合集 鸿蒙应用签名实操及机制探究 2024 | 美团技术团队热门技术文章汇总 AutoConsis:UI内容一致性智能检测 CIKM 2024 | 美团技术团队精选论文解读 大前端:如何突破动态化容器的天花板? KDD 2024 OAG-Challenge Cup赛道三项冠军技术方案解读 新一代实验分析引擎:驱动履约平台的数据决策 大众点评技术部包揽KDD 2024 OAG-Challenge Cup赛道全部3项冠军 ACL 2024 | 美团技术团队精选论文解读 KDD 2024 | 美团技术团队精选论文解读 基本功 | 一文讲清多线程和多线程同步 小程序可测性能力建设与实践 SIGIR 2024 | 美团技术团队精选论文解读 Spark向量化计算在美团生产环境的实践 CVPR 2024 | 美团技术团队精选论文解读 基于多模态信息抽取的菜品知识图谱构建 DDD在大众点评交易系统演进中的应用 美团外卖基于GPU的向量检索系统实践 美团大规模KV存储挑战与架构实践 基于接口数据变异的App健壮性测试实践 美团技术年货 | 600+页电子书,前端、后端、算法、测试、运维系列大合集 美团RASP大规模研发部署实践总结 2023 | 美团技术团队热门技术文章汇总 美团到店终端从标准化到数字化的演进之路 AIOps在美团的探索与实践——事件管理篇 美团技术博客十周年,感谢一路相伴 基于UI交互意图理解的异常检测方法 如何利用「深度上下文兴趣网络」提升点击率? 基于模式挖掘的可靠性治理探索 代码变更风险可视化系统建设与实践 美团多场景建模的探索与实践 MJDK 如何实现压缩速率的 5 倍提升? 如何提供一个可信的AB测试解决方案 KDD 2023 | 美团技术团队精选论文解读 美团前端研发框架Rome实践和演进趋势 斩获CVPR 2023竞赛2项冠军|美团街景理解中视觉分割技术的探索与应用 MySQL自治平台建设的内核原理及实践(下) CVPR 2023 | 美团技术团队精选论文解读 超大规模数据库集群保稳系列之三:美团数据库容灾体系建设实践 超大规模数据库集群保稳系列之二:数据库攻防演练建设实践 Robust 2.0:支持Android R8的升级版热修复框架 超大规模数据库集群保稳系列之一:高可用系统 一次「找回」TraceId的问题分析与过程思考 基于AI+数据驱动的慢查询索引推荐 SOTA!目标检测开源框架YOLOv6 3.0版本来啦
ReactiveCocoa中潜在的内存泄漏及解决方案
高君 · 2016-08-19 · via 美团技术团队

ReactiveCocoaGitHub开源的一个函数响应式编程框架,目前在美团App中大量使用。用过它的人都知道很好用,也确实为我们的生活带来了很多便利,特别是跟MVVM模式结合使用,更是如鱼得水。不过刚开始使用的时候,可能容易疏忽掉一些隐藏的细节,从而导致内存泄漏等问题。本文就带大家深入了解下ReactiveCocoa中隐藏的一些细节,帮助大家以更加正确的姿势使用ReactiveCocoa。

以下代码和示例基于ReactiveCocoa v2.5

RACObserve引发的血案

RACObserve是ReactiveCocoa中一个相当常用也相当好用的宏,它可以用来监听属性值的改变,然后传递给订阅者。不过在使用的时候有一点需要稍微注意一下,为了直观说明,先上一个小Demo。

- (void)viewDidLoad
{
    [super viewDidLoad];
    RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { //1
        MTModel *model = [[MTModel alloc] init]; // MTModel有一个名为的title的属性
        [subscriber sendNext:model];
        [subscriber sendCompleted];
        return nil;
    }];
    self.flattenMapSignal = [signal flattenMap:^RACStream *(MTModel *model) { //2
        return RACObserve(model, title);
    }];
    [self.flattenMapSignal subscribeNext:^(id x) { //3
        NSLog(@"subscribeNext - %@", x);
    }];
}
  1. 创建一个signal,该signal被订阅后会发送一个MTModel的实例;
  2. 对第一步创建的signal进行flattenMap操作,并将返回的信号保留(之所以要保留,是因为可能希望在其它地方订阅,不过这里为了简单,就直接在第三步进行订阅);
  3. 对第二步产生的信号(self.flattenMapSignal)进行订阅。

这段代码看起来很正常,工作也相当良好,但是当从添加了这段代码的控制器返回时,控制器并没有被释放。这又是为啥呢?看下RACObserve的定义:

#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") \
	})

注意这一句:[target_ rac_valuesForKeyPath:@keypath(TARGET, KEYPATH) observer:self]; 如果将宏简单展开就变成了下面这样:

- (void)viewDidLoad
{
    [super viewDidLoad];
    RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id < RACSubscriber > subscriber) { //1
        GJModel *model = [[GJModel alloc] init];
        [subscriber sendNext:model];
        [subscriber sendCompleted];
        return nil;
    }]; 
    self.flattenMapSignal = [signal flattenMap:^RACStream *(GJModel *model) {//2
        __weak GJModel *target_ = model;
        return [target_ rac_valuesForKeyPath:@keypath(target_, title) observer:self];
    }];
    [self.flattenMapSignal subscribeNext:^(id x) {//3
        NSLog(@"subscribeNext - %@", x);
    }];
}

看到这里,应该发现哪里不对了吧?没错,flattenMap操作接收的block里面出现了self,对self进行了持有,而flattenMap操作返回的信号又由self的属性flattenMapSignal进行了持有,这就造成了循环引用。

Alt text

注意:2是间接持有,从逻辑上来讲,flattenMapSignal会有一个didSubscribeBlock,为了让传递给flattenMap操作的block有意义,didSubscribeBlock会对该block进行持有,从而也就间接持有了self,感兴趣的读者可以去看下相关源码。

OK,找到了问题所在,解决起来也就简单了,使用@weakify和@strongify即可:

- (void)viewDidLoad
{
    [super viewDidLoad];
    RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id < RACSubscriber > subscriber) {
        GJModel *model = [[GJModel alloc] init];
        [subscriber sendNext:model];
        [subscriber sendCompleted];
        return nil;
    }];
    @weakify(self); //
    self.signal = [signal flattenMap:^RACStream *(GJModel *model) {
        @strongify(self); //
        return RACObserve(model, title);
    }];
    [self.signal subscribeNext:^(id x) {
        NSLog(@"subscribeNext - %@", x);
    }];
}

这里之所以容易疏忽,是因为在block里没有很直观的看到self,但是RACObserve的定义里面却用到了self。 其实RACObserve的解释中已经很明确地说明了这个问题。

/// Creates a signal which observes `KEYPATH` on `TARGET` for changes.
///
/// In either case, the observation continues until `TARGET` _or self_ is
/// deallocated. If any intermediate object is deallocated instead, it will be
/// assumed to have been set to nil.
///
/// Make sure to `@strongify(self)` when using this macro within a block! The
/// macro will _always_ reference `self`, which can silently introduce a retain
/// cycle within a block. As a result, you should make sure that `self` is a weak
/// reference (e.g., created by `@weakify` and `@strongify`) before the
/// expression that uses `RACObserve`.
///
/// Examples
///
///    // Observes self, and doesn't stop until self is deallocated.
///    RACSignal *selfSignal = RACObserve(self, arrayController.items);
///
///    // Observes the array controller, and stops when self _or_ the array
///    // controller is deallocated.
///    RACSignal *arrayControllerSignal = RACObserve(self.arrayController, items);
///
///    // Observes obj.arrayController, and stops when self _or_ the array
///    // controller is deallocated.
///    RACSignal *signal2 = RACObserve(obj.arrayController, items);
///
///    @weakify(self);
///    RACSignal *signal3 = [anotherSignal flattenMap:^(NSArrayController *arrayController) {
///        // Avoids a retain cycle because of RACObserve implicitly referencing
///        // self.
///        @strongify(self);
///        return RACObserve(arrayController, items);
///    }];
///
/// Returns a signal which sends the current value of the key path on
/// subscription, then sends the new value every time it changes, and sends
/// completed if self or observer is deallocated.
#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") \
	})

通过这个例子,相信你已经知道了RACObserve的正确使用姿势,也意识到了阅读文档的重要性。 如果说RACObserve潜在的内存泄漏只要稍加留意,使用的时候查看下文档就能避免;那么下面的情况,就相当隐蔽了,就算是看了文档也不一定能看出来。 不信?接着往下看。

RACSubject带来的悲剧

RACSubject是非RAC到RAC的一个桥梁,使用起来也很简单方便,基本的用法如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    RACSubject *subject = [RACSubject subject]; //1
	[subject.rac_willDeallocSignal subscribeCompleted:^{ //2
        NSLog(@"subject dealloc");
    }];
    [subject subscribeNext:^(id x) { //3
        NSLog(@"next = %@", x);
    }];
    [subject sendNext:@1]; //4
}
  1. 创建一个RACSubject的实例;
  2. 订阅subject的dealloc信号,在subject被释放的时候会发送完成信号;
  3. 订阅subject;
  4. 使用subject发送一个值。

接下来看一下输出的结果:

2016-06-13 09:15:25.426 RAC[5366:245360] next = 1
2016-06-13 09:15:25.428 RAC[5366:245360] subject dealloc

工作相当良好,接下来改造下程序,要求对subject发送的所有值进行乘3,这用map很容易就实现了。

- (void)viewDidLoad {
    [super viewDidLoad];
    RACSubject *subject = [RACSubject subject]; 
    [subject.rac_willDeallocSignal subscribeCompleted:^{ 
        NSLog(@"subject dealloc");
    }];
    
    [[subject map:^id(NSNumber *value) { 
        return @([value integerValue] * 3);
    }] subscribeNext:^(id x) { 
        NSLog(@"next = %@", x);
    }];
    [subject sendNext:@1]; 
}

跟之前大体不变,只是对subject进行了map操作然后再订阅,看下输出结果:

2016-06-13 09:21:42.450 RAC[5404:248584] next = 3

的确是进行了乘3操作,符合预期,但是这里有一个很严重的问题,subject dealloc没有输出,也就是说subject没有释放。 这不科学啊!subject看上去没有被任何对象持有。 那究竟是什么情况?下面我们将RACSubject换成RACSignal试试:

- (void)viewDidLoad {
    [super viewDidLoad];
    RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [subscriber sendNext:@1];
        return nil;
    }];
    [signal.rac_willDeallocSignal subscribeCompleted:^{
        NSLog(@"signal dealloc");
    }];
    [[signal map:^id(NSNumber *value) {
        return @([value integerValue] * 3);
    }] subscribeNext:^(id x) {
        NSLog(@"next = %@", x);
    }];
}

逻辑跟之前一样,看一下输出结果:

2016-06-12 23:32:31.669 RACDemo[5085:217082] next = 3
2016-06-12 23:32:31.674 RACDemo[5085:217082] signal dealloc

很明显,signal被释放了。同样的逻辑,signal能正常释放,subject却不能正常释放,太神奇了! 细心的读者看到这里,应该会发现一个问题:上面的几次试验,不管是RACSubject还是RACSignal都没有调用sendCompleted。 难道跟这个有关系?带着这个疑问,再进行如下试验,给RACSubject发送一个完成信号:

- (void)viewDidLoad {
    [super viewDidLoad];
    RACSubject *subject = [RACSubject subject]; 
    [subject.rac_willDeallocSignal subscribeCompleted:^{ 
        NSLog(@"subject dealloc");
    }];
    
    [[subject map:^id(NSNumber *value) { 
        return @([value integerValue] * 3);
    }] subscribeNext:^(id x) { 
        NSLog(@"next = %@", x);
    }];
    [subject sendNext:@1]; 
    [subject sendCompleted];
}

输出结果:

2016-06-12 23:40:19.148 RAC_bindSample[5168:221902] next = 3
2016-06-12 23:40:19.153 RAC_bindSample[5168:221902] subject dealloc

subject被释放了,确实修正了内存泄漏问题。到这里,我们可以得出结论: 使用RACSubject,如果进行了map操作,那么一定要发送完成信号,不然会内存泄漏。

虽然得出了结论,但是留下的疑问也是不少,如果你希望知道这其中的缘由,请继续往下看。 简单来说,留下的疑问有:

  1. 为什么对RACSubject的实例进行map操作之后会产生内存泄漏?
  2. 为什么RACSignal不管是否有map操作,都不会产生内存泄漏?
  3. 针对第一个问题,为什么发送完成可以修复内存泄漏?

带着疑问,咱们继续一探究竟。 讲道理,RACSignalRACSubject虽然都是信号,但是它们有一个本质的区别: RACSubject会持有订阅者(因为RACSubject是热信号,为了保证未来有事件发送的时候,订阅者可以收到信息,所以需要对订阅者保持状态,做法就是持有订阅者),而RACSignal不会持有订阅者。 关于这一点,更详细的说明请看《细说ReactiveCocoa的冷信号与热信号(三):怎么处理冷信号与热信号》。 那么持不持有订阅者,跟内存无法释放又有啥关系呢?不急,先记着有这样一个特性,咱们看看实现。

从上面提出第一个问题可以发现,关键点在于map操作,那么map操作究竟干了什么事情,看下map的实现:

- (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];
}

很简单,只是调用了一下flattenMap,再看下flattenMap怎么实现的:

 - (instancetype)flattenMap:(RACStream * (^)(id value))block {
	Class class = self.class;
	return [[self bind:^{
		return ^(id value, BOOL *stop) {
			id stream = block(value) ?: [class empty];
			NSCAssert([stream isKindOfClass:RACStream.class], @"Value returned from -flattenMap: is not a stream: %@", stream);
			return stream;
		};
	}] setNameWithFormat:@"[%@] -flattenMap:", self.name];
}

也很简单,只是调用了一下bind,再看看bind的实现,bind的实现位于RACSignal.m的92行左右。

 - (RACSignal *)bind:(RACStreamBindBlock (^)(void))block {
	NSCParameterAssert(block != NULL);
	/*
	 * -bind: should:
	 * 
	 * 1. Subscribe to the original signal of values.
	 * 2. Any time the original signal sends a value, transform it using the binding block.
	 * 3. If the binding block returns a signal, subscribe to it, and pass all of its values through to the subscriber as they're received.
	 * 4. If the binding block asks the bind to terminate, complete the _original_ signal.
	 * 5. When _all_ signals complete, send completed to the subscriber.
	 * 
	 * If any signal sends an error at any point, send that to the subscriber.
	 */
	return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
		RACStreamBindBlock bindingBlock = block();
		NSMutableArray *signals = [NSMutableArray arrayWithObject:self];
		// 此处省略了80行代码
		// ...
	}] setNameWithFormat:@"[%@] -bind:", self.name];
}

如果你下载了源代码(不想下源码的话,也可以在线查看),并且看到了这里,相信你的感觉一定是一脸懵逼的,不要激动,虽然这个方法很长,看上去也不那么好懂,但是关键点就那么几个地方,掌握了关键点就基本能get了。 ReactiveCocoa的作者更是罕见地在实现文件了写了一大段注释来说明bind方法的用途,根据作者的注释再去理解这个方法会轻松很多。 这里贴一个图,方便大家理解:

Alt text

OK,了解了bind操作的用途,也是时候回归主题了——内存是怎么泄露的。 首先我们看到,在didSubscribe的开头,就创建了一个数组signals,并且持有了self,也就是源信号:

NSMutableArray *signals = [NSMutableArray arrayWithObject:self];

(p.s. 如果你不知道didSubscribe是什么,也不了解ReactiveCocoa中信号的订阅过程,可以先看下《RACSignal的Subscription深入分析》)

接下来会对源信号进行订阅:

RACDisposable *bindingDisposable = [self subscribeNext:^(id x) {
    // Manually check disposal to handle synchronous errors.
    if (compoundDisposable.disposed) return;
    BOOL stop = NO;
    id signal = bindingBlock(x, &stop);
    @autoreleasepool {
        if (signal != nil) addSignal(signal);
        if (signal == nil || stop) {
            [selfDisposable dispose];
            completeSignal(self, selfDisposable);
        }
    }
} error:^(NSError *error) {
    //...
} completed:^{
    //...
}];

订阅者会持有nextBlock、errorBlock、completedBlock三个block,为了简单,我们只讨论nextBlock。 从nextBlock中的completeSignal(self, selfDisposable);这一行代码可以看出,nextBlock对self,也就是源信号进行了持有,再看到if (signal != nil) addSignal(signal);这一行,nextBlock对addSignal进行了持有,addSignal是在订阅self之前定义的一个block。

void (^addSignal)(RACSignal *) = ^(RACSignal *signal) {
    @synchronized (signals) {
        [signals addObject:signal];
    }
    //...
};

addSignal这个block里面对一开始创建的数组signals进行了持有,用一幅图来描述下刚才所说的关系:

Alt text

如果这个signal是一个RACSignal,那么是没有任何问题的;如果是signal是一个RACSubject,那问题就来了。还记得前面说过的RACSignal和RACSubject的区别吗?RACSubject会持有订阅者,而RACSignal不会持有订阅者,如果signal是一个RACSubject,那么图应该是这样的:

Alt text

很明显,产生了循环引用!!!到这里,也就解答了前面提出的三个问题的前两个: 对一个信号进行了map操作,那么最终会调用到bind。 如果源信号是RACSubject,由于RACSubject会持有订阅者,所以产生了循环引用(内存泄漏); 如果源信号是RACSignal,由于RACSignal不会持有订阅者,那么也就不存在循环引用。

还剩下最后一个问题:如果源信号是RACSubject,为什么发送完成可以修复内存泄漏? 来看下订阅者收到完成信号之后干了些什么:

RACDisposable *bindingDisposable = [self subscribeNext:^(id x) {
    //...
} error:^(NSError *error) {
    //...
} completed:^{
    @autoreleasepool {
        completeSignal(self, selfDisposable);
    }
}];

很简单,只是调用了一下completeSignal这个block。再看下这个block内部在干嘛:


void (^completeSignal)(RACSignal *, RACDisposable *) = ^(RACSignal *signal, RACDisposable *finishedDisposable) {
    BOOL removeDisposable = NO;
    @synchronized (signals) {
        [signals removeObject:signal]; //1
        if (signals.count == 0) {
            [subscriber sendCompleted]; //2
            [compoundDisposable dispose]; //3
        } else {
            removeDisposable = YES;
        }
    }
    if (removeDisposable) [compoundDisposable removeDisposable:finishedDisposable]; //4
};

//1这里从signals这个数组中移除传入的signal,也就断掉了signals持有subject这条线。 //2、//3、//4其实干的事情差不多,都是拿到对应的disposable调用dispose,这样资源就得到了回收,subject就不会再持有subscriber,subscriber也会对自己的nextBlock、errorBlock、completedBlock三个block置为nil,就不会存在引用关系,所有的对象都得到了释放。 有兴趣的同学可以去了解下RACDisposable,它也是ReactiveCocoa中的重要一员,对理解源码有很大的帮助。 map只是一个很典型的操作,其实在ReactiveCocoa的实现中,几乎所有的操作底层都会调用到bind这样一个方法,包括但不限于: map、filter、merge、combineLatest、flattenMap ……

所以在使用ReactiveCocoa的时候也一定要仔细,对信号操作完成之后,记得发送完成信号,不然可能在不经意间就导致了内存泄漏。

RACSubject就是一个比较典型直接的例子。除此之外,如果在对一个信号进行类似replay这样的操作之后,也一定要保证源信号发送完成;不然,也是会有内存泄漏的。

RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@1];
    [subscriber sendCompleted]; // 保证源信号发送完成
    return nil;
}];

RACSignal *replaySignal = [signal replay]; // 这里返回的其实是一个RACReplaySubject

[[replaySignal map:^id(NSNumber *value) {
    return @([value integerValue] * 3);
}] subscribeNext:^(id x) {
    NSLog(@"subscribeNext - %@", x);
}];

总之,一句话:使用ReactiveCocoa必须要保证信号发送完成或者发送错误。

参考资料