7

ReactiveCocoa-RACChannel

 4 years ago
source link: https://chipengliu.github.io/2019/04/01/ReactiveCocoa-RACChannel/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

之前分析过 RACSignal,数据流向是单向而且是1对1的,如果想使用双向绑定的效果,可以使用 ReactiveCocoa 框架中提供的 RACChannel;接来下分析 RACChannel 底层的实现原理。

我们以一个例子开始分析,比如将视图控制器中的 UITextField text 属性和 viewModel 中的 inputText 属性进行双向绑定,一般会这么做:

1
RACChannelTo(viewModel, inputText) = textField.rac_newTextChannel;

绑定之后,当 textField 的输入内容变化之后,会通知到 viewModel 的 inputText 进行变化,同样的当

inputText 被主动修改之后 textField 的输入框文本也会同步修改

image-20190401151228767

RACChannel

1
2
3
4
5
6
7
8
9
10
11
12
13
@interface RACChannel<ValueType> : NSObject

@property (nonatomic, strong, readonly) RACChannelTerminal<ValueType> *leadingTerminal;

@property (nonatomic, strong, readonly) RACChannelTerminal<ValueType> *followingTerminal;

@end

@interface RACChannelTerminal<ValueType> : RACSignal<ValueType> <RACSubscriber>

- (instancetype)init __attribute__((unavailable("Instantiate a RACChannel instead")));

- (void)sendNext:(nullable ValueType)value;

RACChannel 持有2个 RACChannelTerminal 类型的只读属性 leadingTerminalfollowingTerminal,RACChannelTerminal 是 RACChannel 的子类并实现了 RACSubscriber 协议

对 viewModel 的数据进行的操作会发送给 leadingTerminal 再通过内部转发给 followingTerminal,由于 textField 是 followingTerminal 的订阅者,所以消息最终会发送到视图上

类似的当 textField 输入内容发生变化的时候会把数据发送给 followingTerminal,然后内部转发给 leadingTerminal,因为 viewModel 是 leadingTerminal 的订阅者,所以数据最终会通知给 viewModel。

RACChannelTerminal

在这个过程中,RACChannelTerminal 作用类似管道(pipe)和网络连接中的 socket,都表示连接中的一端;RACChannelTerminal 执行 sendNext,类似 socket 进行 write 给对端发送数据,类似的 subscribe 可以类比成 socket read。

了解了大概作用之后,在看看 RACChannelTerminal 的具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@interface RACChannelTerminal<ValueType> ()

/// The values for this terminal.
@property (nonatomic, strong, readonly) RACSignal<ValueType> *values;

/// A subscriber will will send values to the other terminal.
@property (nonatomic, strong, readonly) id<RACSubscriber> otherTerminal;

- (instancetype)initWithValues:(RACSignal<ValueType> *)values otherTerminal:(id<RACSubscriber>)otherTerminal;

@end

@implementation RACChannelTerminal

#pragma mark Lifecycle

- (instancetype)initWithValues:(RACSignal *)values otherTerminal:(id<RACSubscriber>)otherTerminal {
NSCParameterAssert(values != nil);
NSCParameterAssert(otherTerminal != nil);

self = [super init];

_values = values;
_otherTerminal = otherTerminal;

return self;
}

#pragma mark RACSignal

- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber {
return [self.values subscribe:subscriber];
}

#pragma mark <RACSubscriber>

- (void)sendNext:(id)value {
[self.otherTerminal sendNext:value];
}

- (void)sendError:(NSError *)error {
[self.otherTerminal sendError:error];
}

- (void)sendCompleted {
[self.otherTerminal sendCompleted];
}

- (void)didSubscribeWithDisposable:(RACCompoundDisposable *)disposable {
[self.otherTerminal didSubscribeWithDisposable:disposable];
}

@end

RACChannelTerminal 定义了2个私有属性:

  • values:RACSignal,是所发送数据的信号源
  • otherTerminal:RACChannelTerminal类型,接受 values 发出信号的对端端点

