RAC-异步行为

前言

OC中的异步行为:

  • block
  • delegate
  • KVO
  • 通知
  • target-action机制

异步行为的共同点:

  • 何时触发并不确定,观察者需要等待其触发时的回调;
  • 异步行为的创建和触发并不在同一块区域,往往是分开的。

异步行为的缺点:

  • 异步行为的创建和回调相对分散,这不符合代码规范中”高内聚”的要求;
  • 使用不当时,这些行为往往会造成循环引用等问题。

RAC文档 摘要:

One of the major advantages of RAC is that it provides a single, unified approach to dealing with asynchronous behaviors, including delegate methods, callback blocks, target-action mechanisms, notifications, and KVO.

RAC 借鉴了RX的思想,它处理这些问题的核心在于信号

针对内聚问题,RAC将异步行为的实现封装在信号的didSubscribeblock 中,从而能对外提供统一的接口。我们只需要在创建完信号后订阅信号,即可在异步行为触发时收到回调,这样创建、监听、业务逻辑聚合到了一起;

针对第二点,信号的 block 内向订阅者发送了sendNext:事件后,会自动清理资源和引用,从而解决了循环引用问题。

下面将分别介绍它们的使用和实现原理。

block

block 创建完成后在随后的某个时间点被调用,执行其内部定义的业务。RAC中block的使用如下:

#示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@property (nonatomic, strong) id<RACSubscriber> subscriber;

- (void)viewDidLoad {
[super viewDidLoad];

//1.信号的使用
RACSignal *s1 = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
_subscriber = subscriber;
return [RACDisposable disposableWithBlock:^{
NSLog(@"++s1 Disposed~");
}];
}];

//订阅消息,创建block
[s1 subscribeNext:^(id x) {
NSLog(@"+++订阅触发:%@",x);
}];
//调用block
[_subscriber sendNext:@"Hello world~"];
}

执行[s1 subscribeNext:]订阅信号时,我们传入了一个 nextBlock,它会被保存起来以在后续被调用。subscribeNext:的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//RACSignal.m
- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock {
NSCParameterAssert(nextBlock != NULL);
//1.创建订阅者
RACSubscriber *o = [RACSubscriber subscriberWithNext:nextBlock error:NULL completed:NULL];
return [self subscribe:o];
}

//RACSubscriber.m
+ (instancetype)subscriberWithNext:(void (^)(id x))next error:(void (^)(NSError *error))error completed:(void (^)(void))completed {
RACSubscriber *subscriber = [[self alloc] init];

//2.保存 block
subscriber->_next = [next copy];
subscriber->_error = [error copy];
subscriber->_completed = [completed copy];

return subscriber;
}

信号内部自动创建了一个订阅者对象,并将我们传入的 nextBlock 保存了起来。

随后在示例中我们调用了[_subscriber sendNext:],其实现如下:

1
2
3
4
5
6
7
- (void)sendNext:(id)value {
@synchronized (self) {
void (^nextBlock)(id) = [self.next copy];
if (nextBlock == nil) return;
nextBlock(value);
}
}

即调用 sendNext: 相当于调用了之前保存在订阅者内部的nextBlock并向其输入了一个新值。

KVO

#示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@property (nonatomic, copy) NSString *text;

- (void)viewDidLoad {
[super viewDidLoad];
[[self rac_valuesAndChangesForKeyPath:@"text"
options:NSKeyValueObservingOptionNew
observer:self]
subscribeNext:^(RACTwoTuple<id,NSDictionary *> *x) {
NSLog(@"++self.text update:%@",x.first);
}];

self.text = @"Text1";
_text = @"Text2";
}

日志:

1
++self.text update:Text1

注意:要使用self.xxx来赋值,下划线的方式是给成员变量直接赋值,并不会触发KVO回调~~

  • RAC的KVO实现原理:

