优质广告供应商

广告是为了更好地支持作者创作

runtime进行曲,objc_msgSend的前世今生(二)

概要:傻瓜式讲解动态绑定和消息转发。
学习进度:

一、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中也用到消息转发相关内容,暂不介绍,后续后有专门的文章。

六、消息传递流程图

objc_msgSend全过程(假设存在动态绑定)

七、文献

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

优质广告供应商

广告是为了更好地支持作者创作

推荐阅读更多精彩内容