RACChannelTerminal 被订阅,比如订阅者调用 -subscribeNext: 等方法的时候,会执行 -subscribe 方法,于是最终订阅者会订阅 values 信号;当 RACChannelTerminal 发送信号的时候则会转发给对端断点 otherTerminal。

RACChannel 初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (instancetype)init {
self = [super init];

// We don't want any starting value from the leadingSubject, but we do want
// error and completion to be replayed.
RACReplaySubject *leadingSubject = [[RACReplaySubject replaySubjectWithCapacity:0] setNameWithFormat:@"leadingSubject"];
RACReplaySubject *followingSubject = [[RACReplaySubject replaySubjectWithCapacity:1] setNameWithFormat:@"followingSubject"];

// Propagate errors and completion to everything.
[[leadingSubject ignoreValues] subscribe:followingSubject];
[[followingSubject ignoreValues] subscribe:leadingSubject];

_leadingTerminal = [[[RACChannelTerminal alloc] initWithValues:leadingSubject otherTerminal:followingSubject] setNameWithFormat:@"leadingTerminal"];
_followingTerminal = [[[RACChannelTerminal alloc] initWithValues:followingSubject otherTerminal:leadingSubject] setNameWithFormat:@"followingTerminal"];

return self;
}

@end

初始化过程中首先会先初始化2个 RACReplaySubject 变量:

  1. leadingSubject:capacity为0,订阅者只能收到订阅之后 leadingSubject 发送的信号

  2. followingSubject:capacity为1,订阅者订阅之后马上会搜到 followingSubject 之前最新发送的一个信号

  3. 将 followingSubject 设置成 leadingSubject 的订阅者,只接受 followingSubject 发送的 sendCompleted / sendError;同理 leadingSubject 也是 followingSubject 的订阅者并且只会收到 sendCompleted / sendError 事件。

    处理后,当 leadingSubject(followingSubject) 发送 sendCompleted / sendError 之后,followingSubject(leadingSubject) 的 订阅者会收到相关信号。

  4. 初始化 leadingTerminal 和 followingTerminal,leadingTerminal/followingTerminal 被订阅的时候,实际上是订阅私有属性 values 的信号,leadingTerminal/followingTerminal 执行 sendNext 发送信号的时候会,其订阅者不会收到信号,而是转发给私有属性 otherTerminal 发送给它的订阅者

RACChannel 双向传输过程

分析完 RACChannel 初始化过程之后,回到文章开头的例子

1
RACChannelTo(viewModel, inputText) = textField.rac_newTextChannel;

首先看宏 RACChannelTo

1
2
3
4
5
6
7
#define RACChannelTo(TARGET, ...) \
metamacro_if_eq(1, metamacro_argcount(__VA_ARGS__)) \
(RACChannelTo_(TARGET, __VA_ARGS__, nil)) \
(RACChannelTo_(TARGET, __VA_ARGS__))

#define RACChannelTo_(TARGET, KEYPATH, NILVALUE) \
[[RACKVOChannel alloc] initWithTarget:(TARGET) keyPath:@keypath(TARGET, KEYPATH) nilValue:(NILVALUE)][@keypath(RACKVOChannel.new, followingTerminal)]

内部是调用了 RACKVOChannel 的方法 -initWithTarget:keyPath:nilValue:

1
[[RACKVOChannel alloc] initWithTarget:(TARGET) keyPath:@keypath(TARGET, KEYPATH) nilValue:(NILVALUE)][@keypath(RACKVOChannel.new, followingTerminal)]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
- (instancetype)initWithTarget:(__weak NSObject *)target keyPath:(NSString *)keyPath nilValue:(id)nilValue {
NSCParameterAssert(keyPath.rac_keyPathComponents.count > 0);

NSObject *strongTarget = target;

self = [super init];

_target = target;
_keyPath = [keyPath copy];

......

if (strongTarget == nil) {
[self.leadingTerminal sendCompleted];
return self;
}

RACDisposable *observationDisposable = [strongTarget rac_observeKeyPath:keyPath options:NSKeyValueObservingOptionInitial observer:nil block:^(id value, NSDictionary *change, BOOL causedByDealloc, BOOL affectedOnlyLastComponent) {
if (!causedByDealloc && affectedOnlyLastComponent && self.currentThreadData.ignoreNextUpdate) {
[self destroyCurrentThreadData];
return;
}

[self.leadingTerminal sendNext:value];
}];

......

[[self.leadingTerminal
finally:^{
[observationDisposable dispose];
}]
subscribeNext:^(id x) {
NSObject *object = (keyPathComponentsCount > 1 ? [self.target valueForKeyPath:keyPathByDeletingLastKeyPathComponent] : self.target);
if (object == nil) return;

......

[object setValue:x ?: nilValue forKey:lastKeyPathComponent];
} error:^(NSError *error) {
NSCAssert(NO, @"Received error in %@: %@", self, error);
NSLog(@"Received error in %@: %@", self, error);
}];

@weakify(self);

[strongTarget.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{
@strongify(self);
[self.leadingTerminal sendCompleted];
self.target = nil;
}]];

return self;
}

省略部分代码,其主要流程:

  1. 首先判断 taget 是否为空,如果是 nil 则 leadingTerminal 发送 sendCompleted,followingTerminal 的订阅者此时会收到 sendCompleted 事件。反之则执行步骤2
  2. 调用 KVO 监听到 target 对应 keyPath 变化的时候,leadingTerminal 发送 value,followingTerminal 订阅者会收到该 value
  3. 订阅 leadingTerminal 收到 value 之后利用 KVC 赋值到 target 对应的属性
image-20190402215557859

初始化完 RACKVOChannel 之后,RACChannelTo 宏后半部分 [@keypath(RACKVOChannel.new, followingTerminal)] 直接上会调用其重载的函数 -objectForKeyedSubscript: 并返回 followingTerminal 对象

1
2
3
4
5
6
7
8
- (RACChannelTerminal *)objectForKeyedSubscript:(NSString *)key {
NSCParameterAssert(key != nil);

RACChannelTerminal *terminal = [self valueForKey:key];
NSCAssert([terminal isKindOfClass:RACChannelTerminal.class], @"Key \"%@\" does not identify a channel terminal", key);

return terminal;
}

样例代码可以改变成

1
[[RACKVOChannel alloc] initWithTarget:viewModel keyPath:@"inputText" nilValue:nil][@"followingTerminal"] = [[RACKVOChannel alloc] initWithTarget:textField keyPath:@"text" nilValue:nil][@"followingTerminal"];

这里进行 = 号进行复制,最终会执行重载方法 - (void)setObject:forKeyedSubscript:

1
2
3
4
5
6
7
- (void)setObject:(RACChannelTerminal *)otherTerminal forKeyedSubscript:(NSString *)key {
NSCParameterAssert(otherTerminal != nil);

RACChannelTerminal *selfTerminal = [self objectForKeyedSubscript:key];
[otherTerminal subscribe:selfTerminal];
[[selfTerminal skip:1] subscribe:otherTerminal];
}

根据样例代码分析,这里 selfTerminal 是 viewModel 侧的 followingTerminal,otherTerminal 则是 textField 侧的 followingTerminal;

otherTerminal 把订阅者设置为 selfTerminal,otherTerminal 执行 sendNext 的时候,selfTerminal 的订阅者会收到信号;

同理 selfTerminal 把订阅者设置为 otherTerminal,selfTerminal 执行 sendNext 的时候,otherTerminal 的订阅者会收到信号,但是只会收到 selfTerminal 第二个信号及其后续信号。

image-20190403111110637

至此,RACChannel 双向通信大概过程分析已经完成

- (void)setObject:forKeyedSubscript:-objectForKeyedSubscript: 相关资料可参考: 对象下标索引


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK