ReactiveObjC-从RAC宏说起

这里说的ReactiveObjC,就是ReactiveCocoaObjective-C版本:

https://github.com/ReactiveCocoa/ReactiveObjC

从一个小例子开始

下面的代码,实现的效果是,当用户名输入框密码输入框都有内容时,登录按钮才会变得可用,否则,不可用。

1
2
3
4
5
6
7
8
9
NSArray *signals = @[
self.usernameTF.rac_textSignal,
self.passwordTF.rac_textSignal,
];
RAC(self.loginBtn, enabled) = [RACSignal combineLatest:signals reduce:^id _Nonnull (NSString *username, NSString *password) {
username = [username stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
password = [password stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
return @(username.length > 0 && password.length > 0);
}];

以上代码在经过预处理后,会被转化为:

1
2
3
4
5
6
7
8
9
NSArray *signals = @[
self.usernameTF.rac_textSignal,
self.passwordTF.rac_textSignal,
];
[[RACSubscriptingAssignmentTrampoline alloc] initWithTarget:(self.loginBtn) nilValue:(((void *)0))][@(((void)(__objc_no && ((void)self.loginBtn.enabled, __objc_no)), "enabled"))] = [RACSignal combineLatest:signals reduce:^id _Nonnull (NSString *username, NSString *password) {
username = [username stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
password = [password stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
return @(username.length > 0 && password.length > 0);
}];

将其中的[@(((void)(__objc_no && ((void)self.loginBtn.enabled, __objc_no)), "enabled"))]简化一下,就变成了:

1
2
3
4
5
6
7
8
9
NSArray *signals = @[
self.usernameTF.rac_textSignal,
self.passwordTF.rac_textSignal,
];
[[RACSubscriptingAssignmentTrampoline alloc] initWithTarget:(self.loginBtn) nilValue:(((void *)0))][@"enabled"] = [RACSignal combineLatest:signals reduce:^id _Nonnull (NSString *username, NSString *password) {
username = [username stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
password = [password stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
return @(username.length > 0 && password.length > 0);
}];

上面的代码实际上就是调用RACSubscriptingAssignmentTrampoline中的- setObject:forKeyedSubscript:方法:

1
2
3
4
5
[[[RACSubscriptingAssignmentTrampoline alloc] initWithTarget:(self.loginBtn) nilValue:((void *)0)] setObject:[RACSignal combineLatest:signals reduce:^id _Nonnull (NSString *username, NSString *password) {
username = [username stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
password = [password stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
return @(username.length > 0 && password.length > 0);
}] forKeyedSubscript:@"enabled"];

为了弄明白,上面的代码是如何展开的,就需要厘清RAC宏。

RAC宏

在厘清RAC宏之前,需要先弄明白其所涉及的所有宏定义。

先来看看它们是如何被定义的。

所有涉及的宏

RAC.png

在上面的图中,可以清晰地看出每个宏的定义,及其实现所依赖的宏。『请在新标签页新窗口中打开图像,以查看高清图』

宏定义释疑

描述
metamacro_concat_(A, B) 以字符串的形式连接A与B。示例:metamacro_concat_(1, 2),其结果为12
metamacro_concat(A, B) 同上
keypath1(PATH) 在它的实现中,逗号表达式中最前面的NO,会在运行时,导致&&之后的((void)PATH, NO))不会执行,节省了开销。示例:keypath1(self.enabled),结果为(((void)(NO && ((void)self.enabled, NO)), strchr(# self.enabled, '.') + 1)),也就是(((void)(NO && ((void)self.enabled, NO)), strchr("self.enabled", '.') + 1))(((void)(NO && ((void)self.enabled, NO)), ".enabled" + 1))(NO, "enabled")"enabled"。注意,这里的PATH,必须要有『.』,否则在运行的时候会导致崩溃:比如keypath1(enabled),会变成(((void)(NO && ((void)enabled, NO)), strchr(# enabled, '.') + 1)),导致找不到『.』,strchr(# enabled, '.')的结果为NULL,最终结果为NULL + 1,使用时会导致出现EXC_BAD_ACCESS
keypath2(OBJ, PATH) 在它的实现中,OBJ.PATH,会在编译期,进行相关的校验,如果OBJ没有对应的PATH,会出现编译错误;逗号表达式中最前面的NO,会在运行时,导致&&之后的((void)OBJ.PATH, NO))不会执行,节省了开销。示例:keypath2(self.loginBtn, enabled),结果为(((void)(NO && ((void)self.loginBtn.enabled, NO)), # enabled)),也就是(NO, "enabled")"enabled"
keypath(...) 会在编译期,对key path进行校验。返回由可变参数构成的key path。示例:keypath(self.loginBtn, enabled),结果为"enabled"
metamacro_at(N, ...) 返回索引为N的可变参数(索引以0开始)。必须至少提供N + 1个可变参数,N为区间[0, 20]中的整数。其展开后就是metamacro_atN(...),这里的N就是区间[0, 20]中的一个整数。示例:metamacro_at(3, 1, 3, 5, 7),因为第一个参数为3,因此必须至少提供4个可变参数(这里的可变参数1、3、5、7,总数量为4),其中,7的索引为3,因此,结果为7。来看看为什么结果是7:把metamacro_at(3, 1, 3, 5, 7)展开后就是metamacro_concat(metamacro_at, 3)(__VA_ARGS__) ,其中metamacro_concat(metamacro_at, 3)的结果为metamacro_at3,所以结果为metamacro_at3(1, 3, 5, 7)。在metamacros.h文件中,有20个metamacro_at扩展,分别是metamacro_at1metamacro_at2metamacro_at19metamacro_at20,它们都使用了metamacro_head,其定义为#define metamacro_head(...) metamacro_head_(__VA_ARGS__, 0)metamacro_head_的定义是#define metamacro_head_(FIRST, ...) FIRST。再看看metamacro_at3的定义:#define metamacro_at3(_0, _1, _2, ...) metamacro_head(__VA_ARGS__),因此将metamacro_at3(1, 3, 5, 7)一步步展开,就是metamacro_head(7)metamacro_head_(7, 0),结果就是7
metamacro_argcount(...) 返回所传入的参数的总个数。必须至少提供1个参数。示例:metamacro_argcount(1),展开后就是metamacro_at(20, 1, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1),其结果为索引为20的可变参数,也就是1(注意,metamacro_at的第一个参数20,并不属于可变参数的一部分)
metamacro_if_eq(A, B) 检查A、B是否相等,如果相等,就展开后面的第一个参数列表,否则就展开后面的第二个参数列表
RAC_(TARGET, KEYPATH, NILVALUE) 展开后就是[[RACSubscriptingAssignmentTrampoline alloc] initWithTarget:(TARGET) nilValue:(NILVALUE)][@keypath(TARGET, KEYPATH)]。示例:RAC_(self.loginBtn, enabled, nil),其结果就是[[RACSubscriptingAssignmentTrampoline alloc] initWithTarget:(self.loginBtn) nilValue:(nil)][@keypath(self.loginBtn, enabled)][[RACSubscriptingAssignmentTrampoline alloc] initWithTarget:(self.loginBtn) nilValue:(nil)][@"enabled"]
RAC(TARGET, ...) 因为其使用了metamacro_if_eq,因此,不是展开(RAC_(TARGET, __VA_ARGS__, nil)),就是展开(RAC_(TARGET, __VA_ARGS__))。其可变参数的个数,只能是1或2,如果是1个可变参数,就对应KEYPATH;如果是2个可变参数,就对应KEYPATHNILVALUE

ReactiveObjC-从RAC宏说起
https://daniate.github.io/2016/10/31/ReactiveObjC-从RAC宏说起/
作者
Daniate
发布于
2016年10月31日
许可协议