8d470ef0a557dac0c9e5afdc9da09219
从代理到 RACSignal

ReactiveCocoa 将 Cocoa 中的 Target-Action、KVO、通知中心以及代理等设计模式都桥接到了 RAC 的世界中,我们在随后的几篇文章中会介绍 RAC 如何做到了上面的这些事情,而本篇文章会介绍 ReactiveCocoa 是如何把代理转换为信号的。

RACDelegateProxy

从代理转换成信号所需要的核心类就是 RACDelegateProxy,这是一个设计的非常巧妙的类;虽然在类的头文件中,它被标记为私有类,但是我们仍然可以使用 -initWithProtocol: 方法直接初始化该类的实例。

- (instancetype)initWithProtocol:(Protocol *)protocol {
    self = [super init];
    class_addProtocol(self.class, protocol);
    _protocol = protocol;
    return self;
}

从初始化方法中,我们可以看出 RACDelegateProxy 是一个包含实例变量 _protocol 的类:

在整个 RACDelegateProxy 类的实现中,你都不太能看出与这个实例变量 _protocol 的关系;稍微对 iOS 有了解的人可能都知道,在 Cocoa 中有一个非常特别的根类 NSProxy,而从它的名字我们也可以推断出来,NSProxy 一般用于实现代理(主要是对消息进行转发),但是 ReactiveCocoa 中这个 delegate 的代理 RACDelegateProxy 并没有继承这个 NSProxy 根类:

@interface RACDelegateProxy : NSObject

@end

那么 RACDelegateProxy 是如何作为 Cocoa 中组件的代理,并为原生组件添加 RACSignal 的支持呢?我们以 UITableView 为例来展示 RACDelegateProxy 是如何与 UIKit 组件互动的,我们需要实现的是以下功能:

在点击所有的 UITableViewCell 时都会自动取消点击状态,通常情况下,我们可以直接在代理方法 -tableView:didSelectRowAtIndexPath: 中执行 -deselectRowAtIndexPath:animated: 方法:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
}

使用信号的话相比而言就比较麻烦了:

RACDelegateProxy *proxy = [[RACDelegateProxy alloc] initWithProtocol:@protocol(UITableViewDelegate)];
objc_setAssociatedObject(self, _cmd, proxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
proxy.rac_proxiedDelegate = self;
[[proxy rac_signalForSelector:@selector(tableView:didSelectRowAtIndexPath:)]
 subscribeNext:^(RACTuple *value) {
     [value.first deselectRowAtIndexPath:value.second animated:YES];
 }];
self.tableView.delegate = (id<UITableViewDelegate>)proxy;
  • 1.初始化 RACDelegateProxy 实例,传入 UITableViewDelegate 协议,并将实例存入视图控制器以确保实例不会被意外释放造成崩溃;
  • 2.设置代理的 rac_proxiedDelegate 属性为视图控制器;
  • 3.使用 -rac_signalForSelector: 方法生成一个 RACSignal,在 -tableView:didSelectRowAtIndexPath: 方法调用时将方法的参数打包成 RACTuple 向信号中发送新的 next 消息;
  • 4.重新设置 UITableView 的代理;

UITableViewDelgate 中的代理方法执行时,实际上会被 RACDelegateProxy 拦截,并根据情况决定是处理还是转发:

如果 RACDelegateProxy 实现了该代理方法就会交给它处理,如:-tableView:didSelectRowAtIndexPath:;否则,当前方法就会被转发到原 delegate 上,在这里就是 UIViewController 对象。

RACDelegateProxy 中有两个值得特别注意的问题,一是 RACDelegateProxy 是如何进行消息转发的,有事如何将自己无法实现的消息交由原代理处理,第二是 RACDelegateProxy 如何通过方法 -rac_signalForSelector:在原方法调用时以 RACTuple 的方式发送到 RACSignal 上。

消息转发的实现

首先,我们来看 RACDelegateProxy 是如何在无法响应方法时,将方法转发给原有的代理的;RACDelegateProxy 通过覆写几个方法来实现,最关键的就是 -forwardInvocation:方法:

- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:self.rac_proxiedDelegate];
}

当然,作为消息转发流程的一部分 -methodSignatureForSelector: 方法也需要在 RACDelegateProxy 对象中实现:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
    struct objc_method_description methodDescription = protocol_getMethodDescription(_protocol, selector, NO, YES);
    if (methodDescription.name == NULL) {
        methodDescription = protocol_getMethodDescription(_protocol, selector, YES, YES);
        if (methodDescription.name == NULL) return [super methodSignatureForSelector:selector];
    }
    return [NSMethodSignature signatureWithObjCTypes:methodDescription.types];
}

我们会从协议的方法中尝试获取其中的可选方法和必须实现的方法,最终获取方法的签名 NSMethodSignature 对象。

整个方法决议和消息转发的过程如下图所示,在整个方法决议和消息转发的过程中 Objective-C 运行时会再次提供执行该方法的机会。

