NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

从代理到 RACSignal

  • 2019-12-06
  • 本文字数:8232 字

    阅读完需:约 27 分钟

从代理到 RACSignal

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


RACDelegateProxy

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


Objective-C


- (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 根类:


Objective-C


@interface RACDelegateProxy : NSObject
@end
复制代码


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



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


Objective-C


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


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


Objective-C


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: 方法:


Objective-C


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


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


Objective-C


- (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:


Objective-C


- (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 的实现还是比较复杂的。


Objective-C


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

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


Objective-C


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

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


Objective-C


typeEncoding = RACSignatureForUndefinedSelector(selector);class_addMethod(class, selector, _objc_msgForward, typeEncoding);
复制代码


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


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


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 类型的。



生成了类型编码之后,由于我们并没有在当前类中找到该选择子对应的方法,所以会使用 class_addMethod 为当前类提供一个方法的实现,直接将当前选择子的实现改为 _objc_msgForward


targetMethod == NULL && protocol != NULL

当类中不存在当前选择子对应的方法 targetMethod,但是向当前函数中传入了协议时,我们会尝试从协议中获取方法描述:


Objective-C


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);
复制代码


这里会使用 protocol_getMethodDescription 两次从协议中获取可选和必须实现的方法的描述,并从结构体中拿出类型编码,最后为类添加这个之前不存在的方法:



在这种情况下,其最后的结果与上一种的完全相同,因为它们都是对不存在该方法,只需要获得方法的类型编码并将实现添加为 _objc_msgForward,交给消息转发流程进行处理即可。

targetMethod != NULL

在目标方法的实现不为空并且它的实现并不是 _objc_msgForward 时,我们就会进入以下流程修改原有方法的实现:


Objective-C


const char *typeEncoding = method_getTypeEncoding(targetMethod);
class_addMethod(class, aliasSelector, method_getImplementation(targetMethod), typeEncoding);class_replaceMethod(class, selector, _objc_msgForward, method_getTypeEncoding(targetMethod));
复制代码


同样,我们需要获得目标方法的方法签名、添加 aliasSelector 这个新方法,最后在修改原方法的实现到 _objc_msgForward



上图展示了在目标方法不为空并且其实现不为 _objc_msgForward 时,NSObjectRACSignalForSelector 是如何修改原方法实现的。

调剂类的方法

NSObjectRACSignalForSelector 在修改原选择子方法实现的之前就已经修改了当前类很多方法的实现:


  • -methodSignatureForSelector:

  • -class

  • -respondsToSelector

  • -forwardInvocation:


整个调剂方法的过程 RACSwizzleClass 还是比较复杂的,我们可以分三部分看下面的代码:


Objective-C


static Class RACSwizzleClass(NSObject *self) {  Class statedClass = self.class;  Class baseClass = object_getClass(self);
NSString *className = NSStringFromClass(baseClass); const char *subclassName = [className stringByAppendingString:RACSubclassSuffix].UTF8String; Class subclass = objc_getClass(subclassName);
if (subclass == nil) { subclass = objc_allocateClassPair(baseClass, subclassName, 0); if (subclass == nil) return nil;
RACSwizzleForwardInvocation(subclass); RACSwizzleRespondsToSelector(subclass); RACSwizzleGetClass(subclass, statedClass); RACSwizzleGetClass(object_getClass(subclass), statedClass); RACSwizzleMethodSignatureForSelector(subclass);
objc_registerClassPair(subclass); } object_setClass(self, subclass); return subclass;}
复制代码


  1. 从当前类 RACDelegateProxy 衍生出一个子类 RACDelegateProxy_RACSelectorSignal

  2. 调用各种 RACSwizzleXXX 方法修改当前子类的一些表现;

  3. RACDelegateProxy 对象的类设置成自己,这样就会在查找方法时,找到 RACDelegateProxy_RACSelectorSignal 中的实现;


在修改的几个方法中最重要的就是 -forwardInvocation:


Objective-C


static void RACSwizzleForwardInvocation(Class class) {  SEL forwardInvocationSEL = @selector(forwardInvocation:);  Method forwardInvocationMethod = class_getInstanceMethod(class, forwardInvocationSEL);
void (*originalForwardInvocation)(id, SEL, NSInvocation *) = NULL; if (forwardInvocationMethod != NULL) { originalForwardInvocation = (__typeof__(originalForwardInvocation))method_getImplementation(forwardInvocationMethod); }
id newForwardInvocation = ^(id self, NSInvocation *invocation) { BOOL matched = RACForwardInvocation(self, invocation); if (matched) return;
if (originalForwardInvocation == NULL) { [self doesNotRecognizeSelector:invocation.selector]; } else { originalForwardInvocation(self, forwardInvocationSEL, invocation); } };
class_replaceMethod(class, forwardInvocationSEL, imp_implementationWithBlock(newForwardInvocation), "v@:@");}
复制代码


这个方法中大部分的内容都是平淡无奇的,在新的 -forwardInvocation: 方法中,执行的 RACForwardInvocation 是实现整个消息转发的关键内容:


Objective-C


static BOOL RACForwardInvocation(id self, NSInvocation *invocation) {  SEL aliasSelector = RACAliasForSelector(invocation.selector);  RACSubject *subject = objc_getAssociatedObject(self, aliasSelector);
Class class = object_getClass(invocation.target); BOOL respondsToAlias = [class instancesRespondToSelector:aliasSelector]; if (respondsToAlias) { invocation.selector = aliasSelector; [invocation invoke]; }
if (subject == nil) return respondsToAlias;
[subject sendNext:invocation.rac_argumentsTuple]; return YES;}
复制代码


-rac_signalForSelector: 方法返回的 RACSignal 上接收到的参数信号,就是从这个方法发送过去的,新的实现 RACForwardInvocation 改变了原有的 selectoraliasSelector,然后使用 -invoke 完成该调用,而所有的参数会以 RACTuple 的方式发送到信号上。


像其他的方法 -respondToSelector: 等等,它们的实现就没有这么复杂并且重要了:


Objective-C


id newRespondsToSelector = ^ BOOL (id self, SEL selector) {    Method method = rac_getImmediateInstanceMethod(class, selector);
if (method != NULL && method_getImplementation(method) == _objc_msgForward) { SEL aliasSelector = RACAliasForSelector(selector); if (objc_getAssociatedObject(self, aliasSelector) != nil) return YES; }
return originalRespondsToSelector(self, respondsToSelectorSEL, selector);};
复制代码


rac_getImmediateInstanceMethod 从当前类获得方法的列表,并从中找到与当前 selector 同名的方法 aliasSelector,然后根据不同情况判断方法是否存在。


class 的修改,是为了让对象对自己的身份『说谎』,因为我们子类化了 RACDelegateProxy,并且重新设置了对象的类,将所有的方法都转发到了这个子类上,如果不修改 class 方法,那么当开发者使用它自省时就会得到错误的类,而这是我们不希望看到的。


Objective-C


static void RACSwizzleGetClass(Class class, Class statedClass) {  SEL selector = @selector(class);  Method method = class_getInstanceMethod(class, selector);  IMP newIMP = imp_implementationWithBlock(^(id self) {    return statedClass;  });  class_replaceMethod(class, selector, newIMP, method_getTypeEncoding(method));}
复制代码


在最后我们会对获得方法签名的 -methodSignatureForSelector: 方法进行修改:


Objective-C


IMP newIMP = imp_implementationWithBlock(^(id self, SEL selector) {    Class actualClass = object_getClass(self);    Method method = class_getInstanceMethod(actualClass, selector);    if (method == NULL) {        struct objc_super target = {            .super_class = class_getSuperclass(class),            .receiver = self,        };        NSMethodSignature * (*messageSend)(struct objc_super *, SEL, SEL) = (__typeof__(messageSend))objc_msgSendSuper;        return messageSend(&target, @selector(methodSignatureForSelector:), selector);    }
char const *encoding = method_getTypeEncoding(method); return [NSMethodSignature signatureWithObjCTypes:encoding];});
复制代码


在方法不存在时,通过 objc_msgSendSuper 调用父类的 -methodSignatureForSelector: 方法获取方法签名。

方法调用的过程

在一般情况下,Objective-C 中某一消息被发送到一个对象时,它会先获取当前对象对应的类,然后从类的选择子表查找该方法对应的实现并执行。



与正常的方法实现查找以及执行过程的简单不同,如果我们对某一个方法调用了 -rac_signalForSelector: 方法,那么对于同一个对象对应的类的所有方法,它们的执行过程会变得非常复杂:



  1. 由于当前对象对应的类已经被改成了 Subclass,即 Class_RACSelectorSignal,所以会在子类中查找方法的实现;

  2. 方法对应的实现已经被改成了 -forwardInvocation:,会直接进入消息转发流程中处理;

  3. 根据传入的选择子获取同名选择子 rac_alias_selector

  4. 拿到当前 NSInvocation 对象中 target 的类,判断是否可以响应该选择子;

  5. NSInvocation 对象中的选择子改为 rac_alias_selector 并执行其实现;

  6. NSInvocation 对象中获取参数并打包成 RACTuple,以 next 消息的形式发送到持有的 RACSubject 热信号上;


这时所有的订阅者才会在该方法被调用时收到消息,完成相应的任务。

总结

ReactiveCocoa 使用了一种非常神奇的办法把原有的代理模式成功的桥接到 RACSignal 的世界中,并为我们提供了 RACDelegateProxy 这一接口,能够帮助我们以信号的形式监听所有的代理方法,可以用 block 的形式去代替原有的方法,为我们减少一些工作量。

References


Github Repo:iOS-Source-Code-Analyze


Source: https://draveness.me/racdelegateproxy


本文转载 Draveness 技术博客。


原文链接:https://draveness.me/racdelegateproxy


2019-12-06 11:09545

评论

发布
暂无评论
发现更多内容

「趣学前端」给不懂技术的朋友简单演示,代码是怎么被编写出来的

叶一一

JavaScript 前端 10月月更

Python进阶(二十二)Python3使用PyMysql连接mysql数据库

No Silver Bullet

Python3 MySQL数据库 pymysql 10月月更

Python进阶(二十三)Django使用pymysql连接MySQL数据库做增删改查

No Silver Bullet

Python django MySQL数据库 pymysql 10月月更

什么是数据产品经理?数据产品经理与传统产品经理有什么区别?

雨果

数据产品经理

跟着卷卷龙一起学Camera--双摄02

卷卷龙

ISP camera 10月月更

树莓派4B安装64位Linux(不用显示器键盘鼠标)

程序员欣宸

树莓派 10月月更

跟着卷卷龙一起学Camera--双摄01

卷卷龙

ISP camera 10月月更

Clickhouse:delete提交成功,数据还在

Ken

Clickhouse

挑战30天学完 Python:Day4数据类型-字符串str

MegaQi

挑战30天学完Python 10月月更

什么是Scrum?Scrum的核心要点和精髓

laofo

Scrum 研发效能 敏捷研发

Spring Batch 可以在一个 Step 中有多个 Tasklet 吗

HoneyMoose

大数据ELK(二十五):添加Elasticsearch数据源

Lansonli

elasticsearch 10月月更

Java基础(六)| Debug模式及基础练习

timerring

Java debug 10月月更

测试需求平台5-Blueprint优化与首个vue页搭建

MegaQi

测试平台开发教程 10月月更

微信朋友圈架构图

Johnny

架构实战训练营9期

Spring Batch 事务限制

HoneyMoose

「CSS畅想」周期性事情怕忘,来看看一个月内都安排在哪天

叶一一

CSS JavaScript 前端 10月月更

正在消失的机器视觉公司

脑极体

有人意图取代SQL,你同意吗?

雨果

sql

Spring之核心容器

楠羽

笔记 SP【ring 10月月更

PriorityQueue源码解析(一)

知识浅谈

Priority Queue 10月月更

【LeetCode】两个链表的第一个重合节点Java题解

Albert

算法 LeetCode 10月月更

「趣学前端」日常浏览的页面为什么可以五颜六色

叶一一

CSS 前端 10月月更

跟着卷卷龙一起学Camera--DNG格式

卷卷龙

ISP camera 10月月更

如何快速对混合云环境进行安全合规检查

HummerCloud

云安全 上云合规 安全合规检测 10月月更

一个实际的例子学习 SAP BTP Java 应用的 @Before 注解使用方式

Jerry Wang

Java 云原生 Cloud SAP 10月月更

Java三大特性(一)—封装

共饮一杯无

Java 面向对象 10月月更

【从0到1学算法】5.Bubble Sort算法-下

Geek_65222d

10月月更

「趣学前端」日常浏览的页面是怎么实现出来的

叶一一

JavaScript 前端 10月月更

首轮Zepoch节点已售罄完结,你期待次轮吗?

鳄鱼视界

大数据ELK(二十六):探索数据(Discovery)

Lansonli

discovery 10月月更

从代理到 RACSignal_语言 & 开发_Draveness_InfoQ精选文章