从使用 Combine 拦截 delegate 看 Runtime 消息转发
CombineCocoa 已有一段时间未更新,但其中许多代码仍能正常工作,比如 Delegate 的这部分。
今天我们先从这部分代码入手,看看如何使用 Combine 拦截 UIKit 中的各类 delegate 对象,再深入探讨其背后的 Runtime 实现。
使用 Combine 拦截 delegate
下面的代码和 CombineCocoa 中的不完全一致,我会做一些调整以及格式上的修改。
首先,我们需要明确最终想要实现的效果,这是做框架时常用的逆向思考方法。比如我们想要一个这样的方法去调用:
// 代替 `UITableViewDelegate.tableView(_:willDisplay:forRowAt:)`
tableView.willDisplayCellPublisher.sink { ... }
/// Combine wrapper for tableView(_:willDisplay:forRowAt:)`
public var willDisplayCellPublisher: AnyPublisher<(cell: UITableViewCell, indexPath: IndexPath), Never> {
return /* 一个 Publisher 对象 */
}
willDisplayCellPublisher
的定义位于 CombineCocoa 仓库的 UITableView+Combine.swift 文件。关键代码如下所示:
@available(iOS 13.0, *)
public extension UITableView {
/// Combine wrapper for `tableView(_:willDisplay:forRowAt:)`
var willDisplayCellPublisher: AnyPublisher<(cell: UITableViewCell, indexPath: IndexPath), Never> {
let selector = #selector(UITableViewDelegate.tableView(_:willDisplay:forRowAt:))
return delegateProxy.interceptSelectorPublisher(selector)
.map { ($0[1] as! UITableViewCell, $0[2] as! IndexPath) }
.eraseToAnyPublisher()
}
override var delegateProxy: DelegateProxy {
TableViewDelegateProxy.createDelegateProxy(for: self)
}
}
@available(iOS 13.0, *)
private class TableViewDelegateProxy: DelegateProxy, UITableViewDelegate, DelegateProxyType {
func setDelegate(to object: UITableView) {
object.delegate = self
}
}
实现看起来稍显复杂,我们逐步分析,先来看 willDisplayCellPublisher
这个计算属性。从 interceptSelectorPublisher
方法的字面含义上来看,猜测它是拦截了 #selector(UITableViewDelegate.tableView(_:willDisplay:forRowAt:))
方法,将相关调用封装为 Publisher 供外部订阅。
delegateProxy
属性本质上是 TableViewDelegateProxy
类型,其直接父类为 DelegateProxy
,而它的父类最终是 ObjcDelegateProxy
,这是一个纯 Objective‑C 类型。
让我们先跳过 DelegateProxy
,看看 ObjcDelegateProxy
都干了些什么。
在看之前我们应该就能想到,它里面一定涉及到了 ObjC runtime 的使用,否则没有必要特地使用 ObjC
相关代码位于 Runtime 目录下。
// Runtime/include/ObjcDelegateProxy.h
#import <Foundation/Foundation.h>
@interface ObjcDelegateProxy: NSObject
@property (nonnull, strong, atomic, readonly) NSSet <NSValue *> *selectors;
/// 当拦截到对应选择子时被调用,参数为方法名和参数列表
- (void)interceptedSelector:(SEL _Nonnull)selector arguments:(NSArray * _Nonnull)arguments;
- (BOOL)respondsToSelector:(SEL _Nonnull)aSelector;
- (BOOL)canRespondToSelector:(SEL _Nonnull)selector;
@end
从接口设计上来说这个类型暴露了比较多的方法,其中 interceptedSelector
方法是在 willDisplayCellPublisher
中用到的那个,一会儿重点关注一下。
再看看 .m 文件中的实现:
我省略了查找方法选择子相关方法的实现,本文重点关注方法转发相关的部分。
/// Runtime/ObjcDelegateProxy.m
#import <Foundation/Foundation.h>
#import "include/ObjcDelegateProxy.h"
#import <objc/runtime.h>
/// 辅助宏:将对象包装成 NSValue,以非保留方式持有
#define OBJECT_VALUE(object) [NSValue valueWithNonretainedObject:(object)]
/// 全局静态字典:Key 为代理类,Value 为该类声明的所有拦截选择子集合
static NSMutableDictionary<NSValue *, NSSet<NSValue *> *> *allSelectors;
@implementation ObjcDelegateProxy
- (NSSet *)selectors {
return allSelectors[OBJECT_VALUE(self.class)];
}
+ (void)initialize
{
@synchronized (ObjcDelegateProxy.class) {
if (!allSelectors) {
allSelectors = [NSMutableDictionary new];
}
// 获取本类及其父类中所有 returnType 为 void 的方法选择子
allSelectors[OBJECT_VALUE(self)] = [self selectorsOfClass:self
withEncodedReturnType:[NSString stringWithFormat:@"%s", @encode(void)]];
}
}
- (BOOL)respondsToSelector:(SEL _Nonnull)aSelector {
return [super respondsToSelector:aSelector] || [self canRespondToSelector:aSelector];
}
- (BOOL)canRespondToSelector:(SEL _Nonnull)selector {
for (id current in allSelectors[OBJECT_VALUE(self.class)]) {
if (selector == (SEL) [current pointerValue]) {
return true;
}
}
return false;
}
- (void)interceptedSelector:(SEL _Nonnull)selector arguments:(NSArray * _Nonnull)arguments {
// 默认实现,子类可重写以处理拦截到的方法
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
NSArray * _Nonnull arguments = unpackInvocation(anInvocation);
[self interceptedSelector:anInvocation.selector arguments:arguments];
}
/// 从一个 NSInvocation 对象中提取出参数值,并包装为一个 NSArray<id> 返回
NSArray * _Nonnull unpackInvocation(NSInvocation * _Nonnull invocation) {
NSUInteger numberOfArguments = invocation.methodSignature.numberOfArguments;
NSMutableArray *arguments = [NSMutableArray arrayWithCapacity:numberOfArguments - 2];
// 忽略前两个参数:self 和 _cmd(Objective-C 的默认前两个参数)
for (NSUInteger index = 2; index < numberOfArguments; ++index) {
const char *argumentType = [invocation.methodSignature getArgumentTypeAtIndex:index];
// Skip const type qualifier.
if (argumentType[0] == 'r') {
argumentType++;
}
// 辅助宏:判断参数类型
#define isArgumentType(type) \
strcmp(argumentType, @encode(type)) == 0
// 辅助宏:提取基础数值类型参数,并转为 NSNumber
#define extractTypeAndSetValue(type, value) \
type argument = 0; \
[invocation getArgument:&argument atIndex:index]; \
value = @(argument); \
id _Nonnull value;
// 引用类型或 block 类型,直接解包
if (isArgumentType(id) || isArgumentType(Class) || isArgumentType(void (^)(void))) {
__unsafe_unretained id argument = nil;
[invocation getArgument:&argument atIndex:index];
value = argument;
}
// 以下全部是基本类型,统一转为 NSNumber
else if (isArgumentType(char)) {
extractTypeAndSetValue(char, value);
}
else if (isArgumentType(short)) {
extractTypeAndSetValue(short, value);
}
else if (isArgumentType(int)) {
extractTypeAndSetValue(int, value);
}
else if (isArgumentType(long)) {
extractTypeAndSetValue(long, value);
}
else if (isArgumentType(long long)) {
extractTypeAndSetValue(long long, value);
}
else if (isArgumentType(unsigned char)) {
extractTypeAndSetValue(unsigned char, value);
}
else if (isArgumentType(unsigned short)) {
extractTypeAndSetValue(unsigned short, value);
}
else if (isArgumentType(unsigned int)) {
extractTypeAndSetValue(unsigned int, value);
}
else if (isArgumentType(unsigned long)) {
extractTypeAndSetValue(unsigned long, value);
}
else if (isArgumentType(unsigned long long)) {
extractTypeAndSetValue(unsigned long long, value);
}
else if (isArgumentType(float)) {
extractTypeAndSetValue(float, value);
}
else if (isArgumentType(double)) {
extractTypeAndSetValue(double, value);
}
else if (isArgumentType(BOOL)) {
extractTypeAndSetValue(BOOL, value);
}
else if (isArgumentType(const char *)) {
extractTypeAndSetValue(const char *, value);
}
// 如果不是任何已知类型,就用 NSValue 包裹原始数据,比如 CGRect、CGSize
else {
NSUInteger size = 0;
NSGetSizeAndAlignment(argumentType, &size, NULL);
NSCParameterAssert(size > 0);
// 使用栈内存来存储未知数据
uint8_t data[size];
[invocation getArgument:&data atIndex:index];
value = [NSValue valueWithBytes:&data objCType:argumentType];
}
[arguments addObject:value];
}
return arguments;
}
+ (NSSet <NSValue *> *) selectorsOfClass: (Class _Nonnull __unsafe_unretained) class
withEncodedReturnType: (NSString *) encodedReturnType {
// 省略相关实现
}
+ (NSSet <NSValue *> *) selectorsOfProtocol: (Protocol * __unsafe_unretained) protocol
andEncodedReturnType: (NSString *) encodedReturnType {
// 省略相关实现
}
+ (NSSet <NSValue *> *) selectorsOfProtocolPointer: (Protocol * __unsafe_unretained * _Nullable) pointer
count: (NSInteger) count
andEncodedReturnType: (NSString *) encodedReturnType {
// 省略相关实现
}
+ (NSString *)encodedMethodReturnTypeForMethod: (struct objc_method_description) method {
return [[NSString alloc] initWithBytes:method.types length:1 encoding:NSASCIIStringEncoding];
}
@end
从源码得知,ObjcDelegateProxy
及其子类在初始化类型时调用 initialize
方法,方法内会以类型为 key,将所有返回类型为 void
的方法存储到 allSelectors
这个私有静态全局属性中。
respondsToSelector:
会先调用 canRespondToSelector:
,后者会在 allSelectors
中查找该 selector
。如果找到了,就返回 YES
,使得运行时认为此方法已被实现,从而跳过消息转发的后续流程。
实际情况肯定是不会实现相关方法,接着就会通过消息转发流程执行 forwardInvocation
方法,将通过 unpackInvocation
方法拿到的参数,和方法选择子一起交给 interceptedSelector
方法,供子类覆写使用。
大致的流程如下所示:
[系统调用 delegate 方法]
↓
[调用 respondsToSelector:]
↓
ObjcDelegateProxy 判断是否支持该 selector(从协议中收集)
↓
系统继续调用该方法,但 proxy 未实现 ——>
↓
触发 forwardInvocation
↓
unpackInvocation -> 拿到参数
↓
interceptedSelector -> 子类处理
↓
发出事件或自定义逻辑
相信现在你应该能明白为什么特地使用 ObjC 来实现了:Swift 不支持
NSInvocation
,也无法重写forwardInvocation
进行完整的消息转发。
回到 TableViewDelegateProxy
,它除了间接继承自 ObjcDelegateProxy
之外,还遵循了 DelegateProxyType
协议,这个 swift 协议的定义就比较简单常见了:
import Foundation
private var associatedKey = "delegateProxy"
public protocol DelegateProxyType {
associatedtype Object
func setDelegate(to object: Object)
}
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public extension DelegateProxyType where Self: DelegateProxy {
static func createDelegateProxy(for object: Object) -> Self {
// 确保线程安全,避免在多线程环境下重复创建/状态竞争。
objc_sync_enter(self)
defer { objc_sync_exit(self) }
let delegateProxy: Self
if let associatedObject = objc_getAssociatedObject(object, &associatedKey) as? Self {
delegateProxy = associatedObject
} else {
delegateProxy = .init()
objc_setAssociatedObject(object, &associatedKey, delegateProxy, .OBJC_ASSOCIATION_RETAIN)
}
delegateProxy.setDelegate(to: object)
return delegateProxy
}
}
通过这个协议,我们可以便捷地为 DelegateProxy
子类添加 createDelegateProxy(_:)
方法,方法内部使用懒加载,创建了一个关联对象,随后调用 setDelegate(to:)
方法,将这个关联对象设置为 object
的代理。
看完这些前置条件,我们可以看一看 DelegateProxy
这个类型了(DelegateProxy.swift):
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
open class DelegateProxy: ObjcDelegateProxy {
private var dict: [Selector: [([Any]) -> Void]] = [:]
private var subscribers = [AnySubscriber<[Any], Never>?]()
...
public override func interceptedSelector(_ selector: Selector, arguments: [Any]) {
dict[selector]?.forEach { handler in
handler(arguments)
}
}
public func intercept(_ selector: Selector, _ handler: @escaping ([Any]) -> Void) {
if dict[selector] != nil {
dict[selector]?.append(handler)
} else {
dict[selector] = [handler]
}
}
public func interceptSelectorPublisher(_ selector: Selector) -> AnyPublisher<[Any], Never> {
DelegateProxyPublisher<[Any]> { subscriber in
// 为了在 deinit 中释放,需要先存储起来
self.subscribers.append(subscriber)
return self.intercept(selector) { args in
_ = subscriber.receive(args)
}
}.eraseToAnyPublisher()
}
deinit {
subscribers.forEach { $0?.receive(completion: .finished) }
subscribers = []
}
}
现在我们可以明白,在 willDisplayCellPublisher
中调用的 interceptSelectorPublisher(_:)
方法,将 #selector(UITableViewDelegate.tableView(_:willDisplay:forRowAt:))
作为 key,用于发送数据的闭包作为 value,存储到 dict
这个字典属性中。这个过程可以看做一个 “注册” 流程。
当消息转发被触发时,interceptedSelector(_: _:)
方法会被调用,通过参数 selector
从 dict
中取出数据发送事件,再将参数作为 value 发送出去。
DelegateProxyPublisher
是一个比较简单的自定义 Publisher
对象,它被定义在 DelegateProxyPublisher,这里就不对其做过多的描述,读者可以翻阅源码自行查看。
总结
至此,相信大家已经清楚了 CombineCocoa 如何利用 Combine 封装 Objective‑C 消息转发来拦截 delegate 调用,它的本质就是使用 Combine 框架,封装消息转发的结果。至于关联对象的使用则是锦上添花。
而且我们也可以明白,为什么 CombineCocoa 这套框架只能拦截没有返回值的代理方法,因为它的 allSelectors
属性里只存储了返回值为 void
的方法。
说到这里,再看看标题,既然提到了 Runtime 的消息转发,就让我们再回顾一下消息转发的相关知识。
Runtime 消息转发
在 ObjC 中,方法的调用都是通过 objc_msgSend
来进行,我们一般称之为 “发送消息”。整个发送消息的流程大致如下所示:
↓
[1] objc_msgSend(obj, sel)
↓
[2] 方法未找到 -> runtime 会调用:
+resolveInstanceMethod: 或 +resolveClassMethod:
↓
[3] 仍未处理?调用:
- forwardingTargetForSelector:
(提供另一个对象转发 selector)
↓
[4] 还不行?runtime 调用:
- methodSignatureForSelector:
↓(你必须返回一个 NSMethodSignature)
[5] 然后调用:
- forwardInvocation:
↓
[6] 如果 forwardInvocation 没处理,会 crash:
unrecognized selector sent to instance
这也就是我们常说的 “使用 ObjC 调用一个未实现的方法时,未必会触发崩溃”,因为我们可以在运行时动态添加实现,或者使用消息转发。
动态方法解析
第一阶段被称之为 “动态方法解析”(Dynamic Method Resolution),即 +resolveInstanceMethod:
或 +resolveClassMethod:
方法中,在运行时动态地添加一个方法。
快速转发
第二阶段被称之为 “快速转发”(Fast Forwarding),即 -forwardingTargetForSelector:
方法中把这次函数调用转发给另外一个对象。
完整消息转发
如果这两步都没处理,则会触发第三阶段:“完整消息转发”(Full Message Forwarding),或者叫 “标准消息转发”(Standard Message Forwarding),也就是上文提到的 -methodSignatureForSelector:
+ -forwardInvocation:
这两个方法。
-methodSignatureForSelector:
是调用 -forwardInvocation:
的前提,该方法负责返回方法的签名信息,也就是参数数量、类型、返回值类型等,为 NSMethodSignature
类型。 NSObject 有一个默认的 -methodSignatureForSelector:
方法实现,默认实现会去当前类的 method list 中查找是否存在名为 selector
的方法,如果找到了,就创建并返回一个对应的 NSMethodSignature
;否则返回 nil
。如果你不重写它,而 selector
确实不存在,默认实现就会返回 nil
,这会直接导致 runtime 抛出 unrecognized selector sent to instance
异常,不会进入 -forwardInvocation:
。
在 CombineCocoa 的场景中,ObjcDelegateProxy
没有覆写 -methodSignatureForSelector:
方法,但是因为其子类实现了对应的代理协议,比如 UITableViewDelegate
,所以相关的 @objc
方法会被添加到方法列表中,所以 -methodSignatureForSelector:
能够生成正确的 NSMethodSignature
对象,进而调用到 -forwardInvocation:
方法。