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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • 转载:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麦子阅读 727评论 0 2
  • 参考链接: http://www.cnblogs.com/ioshe/p/5489086.html 简介 Runt...
    乐乐的简书阅读 2,129评论 0 9
  • Runtime是什么 Runtime 又叫运行时,是一套底层的 C 语言 API,其为 iOS 内部的核心之一,我...
    SuAdrenine阅读 868评论 0 3
  • 我所自学的内容都是Python3的,所以后面的内容也是Python3的,操作系统为MacOS,后面也会出现Linu...
    ZYiDa阅读 522评论 0 0