写点什么

从代理到 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:09722

评论

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

一文掌握 Docker 技术体系

博文视点Broadview

TiFlash 源码阅读(一) TiFlash 存储层概览

PingCAP

STM32+华为云IOT制作酒驾监控系统:上车就监控

华为云开发者联盟

mqtt stm32 华为云IoT 酒驾 酒驾监控系统

为什么 Rust 是 Stack Overflow 最受欢迎语言?

非凸科技

c++ rust 性能 Stack Overflow 内存安全

好的每日站会,应该这么开 | 敏捷开发落地指南

阿里云云效

云计算 阿里云 敏捷开发 研发敏捷 每日站会

帮助文档——助客户快速了解您的产品如何使用

小炮

帮助文档

分享一个JDK批量异步任务工具Completion Service,超好用

华为云开发者联盟

jdk 线程 异步 CompletionService 批量异步任务工具

IOS技术分享| ARCallPlus 开源项目(二)

anyRTC开发者

ios 开源 音视频 移动开发 呼叫邀请

基于场景文字的多模态融合的图像分类

华为云开发者联盟

计算机视觉 图像分类 场景文本 图像视觉 多模态融合分析

无聊科技正经事周刊(第4期):理性囤货与人工智能预测

潘大壮

程序员 科技 行业趋势 科技周刊

等保2.0国家标准是什么?与等保1.0有啥变化?

行云管家

网络安全 等保 等级保护 等保2.0

阿里云机器学习PAI开源中文NLP算法框架EasyNLP,助力NLP大模型落地

阿里云大数据AI技术

深度学习 nlp 开源技术

活动报名|OpenHarmony 战“码”先锋,PR征集令

OpenHarmony开发者

OpenHarmony

深入微服务-SpringCloud调用组件Feign

janyxe

spring Spring Cloud Feign OpenFegin

架构实战营 - 方案设计文档模板

华仔

架构实战营 文档模板 方案设计

Enhanced SWAP内存管理 OpenHarmony构建新的内存管理优化方案——ESWAP

科技汇

不要再焦虑了,进大厂真的没你想象的那么困难

Java架构追梦

Java java面试 后端开发

资讯|WebRTC M99 更新

网易云信

WebRTC

如何完成与龙蜥操作系统的兼容验证,看这里! | 一周动态

OpenAnolis小助手

操作系统 龙蜥社区 一周动态

解析数仓OLAP函数:ROLLUP、CUBE、GROUPING SETS

华为云开发者联盟

Rollup GaussDB(DWS) cube GROUPING SETS OLAP函数

毕业总结

孙强

#架构实战营

圈重点!一图读懂OpenHarmony技术日

OpenHarmony开发者

OpenHarmony 技术日

面试突击43:lock、tryLock、lockInterruptibly有什么区别?

王磊

Java 面试题

短短6小时,AI设计出40000种毒气分子,很多毒性远超战用神经毒剂

图灵教育

AI

OpenHarmony技术日全面解读3.1 Release版本 系统基础能力再升级

OpenHarmony开发者

OpenHarmony OpenHarmony 3.1 Release

得物技术浅谈深入浅出的Redis分布式锁

得物技术

redis 分布式 分布式锁 CAP 一致性

稳定性领导者!阿里云获得信通院多项系统稳定性最高级认证

阿里巴巴云原生

阿里云 云原生 可观测 性能压测 获奖

浅谈小程序开源业务架构建设之路

百度Geek说

小程序自动化测试框架原理剖析

百度Geek说

小程序 百度

分布式数据对象:超级终端的"全局变量"

科技汇

TiDB 查询优化及调优系列(二)TiDB 查询计划简介

PingCAP

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