本文由我们团队的何知昶 组内分享后总结。
最近看了『神奇的 BlocksKit』系列,里面说到动态代理是BlocksKit的精华部分,对于使用block实现委托方法比较好奇,于是下载了源码阅读了一下。
Block已被广泛用于iOS编程。它们通常被用作可并发执行的逻辑单元的封装,或者作为事件触发的回调。Block比传统回调函数有2点优势:
Objective-C中调用方法其实就是向对象发送消息,比如:
[obj msg];
这句代码的含义就是向对象obj发送msg的消息,编译器会调用底层的obj_msgSend( ),从缓存和方法表中,找到对应的IMP指针并执行。有时候在编写程序时,经常会遇到异常报错:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Obj msg]: unrecognized selector sent to instance'
这类报错的根本原因是类Obj的实例并没有实现方法msg,但是在报错之前,系统会通过消息转发机制来确保方法msg没有其他的实现。再三确认没有“补救”措施后,然后抛出异常提示。
对于消息转发,在博客『 轻松学习之 Objective-C消息转发 』中有个很好的比喻:
比赛足球时,脚下有球的那名球员,如果他的位置不利于射门或者他的球即将被对方球员抢断,这时最好是把球传出去,这里的球就相当于消息。
在Objective-c中,消息转发机制有三套方案:
按照动态转发 -> 快速转发 ->慢速转发的过程依次进行补救。
实现动态加解析需要重写如下方法
+(BOOL)resolveInstanceMethod:(SEL)sel; +(BOOL)resolveClassMethod:(SEL)sel;
当我们调用没有实现的方法[obj msg],系统会则进入resolveInstanceMethod:(SEL)sel顺着继承链往上查找是否有msg的实现。若没找到,则返回NO,告诉系统没有找到该方法的实现。通过动态加载,我们可以实现在类中先声明方法,在运行时添加方法实现。
@dynamic声明属性就是一个很好的动态解析的实现,@dynamic告诉编译器在编译期间不要自动创建存取方法,然后在动态解析阶段将存取方法的IMP加入到Class中。
严格来说,动态解析并不是真正意义上的转发。因为在这一过程中,执行方法的依然是消息的接受对象obj,只不过指向msg实现的IMP指针在运行时才加入到obj的方法列表中,而不是在一开始定义的时候,所以动态解析还有一个英文名称叫Lazy Method Resolution。
- (BOOL)respondsToSelector:(SEL)selector; - (BOOL)instancesRespondToSelector:(SEL)selector;
我们在使用以上两方法来判断当前对象是否能够响应msg方法时,也会调用resolveInstanceMethod方法。resolveInstanceMethod返回的BOOL值,通过respondsToSelector返回给判断语句。
动态解析实例如下:
void runTest(id self, SEL _cmd) { NSLog(@"runTest"); } @interface Test : NSObject - (void)run; @end @implementation Test + (BOOL)resolveInstanceMethod:(SEL)sel { if(sel_isEqual(sel, @selector(run))) { class_addMethod([self class], @selector(run), (IMP)runTest, "v@:"); return YES; } return [super resolveInstanceMethod:sel]; } int main(int argc, const char * argv[]) { @autoreleasepool { // insert code here... NSLog(@"Hello, World!"); Test *test = [[Test alloc]init]; BOOL flag = [test respondsToSelector:@selector(run)]; NSLog(@"flag is %d", flag); [test run]; } return 0; }
快速转发的过程有点像网页的重定向,当obj发送的msg消息经过动态解析也没有实现时,系统会将msg消息重定向到另一个对象,在这个对象中实现了与msg消息同名的方法。快速转发需要在obj中实现如下方法:
- (id)forwardingTargetForSelector:(SEL)aSelector { return redirectObj; }
该方法返回的是被重定向的对象,这样就让obj有机会将未知的消息重定向一个新的对象,新对象将作为消息新的接受者来执行msg方法。
快转发实例,添加新的类型Test2实现了run方法,并重定义了快转发所需方法:
@interface Test2 : NSObject - (void)run; @end @implementation Test2 - (void)run { NSLog(@"Test2"); } @end @interface Test : NSObject - (void)run; @end @implementation Test - (id)forwardingTargetForSelector:(SEL)aSelector { return [[Test2 alloc]init]; } @end
了解慢速转发,首先需要知道两个类:NSInvocation和NSMethodSignature。
实现慢转发需要首先重定义如下方法:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel; - (void)forwardInvocation:(NSInvocation *)anInvocation; - (void)invokeWithTarget:(id)anObject;
methodSignatureForSelector:被用于协议的实现中,也用于在消息转发期间NSInvocation对象的创建,需要返回合适方法签名对象。
forwardInvocation:当obj发送的消息没有相应的实现时,运行时系统会给接受者一个机会让它把消息委派给另一个新的接受者。
在快速转发失败后,运行时系统先调用methodSignatureForSelector:返回一个方法签名用于创建NSInvocation对象,创建好后调用forwardInvocation:方法,在改方法中NSInvocation对象将调用invokeWithTarget:方法唤醒新接受者中的同名方法。
当我们在forwardInvocation:中不想对消息进行处理时,可以调用
- (void)doesNotRecognizeSelector:(SEL)aSelector;
该方法会抛出异常报错NSInvalidArgumentException,并生成错误消息。
慢转发实例,在上例的Test类中分别重定义慢转发所需方法
@interface Test2 : NSObject - (void)run; @end @implementation Test2 - (void)run { NSLog(@"Test2"); } @end @interface Test : NSObject - (void)run; @end @implementation Test - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { NSString *selStr = NSStringFromSelector(aSelector); if([selStr isEqualToString:@"run"]) { NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:"v@:"]; return sig; } return [super methodSignatureForSelector:aSelector]; } - (void)forwardInvocation:(NSInvocation *)anInvocation { SEL sel = [anInvocation selector]; Test2 *test = [[Test2 alloc]init]; if([test respondsToSelector:sel]) { [anInvocation invokeWithTarget:test]; } else { [self doesNotRecognizeSelector:sel]; } }
Runtime系统提供的三种消息转发方式按照:动态解析 -> 快速转发 -> 慢速转发的流程来执行
具体在实现时按照以下的要求来选择实现哪个一个:
BlocksKit是一个提供了更好的block支持的第三方框架,它的主要功能如下:
所谓的动态代理就是将原本需要使用delegate实现的方法,可以使用block实现。例如:使用tableView时,经常需要实现tableView:numberOfRowsInSection:、tableView:cellForRowAtIndexPath:、numberOfSectionsInTableView:等代理方法,现在通过BlocksKit,我们可以使用block来实现这些方法的功能。
以下为BlocksKit中与动态代理有关的几个关键类:
其中A2BlockInvocation、A2DynamicDelegate、NSObject+A2DynamicDelegate是实现BlocksKit动态代理的核心。通过NSObject+A2BlockDelegate,BlocksKit封装了部分UIKit分类,提供了对应的block属性。本文主要讲BlocksKit中动态代理的基本实现,因此不涉及NSObject+A2BlockDelegate中的实现逻辑。
整个动态代理的流程如下:
以下是NSObject+A2DynamicDelegate的声明
@interface NSObject (A2DynamicDelegate) @property (readonly, strong) id bk_dynamicDataSource; @property (readonly, strong) id bk_dynamicDelegate; - (id)bk_dynamicDelegateForProtocol:(Protocol *)protocol; @end
声明很简单,分类提供了一个动态的DataSource代理和动态的Delegate代理,以及一个返回动态代理的方法。通过该方法,系统会从该类以及该类的继承链中查找对应的以A2Dynamic开头的动态代理对象。这些对象都在后台进程中惰性初始化:
//沿着继承链查找对应的A2Dynamic**协议的类 static Class a2_dynamicDelegateClass(Class cls, NSString *suffix) { while (cls) { //1、将cls的委托名改成A2Dynamic**的形式 NSString *className = [NSString stringWithFormat:@"A2Dynamic%@%@", NSStringFromClass(cls), suffix]; //2、根据委托名查找对应的类,若存在则返回 Class ddClass = NSClassFromString(className); if (ddClass) return ddClass; //3、不存在则顺则继承链往父类找 cls = class_getSuperclass(cls); } //在继承链中找不到委托名对应的类,返回A2DynamicDelegate的类 return [A2DynamicDelegate class]; } // 根据协议名查找cls中的关联对象 - (id)bk_dynamicDelegateWithClass:(Class)cls forProtocol:(Protocol *)protocol { __block A2DynamicDelegate *dynamicDelegate; dispatch_sync(a2_backgroundQueue(), ^{ //1、获取NSObject的中关联对象 dynamicDelegate = objc_getAssociatedObject(self, (__bridge const void *)protocol); //2、关联对象不存在,则为NSObject创建一个相应的关联对象 //这一步模拟了UITableView.h中的@property (nonatomic, weak, nullable) id <UITableViewDelegate> delegate; if (!dynamicDelegate) { dynamicDelegate = [[cls alloc] initWithProtocol:protocol]; //NSObject对象中的delegate的作为属性定义为weak引用,而dynamicDelegate在作用域结束后会马上被dealloc,为了确保 //dynamicDelegate的生命周期和委托对象一致,必须使用strong引用(为什么生命周期要一致?) objc_setAssociatedObject(self, (__bridge const void *)protocol, dynamicDelegate, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } }); return dynamicDelegate; }
A2DynamicDelegate类是BlocksKit中非常重要的类,它实现了类的Delegate和DataSource等协议,是实现动态委托的核心类之一。
与一般类都以NSObject为父类不一样,A2DynamicDelegate继承自NSProxy类。NSObject类遵循NSObject协议,同样NSProxy也遵循NSObject协议,NSObject类实现了比NSObject协议更多的东西,比如键值编码,可能你根本不需要用它。
建立proxy对象的目是为了预留大多数未实现的方法,使用forwardInvocation:方法来转发。如果把NSObject作为子类则可能会引入更多包,那样容易导致冲突。而NSProxy通过提供了一个不含多余内容的简洁超类来避免此类发生。
作为实现动态委托的核心类,A2DynamicDelegate的主要功能是:进行委托方法到对应block之间的映射和消息转发。
A2DynamicDelegate类包涵以下属性:
@property (nonatomic, readonly) Protocol *protocol; @property (nonatomic, readonly) NSMapTable *invocationsBySelectors; @property (nonatomic, strong, readonly) NSMutableDictionary *handlers; @property (nonatomic, weak, readonly, nullable) id realDelegate;
protocol存储了A2DynamicDelegate实现的委托协议;invocationsBySelectors是一个NSMapTable对象,它建立了委托方法到block之间的映射;handlers则建立了block属性名到具体block实现的映射关系;realDelegate是对象真正的代理,它在NSObject+A2BlockDelegate 中会进行方法调剂,修改原有方法的实现,每次在设置delegate时,会将这个值设置传到realDelegate中。由于handlers和realDelegate主要用于NSObject+A2BlockDelegate拓展动态的block属性,因此不作讲解。
首先我们来看一下,委托方法到block之间映射的建立。
- (void)implementMethod:(SEL)selector withBlock:(id)block { NSCAssert(selector, @"Attempt to implement or remove NULL selector"); BOOL isClassMethod = self.isClassProxy; if (!block) { [self.invocationsBySelectors bk_removeObjectForSelector:selector]; return; } //查询selector在protocol中对应的方法描述 struct objc_method_description methodDescription = protocol_getMethodDescription(self.protocol, selector, YES, !isClassMethod); if (!methodDescription.name) methodDescription = protocol_getMethodDescription(self.protocol, selector, NO, !isClassMethod); //根据方法描述来选择如何创建A2BlockInvocation对象 A2BlockInvocation *inv = nil; if (methodDescription.name) { NSMethodSignature *protoSig = [NSMethodSignature signatureWithObjCTypes:methodDescription.types]; inv = [[A2BlockInvocation alloc] initWithBlock:block methodSignature:protoSig]; } else { inv = [[A2BlockInvocation alloc] initWithBlock:block]; } //建立A2BlockInvocation与selector之间的映射关系 [self.invocationsBySelectors bk_setObject:inv forSelector:selector]; }
代码逻辑很清晰,根据protocol中的selector对应的方法描述来创建A2BlockInvocation对象,然后以selector为key,以A2BlockInvocation对象放入的NSMapTable中。
接下来,我们来看一下methodSignatureForSelector:和forwardInvocation:方法的实现。在前面的消息转发机制中说过,methodSignatureForSelector:返回一个用于创建NSInvocation对象的方法签名,而forwardInvocation:根据创建的NSInvocation对象将消息转发出去。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { A2BlockInvocation *invocation = nil; if ((invocation = [self.invocationsBySelectors bk_objectForSelector:aSelector])) return invocation.methodSignature; else if ([self.realDelegate methodSignatureForSelector:aSelector]) return [self.realDelegate methodSignatureForSelector:aSelector]; else if (class_respondsToSelector(object_getClass(self), aSelector)) return [object_getClass(self) methodSignatureForSelector:aSelector]; //NSObject中是否包含sel的方法 return [[NSObject class] methodSignatureForSelector:aSelector]; }
这里的逻辑如下:
- (void)forwardInvocation:(NSInvocation *)outerInv { SEL selector = outerInv.selector; A2BlockInvocation *innerInv = nil; //从映射表中取出A2BlockInvocation if ((innerInv = [self.invocationsBySelectors bk_objectForSelector:selector])) { [innerInv invokeWithInvocation:outerInv]; } else if ([self.realDelegate respondsToSelector:selector]) { [outerInv invokeWithTarget:self.realDelegate]; } }
forwardInvocation:的实现逻辑如下:
A2BlockInvocation类是BlocksKit能够实现动态代理的另一核心类。在消息转发机制中,慢速转发不仅要重写方法,还需要创建相应的NSMethodSignature对象和NSInvocation对象。在BlocksKit中,A2BlcokInvocation类则扮演了NSInvocation的角色。
@property (nonatomic, strong, readonly) NSMethodSignature *methodSignature; @property (nonatomic, copy, readonly) id block; @property (nonatomic, readonly) NSMethodSignature *blockSignature;
A2BlockInvocation类有三个属性:
在Objective-C中,方法和block都有各自的类型签名,其中方法的签名有NSMethodSignature类表示,但是block并没有一个类来存储其签名信息。在BlocksKit中,有一个_BKBlock结构存储了block的签名信息,这是一个仿照runtime的block结构体定义的一个数据接口。
block的数据结构:
//_BKBlock结构 typedef struct _BKBlock { __unused Class isa; BKBlockFlags flags; __unused int reserved; void (__unused *invoke)(struct _BKBlock *block, ...); struct { unsigned long int reserved; unsigned long int size; // requires BKBlockFlagsHasCopyDisposeHelpers void (*copy)(void *dst, const void *src); void (*dispose)(const void *); // requires BKBlockFlagsHasSignature const char *signature; const char *layout; } *descriptor; // imported variables } *BKBlockRef;
由于NSInvocation需要有NSMethodSignature才能进行消息转发,因此光有block的数据结构并不起作用。还需要将block数据结构中的信息转换为NSMethodSignature表示;
//检查给定的block,并返回兼容的类型签名 + (NSMethodSignature *)typeSignatureForBlock:(id)block __attribute__((pure, nonnull(1))) { BKBlockRef layout = (__bridge void *)block; if (!(layout->flags & BKBlockFlagsHasSignature)) return nil; void *desc = layout->descriptor; desc += 2 * sizeof(unsigned long int); if (layout->flags & BKBlockFlagsHasCopyDisposeHelpers) desc += 2 * sizeof(void *); if (!desc) return nil; const char *signature = (*(const char **)desc); return [NSMethodSignature signatureWithObjCTypes:signature]; }
上述方法的主要功能是找出block的类型编码,然后根据类型编码创建一个NSMethodSignature对象来表示block的签名blockSignature。
BlocksKit在初始化A2BlockInvocation对象时,会调用调用以下两个方法中的一个:isSignature:compatibleWithSignature:和methodSignatureForBlockSignature:
+ (BOOL)isSignature:(NSMethodSignature *)signatureA compatibleWithSignature:(NSMethodSignature *)signatureB __attribute__((pure)) { if (!signatureA || !signatureB) return NO; if ([signatureA isEqual:signatureB]) return YES; //返回值类型是否兼容 if (!typesCompatible(signatureA.methodReturnType, signatureB.methodReturnType)) return NO; NSMethodSignature *methodSignature = nil, *blockSignature = nil; //参数多的为方法签名,参数少的为block签名,否则两个签名不兼容(IMP的第一个参数传self,第二个是selector(即_cmd),但block调用并没有selector) if (signatureA.numberOfArguments > signatureB.numberOfArguments) { methodSignature = signatureA; blockSignature = signatureB; } else if (signatureB.numberOfArguments > signatureA.numberOfArguments) { methodSignature = signatureB; blockSignature = signatureA; } else { return NO; } NSUInteger numberOfArguments = methodSignature.numberOfArguments; //比较参数是否相等 for (NSUInteger i = 2; i < numberOfArguments; i++) { if (!typesCompatible([methodSignature getArgumentTypeAtIndex:i], [blockSignature getArgumentTypeAtIndex:i - 1])) { return NO; } } return YES; }
该方法当使用initWithBlock:methodSignature:初始化时会被调用,其作用主要时比较methodSignature和blockSignature是否兼容
//创造与给定的block签名兼容的方法签名 + (NSMethodSignature *)methodSignatureForBlockSignature:(NSMethodSignature *)original { if (!original) return nil; if (original.numberOfArguments < 1) { return nil; } if (original.numberOfArguments >= 2 && strcmp(@encode(SEL), [original getArgumentTypeAtIndex:1]) == 0) { return original; } // initial capacity is num. arguments - 1 (@? -> @) + 1 (:) + 1 (ret type) // optimistically assuming most signature components are char[1] NSMutableString *signature = [[NSMutableString alloc] initWithCapacity:original.numberOfArguments + 1]; const char *retTypeStr = original.methodReturnType; [signature appendFormat:@"%s%s%s", retTypeStr, @encode(id), @encode(SEL)]; for (NSUInteger i = 1; i < original.numberOfArguments; i++) { const char *typeStr = [original getArgumentTypeAtIndex:i]; NSString *type = [[NSString alloc] initWithBytesNoCopy:(void *)typeStr length:strlen(typeStr) encoding:NSUTF8StringEncoding freeWhenDone:NO]; [signature appendString:type]; } return [NSMethodSignature signatureWithObjCTypes:signature.UTF8String]; }
当使用initWithBlock:初始化A2BlockInvocation对象时,调用上面的方法,该方法的作用是根据blockSignature来创造methodSignature。
最终当A2BlcokInvocation对象需要执行NSInvocation时,将调用如下方法:
- (BOOL)invokeWithInvocation:(NSInvocation *)outerInv returnValue:(out NSValue **)outReturnValue setOnInvocation:(BOOL)setOnInvocation { NSParameterAssert(outerInv); NSMethodSignature *sig = self.methodSignature; //NSInvocation包涵的methodSignature与A2BlcokInvocation的methodSignature是否相等 if (![outerInv.methodSignature isEqual:sig]) { NSAssert(0, @"Attempted to invoke block invocation with incompatible frame"); return NO; } //创建blockSignature对应的NSInvocation对象 NSInvocation *innerInv = [NSInvocation invocationWithMethodSignature:self.blockSignature]; void *argBuf = NULL; //将block的参数信息传递给NSInvocation对象 for (NSUInteger i = 2; i < sig.numberOfArguments; i++) { const char *type = [sig getArgumentTypeAtIndex:i]; NSUInteger argSize; NSGetSizeAndAlignment(type, &argSize, NULL); if (!(argBuf = reallocf(argBuf, argSize))) { return NO; } //block的签名中没有selector参数 [outerInv getArgument:argBuf atIndex:i]; [innerInv setArgument:argBuf atIndex:i - 1]; } //NSInvocation执行block [innerInv invokeWithTarget:self.block]; NSUInteger retSize = sig.methodReturnLength; if (retSize) { if (outReturnValue || setOnInvocation) { if (!(argBuf = reallocf(argBuf, retSize))) { return NO; } [innerInv getReturnValue:argBuf]; if (setOnInvocation) { [outerInv setReturnValue:argBuf]; } if (outReturnValue) { *outReturnValue = [NSValue valueWithBytes:argBuf objCType:sig.methodReturnType]; } } } else { if (outReturnValue) { *outReturnValue = nil; } } free(argBuf); return YES; }
BlocksKit中应用消息转发机制实现动态代理的实例很多,本文仅讲述了BlocksKit动态代理最基本的实现,关于动态代理实现更多的扩展,可以了解NSObject+A2Block的使用。
NSObject:类与协议
Objective-C 消息转发
神奇的BlocksKit(二)
Objective-C runtime 拾遗(一)——NSInvocation 调用Block