例子中的代理方法最后也被 -forwardInvocation: 方法成功的转发到了 UITableView 的原代理上。

从代理到信号

在 RACDelegateProxy 中的另一个非常神奇的方法就是将某一个代理方法转换成信号的 -signalForSelector::

- (RACSignal *)signalForSelector:(SEL)selector {
    return [self rac_signalForSelector:selector fromProtocol:_protocol];
}

- (RACSignal *)rac_signalForSelector:(SEL)selector fromProtocol:(Protocol *)protocol {
    return NSObjectRACSignalForSelector(self, selector, protocol);
}

该方法会在传入的协议方法被调用时,将协议方法中的所有参数以 RACTuple 的形式发送到返回的信号上,使用者可以通过订阅这个信号来获取所有的参数;而方法 NSObjectRACSignalForSelector 的实现还是比较复杂的。

static RACSignal *NSObjectRACSignalForSelector(NSObject *self, SEL selector, Protocol *protocol) {
    SEL aliasSelector = RACAliasForSelector(selector);

    RACSubject *subject = objc_getAssociatedObject(self, aliasSelector);
    if (subject != nil) return subject;

    Class class = RACSwizzleClass(self);
    subject = [RACSubject subject];
    objc_setAssociatedObject(self, aliasSelector, subject, OBJC_ASSOCIATION_RETAIN);

    Method targetMethod = class_getInstanceMethod(class, selector);
    if (targetMethod == NULL) {
        const char *typeEncoding;
        if (protocol == NULL) {
            typeEncoding = RACSignatureForUndefinedSelector(selector);
        } else {
            struct objc_method_description methodDescription = protocol_getMethodDescription(protocol, selector, NO, YES);
            if (methodDescription.name == NULL) {
                methodDescription = protocol_getMethodDescription(protocol, selector, YES, YES);
            }
            typeEncoding = methodDescription.types;
        }
        class_addMethod(class, selector, _objc_msgForward, typeEncoding);
    } else if (method_getImplementation(targetMethod) != _objc_msgForward) {
        const char *typeEncoding = method_getTypeEncoding(targetMethod);

        class_addMethod(class, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
        class_replaceMethod(class, selector, _objc_msgForward, method_getTypeEncoding(targetMethod));
    }
    return subject;
}

这个 C 函数总共做了两件非常重要的事情,第一个是将传入的选择子对应的实现变为 _objc_msgForward,也就是在调用该方法时,会直接进入消息转发流程,第二是用 RACSwizzleClass 调剂当前类的一些方法。

从 selector 到 _objc_msgForward

我们具体看一下这部分代码是如何实现的,在修改选择子对应的实现之前,我们会先做一些准备工作:

SEL aliasSelector = RACAliasForSelector(selector);

RACSubject *subject = objc_getAssociatedObject(self, aliasSelector);
if (subject != nil) return subject;

Class class = RACSwizzleClass(self);

subject = [RACSubject subject];
objc_setAssociatedObject(self, aliasSelector, subject, OBJC_ASSOCIATION_RETAIN);

Method targetMethod = class_getInstanceMethod(class, selector);

1.获取选择子的别名,在这里我们通过为选择子加前缀 rac_alias_ 来实现;
2.尝试以 rac_alias_selector 为键获取一个热信号 RACSubject;
3.使用 RACSwizzleClass 调剂当前类的一些方法(我们会在下一节中介绍);
4.从当前类中获取目标方法的结构体 targetMethod;

在进行了以上的准备工作之后,我们就开始修改选择子对应的实现了,整个的修改过程会分为三种情况:

下面会按照这三种情况依次介绍在不同情况下,如何将对应选择子的实现改为 _objc_msgForward 完成消息转发的。

targetMethod == NULL && protocol == NULL

在找不到选择子对应的方法并且没有传入协议时,这时执行的代码最为简单:

typeEncoding = RACSignatureForUndefinedSelector(selector);
class_addMethod(class, selector, _objc_msgForward, typeEncoding);

我们会通过 RACSignatureForUndefinedSelector 生成一个当前方法默认的类型编码。

对类型编码不了解的可以阅读苹果的官方文档 Type Encodings · Apple Developer,其中详细解释了类型编码是什么,它在整个 Objective-C 运行时有什么作用。

static const char *RACSignatureForUndefinedSelector(SEL selector) {
    const char *name = sel_getName(selector);
    NSMutableString *signature = [NSMutableString stringWithString:@"v@:"];

    while ((name = strchr(name, ':')) != NULL) {
        [signature appendString:@"@"];
        name++;
    }

    return signature.UTF8String;
}

该方法在生成类型编码时,会按照 : 的个数来为 v@: 这个类型编码添加 @ 字符;简单说明一下它的意思,ReactiveCocoa 默认所有的方法的返回值类型都为空 void,都会传入 self 以及当前方法的选择子 SEL,它们的类型编码可以在下图中找到,分别是 v@:;而 @ 代表 id 类型,也就是我们默认代理方法中的所有参数都是 NSObject 类型的。

top Created with Sketch.