RAC 版的 KVO 与 OC 版的差别并不大,rac_valuesAndChangesForKeyPath:options:observer:方法是RAC在 NSObject 分类中定义的一个方法。它的返回值是一个信号,供我们订阅并自定义回调block。其内部最终是通过RACKVOTrampoline来管理和实现KVO的。

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
//RACKVOTrampoline.m
- (instancetype)initWithTarget:(__weak NSObject *)target
observer:(__weak NSObject *)observer
keyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
block:(RACKVOBlock)block {

NSCParameterAssert(keyPath != nil);
NSCParameterAssert(block != nil);

NSObject *strongTarget = target;
if (strongTarget == nil) return nil;

self = [super init];

_keyPath = [keyPath copy];

_block = [block copy];
_weakTarget = target;
_unsafeTarget = strongTarget;
_observer = observer;

[RACKVOProxy.sharedProxy addObserver:self forContext:(__bridge void *)self];
[strongTarget addObserver:RACKVOProxy.sharedProxy forKeyPath:self.keyPath options:options context:(__bridge void *)self];

[strongTarget.rac_deallocDisposable addDisposable:self];
[self.observer.rac_deallocDisposable addDisposable:self];

return self;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context != (__bridge void *)self) {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
return;
}

RACKVOBlock block;
id observer;
id target;

@synchronized (self) {
block = self.block;
observer = self.observer;
target = self.weakTarget;
}

if (block == nil || target == nil) return;

block(target, observer, change);
}

RACKVOTrampoline将我们传入的observertarget及回调block保存了起来。属性变化后KVO代理中回调了 block 并传回变化的信息。

KVC

KVC本身不算异步行为,因为OC中调用[self valueForKey:@”text”]后,能立刻得到text属性的值。但是 RAC 还是对其进行了封装和扩展,使得 KVC 既能立刻获取属性的值,又能和 KVO 一样持续收到属性变化的回调。

#示例:

1
2
3
4
5
6
7
8
9
10
11
@property (nonatomic, copy) NSString *text;

- (void)viewDidLoad {
[super viewDidLoad];
[[self rac_valuesForKeyPath:@"text" observer:self] subscribeNext:^(id x) {
NSLog(@"++++self.text By KVC:%@",x);
}];

self.text = @"Text1";
_text = @"Text2";
}

日志:

1
2
++++self.text By KVC:(null)
++++self.text By KVC:Text1

调用 rac_valuesForKeyPath: 并订阅信号后,立刻收到了一次回调,返回属性的当前值null

调用 self.text = @”Text1” 给属性赋值后,订阅者的回调再次触发并传回当前的新值。

调用 _text = @”Text2” 给属性的成员变量赋值时,并未触发订阅者回调,这与 RAC 的 KVO 一样。

结合这些情况来看,RAC 的 KVC 似乎与其 KVO 有密切关系,那么我们来看看其方法的实现:

1
2
3
4
5
6
7
8
9
- (RACSignal *)rac_valuesForKeyPath:(NSString *)keyPath observer:(__weak NSObject *)observer {
return [[[self
rac_valuesAndChangesForKeyPath:keyPath options:NSKeyValueObservingOptionInitial observer:observer]
map:^(RACTuple *value) {
// -map: because it doesn't require the block trampoline that -reduceEach: uses
return value[0];
}]
setNameWithFormat:@"RACObserve(%@, %@)", RACDescription(self), keyPath];
}

可以看到,RAC 的 KVC 内部只是调用了其 KVO 方法并返回元组的第一个值。这就不难理解上面的猜测了~

通知

#示例:

1
2
3
4
5
[[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIKeyboardWillShowNotification
object:nil]
subscribeNext:^(NSNotification *x) {
NSLog(@"++通知:%@",x);
}];

监听键盘通知并订阅其返回的信号,待键盘弹出或收回时即可触发订阅者的回调。其实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@implementation NSNotificationCenter (RACSupport)

- (RACSignal *)rac_addObserverForName:(NSString *)notificationName object:(id)object {
@unsafeify(object);
return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
@strongify(object);
id observer = [self addObserverForName:notificationName object:object queue:nil usingBlock:^(NSNotification *note) {
[subscriber sendNext:note];
}];

return [RACDisposable disposableWithBlock:^{
[self removeObserver:observer];
}];
}] setNameWithFormat:@"-rac_addObserverForName: %@ object: <%@: %p>", notificationName, [object class], object];
}
@end

