概要:傻瓜式讲解动态绑定和消息转发。
学习进度:
- runtime小序曲,从运行时多态看这股神秘力量
- runtime进行曲,objc_msgSend的前世今生(一)
- runtime进行曲,objc_msgSend的前世今生(二)
- runtime变奏曲,那些藏在runtime中的接口(一)
- runtime变奏曲,那些藏在runtime中的接口(二)
一、objc_msgSend伪代码复习
伪代码
// 首先看一下objc_msgSend的方法实现的伪代码
id objc_msgSend(id self, SEL op, ...) {
if (!self) return nil;
// 关键代码(a)
IMP imp = class_getMethodImplementation(self->isa, SEL op);
imp(self, op, ...); // 调用这个函数,伪代码...
}
// 查找IMP
IMP class_getMethodImplementation(Class cls, SEL sel) {
if (!cls || !sel) return nil;
IMP imp = lookUpImpOrNil(cls, sel);
if (!imp) {
... // 执行动态绑定
}
IMP imp = lookUpImpOrNil(cls, sel);
if (!imp) return _objc_msgForward; // 这个是用于消息转发的
return imp;
}
// 遍历继承链,查找IMP
IMP lookUpImpOrNil(Class cls, SEL sel) {
if (!cls->initialize()) {
_class_initialize(cls);
}
Class curClass = cls;
IMP imp = nil;
do { // 先查缓存,缓存没有时重建,仍旧没有则向父类查询
if (!curClass) break;
if (!curClass->cache) fill_cache(cls, curClass);
imp = cache_getImp(curClass, sel);
if (imp) break;
} while (curClass = curClass->superclass); // 关键代码(b)
return imp;
}
问题
伪代码中大部分在runtime进行曲,objc_msgSend的前世今生(一)中已经说明的很详细,这里看下上篇中留下的两个小疑问:
- 动态绑定。
- 消息转发。
二、动态绑定
动态绑定,从名称来看就大致懂了。如果调用一个类的方法,而这个类及其父类均没有实现这个方法。那么我们就在运行时绑定此方法到该类。举一例子如下:
// 使用@dynamic表明不自动合成属性a的set和get方法。
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface A : NSObject
@property (nonatomic, assign) NSInteger a;
@end
@implementation A
@dynamic a;
@end
int main(int argc, char * argv[]) {
A *aObject = [[A alloc] init];
NSLog(@"%ld", aObject.a); // 崩于此行
}
// 执行结果:crash,报错如下
2017-01-09 21:25:06.929 block[28341:228218] -[A a]: unrecognized selector sent to instance 0x60800000c580
参照一中objc_msgSend执行步骤,可知A类并没有动态绑定和消息转发,所以返回的imp为空,执行crash。下面我们为其加入动态绑定的方法。
// 代码
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface A : NSObject
@property (nonatomic, assign) NSInteger a;
@end
@implementation A
@dynamic a;
int a(id self, SEL _cmd) {
return 1;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
class_addMethod([self class], @selector(a), (IMP)a, "i@:");
return YES;
}
@end
int main(int argc, char * argv[]) {
A *aObject = [[A alloc] init];
NSLog(@"%ld", aObject.a);
}
// 没有crash,并输出1
2017-01-09 21:50:11.314 block[30605:247164] 1
OC中的方法实质上就是一个有id self和 SEL _cmd两个参数的C方法。
这里的aObject.a中a为实例方法,那么类方法怎么进行动态绑定?即通过resolveClassMethod方法。
// 代码
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface A : NSObject
@end
@implementation A
void b(id self, SEL _cmd) {
NSLog(@"b");
}
+ (BOOL)resolveClassMethod:(SEL)sel {
class_addMethod([self class], @selector(b), (IMP)b, "v@:");
return YES;
}
@end
int main(int argc, char * argv[]) {
[[A class] performSelector:@selector(b)]; // 因为[A b];这种调用方式会编译错误,所以动态调用
}
// 打断点查看,虽然调用了resolveClassMethod,但还是crash
017-01-09 22:26:58.671 block[34171:285065] +[A b]: unrecognized selector sent to class 0x1037b5e30
参照上一篇文章中,A的class中只存有实例方法,A的metaClass中只存有类方法,而相应的调用也是如此,即实例方法在A的class中找,而类方法在A的metaClass中找。所以上述给A class添加方法b并没有作用,仅仅是添加了一个实例方法b。正确方法如下:
// 代码
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface A : NSObject
@end
@implementation A
void b(id self, SEL _cmd) {
NSLog(@"b");
}
+ (BOOL)resolveClassMethod:(SEL)sel {
Class aMeta = objc_getMetaClass(class_getName([self class]));
class_addMethod([aMeta class], @selector(b), (IMP)b, "v@:");
return YES;
}
@end
int main(int argc, char * argv[]) {
[[A class] performSelector:@selector(b)];
}
// 没有crash,输出b,无敌
2017-01-09 22:31:53.440 block[34634:289598] b
三、消息转发
参见一中查找IMP代码。
// 查找IMP
IMP class_getMethodImplementation(Class cls, SEL sel) {
if (!cls || !sel) return nil;
IMP imp = lookUpImpOrNil(cls, sel);
if (!imp) {
... // 执行动态绑定
}
IMP imp = lookUpImpOrNil(cls, sel);
if (!imp) return _objc_msgForward; // 这个是用于消息转发的
return imp;
}
可知,消息转发是objc_msgSend的最后一道防线。如果找不到imp,则会调用下面方法抛出异常。
- (void)doesNotRecognizeSelector:(SEL)aSelector;
而在调用doesNotRecognizeSelector之前,会先调用方法(对于实例方法)。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
若此方法能为另一个类的消息创建一个有效的方法签名(当另一个类中有aSelector则可以创建)。创建方式如下:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature* signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
B *bObject = [[B alloc] init]; // 假设B中有实例方法aSelector
signature = [bObject methodSignatureForSelector:aSelector];
}
return signature;
}
若返回值signature为nil,则执行doesNotRecognizeSelector抛出异常,若signature签名成功,则执行转发方法。
- (void)forwardInvocation:(NSInvocation *)anInvocation {
B *bObject = [[B alloc] init];
[anInvocation invokeWithTarget:bObject];
}
当然,进入forwardInvocation中之后就不会在调用起本类的doesNotRecognizeSelector方法了。除非将这个消息又转回自己(如果调用某个对象的方法没找到,则调用相应类的doesNotRecognizeSelector抛出异常)。又比如,若forwardInvocation什么都不写,则不会有任何现象,也不会crash,也不会抛出异常。
下面看一下完整的实例方法转发代码:
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface B : NSObject
- (void)b;
@end
@implementation B
- (void)b {
NSLog(@"b");
}
- (void)doesNotRecognizeSelector:(SEL)aSelector {
[super doesNotRecognizeSelector:aSelector];
}
@end
@interface A : NSObject
@end
@implementation A
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature* signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
B *bObject = [[B alloc] init];
signature = [bObject methodSignatureForSelector:aSelector];
}
return signature;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
B *bObject = [[B alloc] init];
[anInvocation invokeWithTarget:bObject];
}
- (void)doesNotRecognizeSelector:(SEL)aSelector {
[super doesNotRecognizeSelector:aSelector];
}
@end
int main(int argc, char * argv[]) {
A *aObject = [[A alloc] init];
[aObject performSelector:@selector(b)];
}
实例方法的转发大概讲完了,接下来看下类方法的转发。和实例方法类似,有两个需要额外注意的地方。
- 实例方法是在B的class中查找b方法,若查找类方法,需要在B的metaClass中查找。
- 上述代码中的methodSignatureForSelector、forwardInvocation、doesNotRecognizeSelector在类方法的转发过程不会被触发,需要将前面的“-”换成“+”才会被触发(毕竟是查找类方法,有点区别)。
类方法转发代码如下:
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface B : NSObject
+ (void)b;
@end
@implementation B
+ (void)b {
NSLog(@"b");
}
- (void)doesNotRecognizeSelector:(SEL)aSelector {
[super doesNotRecognizeSelector:aSelector];
}
@end
@interface A : NSObject
@end
@implementation A
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature* signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
Class bMeta = objc_getMetaClass(class_getName([B class]));
signature = [[bMeta class] instanceMethodSignatureForSelector:aSelector];
}
return signature;
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation {
[anInvocation invokeWithTarget:[B class]];
}
+ (void)doesNotRecognizeSelector:(SEL)aSelector {
[super doesNotRecognizeSelector:aSelector];
}
@end
int main(int argc, char * argv[]) {
[[A class] performSelector:@selector(b)];
}
四、消息转发补充
1、forwardingTargetForSelector
看到现在,不少曾经看过消息转发的一些文章的读友可能发现,有一个函数没有被提到:
- (id)forwardingTargetForSelector:(SEL)aSelector;
那么,它是做什么的呢?经过测试,该函数会在methodSignatureForSelector调用之前进行调用,来看一下是否可以进行转发。下面写一个forwardingTargetForSelector实现转发的样例:
// 下述为类方法的转发样例,如果是实例方法,需要将forwardingTargetForSelector改为实例方法
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface B : NSObject
+ (void)b;
@end
@implementation B
+ (void)b {
NSLog(@"b");
}
@end
@interface A : NSObject
@end
@implementation A
+ (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(b)) {
return [B class];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
int main(int argc, char * argv[]) {
[[A class] performSelector:@selector(b)];
}
// 输出
2017-01-10 08:54:39.007 block[52966:479279] b
那么,既然forwardingTargetForSelector可以实现消息转发,为什么还要使用forwardInvocation作为消息管理中心呢?
- 虽然,forwardingTargetForSelector使用简单,不需要重写methodSignatureForSelector,产生的消耗也比forwardInvocation低得多。
- 但是,forwardingTargetForSelector无法获取当前的NSInvocation,或者说少了一些可以操作的值。
2、respondsToSelector:和isKindOfClass:
若不进行重写,respondsToSelector:和isKindOfClass:均只会作用于继承链,而不会触及转发。假设我在四.1中:
// 代码
NSLog(@"%i", [[A class] respondsToSelector:@selector(b)]);
// 虽然A中实现了b方法的转发,但是respondsToSelector:并不会查看
2017-01-10 09:10:47.921 block[54479:495382] 0
当然,我们可以重写respondsToSelector:来保证消息转发链也可以响应。
// 代码,前面为+是因为这里看的是类方法b
+ (BOOL)respondsToSelector:(SEL)aSelector
{
if ( [super respondsToSelector:aSelector] )
return YES;
else {
if ([[B class] respondsToSelector:@selector(b)])
return YES;
}
return NO;
}
// 调用NSLog(@"%i", [[A class] respondsToSelector:@selector(b)]);输出
2017-01-10 09:14:59.227 block[54899:500487] 1
同样的道理,isKindOfClass:、instancesRespondToSelector:和 conformsToProtocol:都会有相同的机制。
五、消息转发应用
1、多重继承
根据上述任意消息转发样例,可知实现了在A中调用B中的方法b,大概可以猜到,这是不是类似继承机制?答案是肯定的,因为OC不支持多继承,此处就给了一个实现多继承的方式,因为我们可以实现任意个类消息的转发,这里就不举例了。
这是一个先进的技术,只适用于没有其他解决方案的情况下。它能作为继承的替代品。如果必须使用这种技术,请确保您充分了解转发的类和被转发的类的行为。
2、NSProxy使用和面向切面编程
暂时没空写这部分,参见神经病院Objective-C Runtime住院第二天——消息发送与转发第四节。
3、JSPatch
JSPatch中也用到消息转发相关内容,暂不介绍,后续后有专门的文章。
六、消息传递流程图
七、文献
1、https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtForwarding.html#//apple_ref/doc/uid/TP40008048-CH105-SW1
2、http://www.jianshu.com/p/4d619b097e20