Skip to content

Commit f57d781

Browse files
author
Draveness
committed
Add RACMulticastConnection article
1 parent 9f4aca9 commit f57d781

12 files changed

+291
-2
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111

1212
## 目录
1313

14-
Latest:[优雅的 RACCommand](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/ReactiveObjC/RACCommand.md)
14+
Latest:[用于多播的 RACMulticastConnection](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/ReactiveObjC/RACMulticastConnection.md)
1515

1616
| Project | Version | Article |
1717
|:-------:|:-------:|:------|
18-
| ReactiveObjC | 2.1.2 | [『状态』驱动的世界:ReactiveCocoa](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/ReactiveObjC/RACSignal.md) <br> [Pull-Driven 的数据流 RACSequence](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/ReactiveObjC/RACSequence.md) <br>[『可变』的热信号 RACSubject](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/ReactiveObjC/RACSubject.md) <br> [优雅的 RACCommand](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/ReactiveObjC/RACCommand.md)|
18+
| ReactiveObjC | 2.1.2 | [『状态』驱动的世界:ReactiveCocoa](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/ReactiveObjC/RACSignal.md) <br> [Pull-Driven 的数据流 RACSequence](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/ReactiveObjC/RACSequence.md) <br>[『可变』的热信号 RACSubject](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/ReactiveObjC/RACSubject.md) <br> [优雅的 RACCommand](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/ReactiveObjC/RACCommand.md) <br> [用于多播的 RACMulticastConnection](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/ReactiveObjC/RACMulticastConnection.md)|
1919
| AsyncDisplayKit | 1.9.81 | [提升 iOS 界面的渲染性能](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/AsyncDisplayKit/提升%20iOS%20界面的渲染性能%20.md)<br> [从 Auto Layout 的布局算法谈性能](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/AsyncDisplayKit/从%20Auto%20Layout%20的布局算法谈性能.md) <br>[预加载与智能预加载(iOS)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/AsyncDisplayKit/预加载与智能预加载(iOS).md)|
2020
| CocoaPods | 1.1.0 | [CocoaPods 都做了什么?](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/CocoaPods/CocoaPods%20都做了什么?.md) <br> [谈谈 DSL 以及 DSL 的应用(以 CocoaPods 为例)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/CocoaPods/谈谈%20DSL%20以及%20DSL%20的应用(以%20CocoaPods%20为例).md)|
2121
| OHHTTPStubs | 5.1.0 | [iOS 开发中使用 NSURLProtocol 拦截 HTTP 请求](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/OHHTTPStubs/iOS%20开发中使用%20NSURLProtocol%20拦截%20HTTP%20请求.md) <br> [如何进行 HTTP Mock(iOS)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/OHHTTPStubs/如何进行%20HTTP%20Mock(iOS).md) |
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
# 用于多播的 RACMulticastConnection
2+
3+
ReactiveCocoa 中的信号信号在默认情况下都是冷的,每次有新的订阅者订阅信号时都会执行信号创建时传入的 block;这意味着对于任意一个订阅者,所需要的数据都会**重新计算**,这在大多数情况下都是开发者想看到的情况,但是这在信号中的 block 有副作用或者较为昂贵时就会有很多问题。
4+
5+
![RACMulticastConnection](images/RACMulticastConnection/RACMulticastConnection.png)
6+
7+
我们希望有一种模型能够将冷信号转变成热信号,并在合适的时间触发,向所有的订阅者发送消息;而今天要介绍的 `RACMulticastConnection` 就是用于解决上述问题的。
8+
9+
## RACMulticastConnection 简介
10+
11+
`RACMulticastConnection` 封装了将一个信号的订阅分享给多个订阅者的思想,它的每一个对象都持有两个 `RACSignal`
12+
13+
![RACMulticastConnection-Interface](images/RACMulticastConnection/RACMulticastConnection-Interface.png)
14+
15+
一个是私有的源信号 `sourceSignal`,另一个是用于广播的信号 `signal`,其实是一个 `RACSubject` 对象,不过对外只提供 `RACSignal` 接口,用于使用者通过 `-subscribeNext:` 等方法进行订阅。
16+
17+
## RACMulticastConnection 的初始化
18+
19+
`RACMulticastConnection` 有一个非常简单的初始化方法 `-initWithSourceSignal:subject:`,不过这个初始化方法是私有的:
20+
21+
```objectivec
22+
- (instancetype)initWithSourceSignal:(RACSignal *)source subject:(RACSubject *)subject {
23+
self = [super init];
24+
25+
_sourceSignal = source;
26+
_serialDisposable = [[RACSerialDisposable alloc] init];
27+
_signal = subject;
28+
29+
return self;
30+
}
31+
```
32+
33+
`RACMulticastConnection` 的头文件的注释中,对它的初始化有这样的说明:
34+
35+
> Note that you shouldn't create RACMulticastConnection manually. Instead use -[RACSignal publish] or -[RACSignal multicast:].
36+
37+
我们不应该直接使用 `-initWithSourceSignal:subject:` 来初始化一个对象,我们应该通过 `RACSignal` 的实例方法初始化 `RACMulticastConnection` 实例。
38+
39+
```objectivec
40+
- (RACMulticastConnection *)publish {
41+
RACSubject *subject = [RACSubject subject];
42+
RACMulticastConnection *connection = [self multicast:subject];
43+
return connection;
44+
}
45+
46+
- (RACMulticastConnection *)multicast:(RACSubject *)subject {
47+
RACMulticastConnection *connection = [[RACMulticastConnection alloc] initWithSourceSignal:self subject:subject];
48+
return connection;
49+
}
50+
```
51+
52+
这两个方法 `-publish``-multicast:` 都是对初始化方法的封装,并且都会返回一个 `RACMulticastConnection` 对象,传入的 `sourceSignal` 就是当前信号,`subject` 就是用于对外广播的 `RACSubject` 对象。
53+
54+
## RACSignal 和 RACMulticastConnection
55+
56+
网络请求在客户端其实是一个非常昂贵的操作,也算是多级缓存中最慢的一级,在使用 ReactiveCocoa 处理业务需求中经常会遇到下面的情况:
57+
58+
```objectivec
59+
RACSignal *requestSignal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
60+
NSLog(@"Send Request");
61+
NSURL *url = [NSURL URLWithString:@"http://localhost:3000"];
62+
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:url];
63+
NSString *URLString = [NSString stringWithFormat:@"/api/products/1"];
64+
NSURLSessionDataTask *task = [manager GET:URLString parameters:nil progress:nil
65+
success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
66+
[subscriber sendNext:responseObject];
67+
[subscriber sendCompleted];
68+
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
69+
[subscriber sendError:error];
70+
}];
71+
return [RACDisposable disposableWithBlock:^{
72+
[task cancel];
73+
}];
74+
}];
75+
76+
[requestSignal subscribeNext:^(id _Nullable x) {
77+
NSLog(@"product: %@", x);
78+
}];
79+
80+
[requestSignal subscribeNext:^(id _Nullable x) {
81+
NSNumber *productId = [x objectForKey:@"id"];
82+
NSLog(@"productId: %@", productId);
83+
}];
84+
```
85+
86+
通过订阅发出网络请求的信号经常会被多次订阅,以满足不同 UI 组件更新的需求,但是以上代码却有非常严重的问题。
87+
88+
![RACSignal-And-Subscribe](images/RACMulticastConnection/RACSignal-And-Subscribe.png)
89+
90+
每一次在 `RACSignal` 上执行 `-subscribeNext:` 以及类似方法时,都会发起一次新的网络请求,我们希望避免这种情况的发生。
91+
92+
为了解决上述问题,我们使用了 `-publish` 方法获得一个多播对象 `RACMulticastConnection`,更改后的代码如下:
93+
94+
```objectivec
95+
RACMulticastConnection *connection = [[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
96+
NSLog(@"Send Request");
97+
...
98+
}] publish];
99+
100+
[connection.signal subscribeNext:^(id _Nullable x) {
101+
NSLog(@"product: %@", x);
102+
}];
103+
[connection.signal subscribeNext:^(id _Nullable x) {
104+
NSNumber *productId = [x objectForKey:@"id"];
105+
NSLog(@"productId: %@", productId);
106+
}];
107+
108+
[connection connect];
109+
```
110+
111+
在这个例子中,我们使用 `-publish` 方法生成实例,订阅者不再订阅源信号,而是订阅 `RACMulticastConnection` 中的 `RACSubject` 热信号,最后通过 `-connect` 方法触发源信号中的任务。
112+
113+
![RACSignal-RACMulticastConnection-Connect](images/RACMulticastConnection/RACSignal-RACMulticastConnection-Connect.png)
114+
115+
> 对于热信号不了解的读者,可以阅读这篇文章 [『可变』的热信号 RACSubject](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/ReactiveObjC/RACSubject.md)
116+
117+
### publish 和 multicast 方法
118+
119+
我们再来看一下 `-publish``-multicast:` 这两个方法的实现:
120+
121+
```objectivec
122+
- (RACMulticastConnection *)publish {
123+
RACSubject *subject = [RACSubject subject];
124+
RACMulticastConnection *connection = [self multicast:subject];
125+
return connection;
126+
}
127+
128+
- (RACMulticastConnection *)multicast:(RACSubject *)subject {
129+
RACMulticastConnection *connection = [[RACMulticastConnection alloc] initWithSourceSignal:self subject:subject];
130+
return connection;
131+
}
132+
```
133+
134+
`-publish` 方法调用时相当于向 `-multicast:` 传入了 `RACSubject`
135+
136+
![publish-and-multicast](images/RACMulticastConnection/publish-and-multicast.png)
137+
138+
`-publish` 只是对 `-multicast:` 方法的简单封装,它们都是通过 `RACMulticastConnection` 私有的初始化方法 `-initWithSourceSignal:subject:` 创建一个新的实例。
139+
140+
在使用 `-multicast:` 方法时,传入的信号其实就是用于广播的信号;这个信号必须是一个 `RACSubject` 本身或者它的子类:
141+
142+
![RACSubject - Subclasses](images/RACMulticastConnection/RACSubject%20-%20Subclasses.png)
143+
144+
传入 `-multicast:` 方法的一般都是 `RACSubject` 或者 `RACReplaySubject` 对象。
145+
146+
### 订阅源信号的时间点
147+
148+
订阅 `connection.signal` 中的数据流时,其实只是向多播对象中的热信号 `RACSubject` 持有的数组中加入订阅者,而这时刚刚创建的 `RACSubject` 中并没有任何的消息。
149+
150+
![SubscribeNext-To-RACSubject-Before-Connect](images/RACMulticastConnection/SubscribeNext-To-RACSubject-Before-Connect.png)
151+
152+
只有在调用 `-connect` 方法之后,`RACSubject` 才会**订阅**源信号 `sourceSignal`
153+
154+
```objectivec
155+
- (RACDisposable *)connect {
156+
self.serialDisposable.disposable = [self.sourceSignal subscribe:_signal];
157+
return self.serialDisposable;
158+
}
159+
```
160+
161+
这时源信号的 `didSubscribe` 代码块才会执行,向 `RACSubject` 推送消息,消息向下继续传递到 `RACSubject` 所有的订阅者中。
162+
163+
![Values-From-RACSignal-To-Subscribers](images/RACMulticastConnection/Values-From-RACSignal-To-Subscribers.png)
164+
165+
`-connect` 方法通过 `-subscribe:` 实际上建立了 `RACSignal``RACSubject` 之间的连接,这种方式保证了 `RACSignal` 中的 `didSubscribe` 代码块只执行了一次。
166+
167+
所有的订阅者不再订阅原信号,而是订阅 `RACMulticastConnection` 持有的热信号 `RACSubject`,实现对冷信号的一对多传播。
168+
169+
`RACMulticastConnection` 中还有另一个用于连接 `RACSignal``RACSubject` 信号的 `-autoconnect` 方法:
170+
171+
```objectivec
172+
- (RACSignal *)autoconnect {
173+
__block volatile int32_t subscriberCount = 0;
174+
return [RACSignal
175+
createSignal:^(id<RACSubscriber> subscriber) {
176+
OSAtomicIncrement32Barrier(&subscriberCount);
177+
RACDisposable *subscriptionDisposable = [self.signal subscribe:subscriber];
178+
RACDisposable *connectionDisposable = [self connect];
179+
180+
return [RACDisposable disposableWithBlock:^{
181+
[subscriptionDisposable dispose];
182+
if (OSAtomicDecrement32Barrier(&subscriberCount) == 0) {
183+
[connectionDisposable dispose];
184+
}
185+
}];
186+
}];
187+
}
188+
```
189+
190+
它保证了在 `-autoconnect` 方法返回的对象被第一次订阅时,就会建立源信号与热信号之间的连接。
191+
192+
### 使用 RACReplaySubject 订阅源信号
193+
194+
虽然使用 `-publish` 方法已经能够解决大部分问题了,但是在 `-connect` 方法调用之后才订阅的订阅者并不能收到消息。
195+
196+
如何才能保存 `didSubscribe` 执行过程中发送的消息,并在 `-connect` 调用之后也可以收到消息?这时,我们就要使用 `-multicast:` 方法和 `RACReplaySubject` 来完成这个需求了。
197+
198+
```objectivec
199+
RACSignal *sourceSignal = [RACSignal createSignal:...];
200+
RACMulticastConnection *connection = [sourceSignal multicast:[RACReplaySubject subject]];
201+
[connection.signal subscribeNext:^(id _Nullable x) {
202+
NSLog(@"product: %@", x);
203+
}];
204+
[connection connect];
205+
[connection.signal subscribeNext:^(id _Nullable x) {
206+
NSNumber *productId = [x objectForKey:@"id"];
207+
NSLog(@"productId: %@", productId);
208+
}];
209+
```
210+
211+
除了使用上述的代码,也有一个更简单的方式创建包含 `RACReplaySubject` 对象的 `RACMulticastConnection`:
212+
213+
```objectivec
214+
RACSignal *signal = [[RACSignal createSignal:...] replay];
215+
[signal subscribeNext:^(id _Nullable x) {
216+
NSLog(@"product: %@", x);
217+
}];
218+
[signal subscribeNext:^(id _Nullable x) {
219+
NSNumber *productId = [x objectForKey:@"id"];
220+
NSLog(@"productId: %@", productId);
221+
}];
222+
```
223+
224+
`-replay` 方法和 `-publish` 差不多,只是内部封装的热信号不同,并在方法调用时就连接原信号:
225+
226+
```objectivec
227+
- (RACSignal *)replay {
228+
RACReplaySubject *subject = [RACReplaySubject subject];
229+
RACMulticastConnection *connection = [self multicast:subject];
230+
[connection connect];
231+
return connection.signal;
232+
}
233+
```
234+
235+
除了 `-replay` 方法,`RACSignal` 中还定义了与 `RACMulticastConnection` 中相关的其它 `-replay` 方法:
236+
237+
```objectivec
238+
- (RACSignal<ValueType> *)replay;
239+
- (RACSignal<ValueType> *)replayLast;
240+
- (RACSignal<ValueType> *)replayLazily;
241+
```
242+
243+
三个方法都会在 `RACMulticastConnection` 初始化时传入一个 `RACReplaySubject` 对象,不过却有一点细微的差别:
244+
245+
![Difference-Between-Replay-Methods](images/RACMulticastConnection/Difference-Between-Replay-Methods.png)
246+
247+
相比于 `-replay` 方法,`-replayLast` 方法生成的 `RACMulticastConnection` 中热信号的容量为 `1`
248+
249+
```objectivec
250+
- (RACSignal *)replayLast {
251+
RACReplaySubject *subject = [RACReplaySubject replaySubjectWithCapacity:1];
252+
RACMulticastConnection *connection = [self multicast:subject];
253+
[connection connect];
254+
return connection.signal;
255+
}
256+
```
257+
258+
`replayLazily` 会在返回的信号被**第一次订阅**时,才会执行 `-connect` 方法:
259+
260+
```objectivec
261+
- (RACSignal *)replayLazily {
262+
RACMulticastConnection *connection = [self multicast:[RACReplaySubject subject]];
263+
return [RACSignal
264+
defer:^{
265+
[connection connect];
266+
return connection.signal;
267+
}];
268+
}
269+
```
270+
271+
## 总结
272+
273+
`RACMulticastConnection` 在处理冷热信号相互转换时非常好用,在 `RACSignal` 中也提供了很多将原有的冷信号通过 `RACMulticastConnection` 转换成热信号的方法。
274+
275+
![RACMulticastConnection](images/RACMulticastConnection/RACMulticastConnection.png)
276+
277+
在遇到冷信号中的行为有副作用后者非常昂贵时,我们就可以使用这些方法将单播变成多播,提高执行效率,减少副作用。
278+
279+
## References
280+
281+
+ [『可变』的热信号 RACSubject](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/ReactiveObjC/RACSubject.md)
282+
+ [细说 ReactiveCocoa 的冷信号与热信号](http://williamzang.com/blog/2015/08/18/talk-about-reactivecocoas-cold-signal-and-hot-signal/)
283+
284+
> Github Repo:[iOS-Source-Code-Analyze](https://github.com/draveness/iOS-Source-Code-Analyze)
285+
>
286+
> Follow: [Draveness · GitHub](https://github.com/Draveness)
287+
>
288+
> Source: http://draveness.me/racconnection
289+
62.1 KB
Loading
23.7 KB
Loading
58 KB
Loading
50.5 KB
Loading
43.4 KB
Loading
26.6 KB
Loading
48.2 KB
Loading
77.6 KB
Loading

0 commit comments

Comments
 (0)