在RAC对 NSNotificationCenter 的扩展中,监听通知后实际上只是调用了OC原生的通知监听方法,在原生回调中向订阅者发送消息~

target-action

#示例:

1
2
3
[[_mBtn1 rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(UIControl *x) {
NSLog(@"++clicked Btn1~");
}];

其内部实现为:

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
@implementation UIControl (RACSignalSupport)

- (RACSignal *)rac_signalForControlEvents:(UIControlEvents)controlEvents {
@weakify(self);

return [[RACSignal
createSignal:^(id<RACSubscriber> subscriber) {
@strongify(self);
//OC原生方法
[self addTarget:subscriber action:@selector(sendNext:) forControlEvents:controlEvents];

RACDisposable *disposable = [RACDisposable disposableWithBlock:^{
[subscriber sendCompleted];
}];
[self.rac_deallocDisposable addDisposable:disposable];

return [RACDisposable disposableWithBlock:^{
@strongify(self);
[self.rac_deallocDisposable removeDisposable:disposable];
[self removeTarget:subscriber action:@selector(sendNext:) forControlEvents:controlEvents];
}];
}]
setNameWithFormat:@"%@ -rac_signalForControlEvents: %lx", RACDescription(self), (unsigned long)controlEvents];
}
@end

RAC在UIControl的分类中返回一个信号,通过OC原生的”addTarget:action:forControlEvents:”添加了subscribertarget,而其响应方法为sendNext:。所以当按钮被点击后会触发subscribersendNext:方法,随即回调订阅者的 block。

delegate

RAC中的delegate可以通过RACSubject来实现。官方文档中关于此类的描述如下:

A subject, represented by the RACSubject class, is a signal that can be manually controlled.
Subjects can be thought of as the “mutable” variant of a signal, much like NSMutableArray is for NSArray. They are extremely useful for bridging non-RAC code into the world of signals.
For example, instead of handling application logic in block callbacks, the blocks can simply send events to a shared subject instead. The subject can then be returned as a RACSignal, hiding the implementation detail of the callbacks.

RACSubject继承自RACSignal,是一种可以由我们控制的信号。

相比于在某个 block 回调中自己处理业务逻辑,RAC 可以让 block 向某个共享的 subject 发送事件并让其处理该业务逻辑。

#示例:

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
//HelloRACController.m
- (IBAction)onAction2:(id)sender {
//push到新界面
ViewControllerII *controller = [[UIStoryboard storyboardWithName:@"Main" bundle:nil]
instantiateViewControllerWithIdentifier:@"ViewControllerII"];
//设置代理信号
controller.delegate = [RACSubject subject];

//订阅代理
[controller.delegate subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
[self.navigationController pushViewController:controller animated:YES];
}

//ViewControllerII.h
@interface ViewControllerII : UIViewController
@property (nonatomic, strong) RACSubject *delegate;
@end

@implementation ViewControllerII

- (IBAction)onDismiss:(id)sender
{
//RAC回调
[self.delegate sendNext:@"++ViewControllerII Closed~"];
[self.navigationController popViewControllerAnimated:YES];
}
@end

RACSubject是信号的子类,我们在 A 中订阅 B 中的信号,并在 B 中向 A 中订阅者发送消息,以此实现代理的功能。

小结

综上,RAC在对异步行为进行封装时所做的工作主要为:

  • 将这些行为封装到信号的 block 中(有些只是调用原生接口);
  • 我们只须订阅此信号就能以一种统一的方式实现业务代码的内聚;
  • 信号向订阅者发送完事件之后随即清理资源和引用;

RAC将这些异步行为的创建、监听、业务回调集中在一片代码区域或交由RAC内部实现,这符合高内聚的设计思想,值得学习和借鉴~


相关参考:

#©RAC-Github


RAC-异步行为
https://davidlii.cn/2021/05/25/RAC-async.html
作者
Davidli
发布于
2021年5月25日
许可协议