iOS runtime 消息机制

  • objc_msgSend

OC中的实例对象调用一个方法称作消息传递,例如:

    Ink *inkInstance = [[Ink alloc] init];
    [inkInstance sendMessage:@"i like beautiful girl"];

代码中,我们将inkInstance这个实例对象称之为消息接收者,sendMessage:我们称之为选择子,即所谓的selector,selector参数共同构成消息,所以
[inkInstance sendMessage:@"i like beautiful girl"]
可以理解为将消息:"发送一个消息: i like beautiful girl"发送给消息的接受者inkInstance
OC中里的消息传递采用动态绑定机制来决定具体调用哪个方法,OC的实例方法在通过runtime转写为C语言后实际就是一个函数(也就是说runtime实际上就是将面向对象的类转变为面向过程的结构体),但是OC并不是在编译期决定调用哪个函数,而是在运行期决定,因为编译期根本不能确定最终会调用哪个函数,这是由于运行期可以修改方法的实现。例如:

id  p= @1024;
//输出1024
NSLog(@"%@", p);
//程序崩溃,报错[__NSCFNumber appendString:]: unrecognized selector ...
[p appendString:@"I'm a good boy"];

这段代码在编译期没有任何问题,因为id类型可以指向任何类型的实例对象,NSString有一个方法appendString:,在编译期不确定这个p到底具体指代什么类型的实例对象,并且在运行期还可以给NSNumber类型添加新的方法,因此编译期发现有appendString:的函数声明就不会报错,但在运行时找不到在NSNumber类中找不到appendString:方法,就会报错。这也就是消息传递的强大之处和弊端,编译期无法检查到未定义的方法,运行期可以添加新的方法。
而OC则是通过强大的runtime将这些方法转换为C语言的函数,但是是如何调用这些函数的呢,这里就将说道我们所说的objc_msgSend。(注:objc/msgSend 只有对象才能发送消息,因此以objc开头 导入 #import <objc/message.h> 或者直接导入 #import <objc/runtime.h> 注意 Xcode 6 之后代码检查 单独使用<objc/message.h>会报错 builtSeting 修改 Enable Strict Checking of objc_msgSend Calls -> NO 才能调用 objc_msgSend)

    Ink *inkInstance = [Ink alloc];
    inkInstance = [inkInstance init];
    //为方便查看通过objc_msgSend转写后的的代码,这里用作两步来执行alloc init方法
    [inkInstance sendMessage:@"i like beautiful girl"];

通过objc_msgSend即可转译为

Ink *inkInstance = objc_msgSend(objc_getClass("Ink"),sel_registerName("alloc"));

这行代码达到了如下几个效果,第一获取Ink类,第二注册alloc方法,第三发送消息,将消息alloc发送给类对象,可以简单的将注册方法理解为,通过方法名获取到转写后C语言函数的函数指针。

inkInstance = objc_msgSend(inkInstance,sel_registerName("init"));

这一行则是,注册了init方法,然后通过objc_msgSend函数将消息init发送给消息的接受者inkInstance

objc_msgSend(inkInstance,sel_registerName("sendMessage:"),@"i like beautiful girl");

这一行代码同样是先注册方法sendMessage:然后通过objc_msgSend函数将消息sendMessage:发送给消息的接收者,只是多了一个参数的传递。
到这里,我们应该就可以看出OC的runtime通过objc_msgSend函数将一个面向对象的消息传递转为了面向过程的函数调用。objc_msgSend函数根据消息的接受者和selector选择适当的方法来调用。这里就涉及到OC的runtime是如何将面向对象的类映射为面向过程的结构体的,我们可以看一下几个重要结构体的定义:

文件objc/runtime.h中有如下定义:
struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

    Class super_class                                        
    const char *name                                         
    long version                                             
    long info                                                
    long instance_size                                       
    struct objc_ivar_list *ivars                             
    struct objc_method_list **methodLists                    
    struct objc_cache *cache                                 
    struct objc_protocol_list *protocols                     
}
/* Use `Class` instead of `struct objc_class *` */

文件objc/objc.h文件中有如下定义
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

注意结构体struct objc_class中包含一个成员变量struct objc_method_list **methodLists,通过名称我们分析出这个成员变量保存了实例方法列表。
结构体struct objc_method_list里面包含以下几个成员变量:结构体struct _objc_method的大小、方法个数以及最重要的方法列表,方法列表存储的是方法描述结构体struct _objc_method,该结构体里保存了选择子方法类型以及方法的具体实现。可以看出方法的具体实现就是一个函数指针,也就是我们自定义的实例方法,选择子也就是selector可以理解为是一个字符串类型的名称,用于查找对应的函数实现

这样就能解释objc_msgSend的工作原理的:

  • 首先,通过接收者的isa指针找到它的class ;

  • 在class的struct objc_method_list找对应的方法 ;

  • 如果class中没有找到,继续往它的superclass中找 ;

  • 一旦找到对应的这个函数,就去执行它的实现IMP.
    如果到了继承树的根部(通常为NSObject)还没有找到,那就会调用NSObjec的一个方法doesNotRecognizeSelector:,这个方法就会报unrecognized selector错误
    而为了避免每次搜索和静态绑定那样直接跳转到函数指针指向的位置去执行,类对象也就是结构体struct objc_class中有一个成员变量struct objc_cache,这个缓存里缓存的正是搜索方法的匹配结果,这样在第二次及以后再访问时就可以采用映射的方式找到相关实现的具体位置。

  • 动态绑定(所属类动态方法解析)

如果沿继承树没有搜索到相关方法则会向接收者所属的类进行一次请求,看是否能够动态的添加一个方法,注意这是一个类方法,因为是向接收者所属的类进行请求。这里就涉及到两个方法:

//通过类对象调用的未实现的方法则会执行此方法
+ (BOOL)resolveClassMethod:(SEL)sel
//通过实例对象调用的未实现的方法则会执行此方法
+ (BOOL)resolveInstanceMethod:(SEL)sel

举个🌰
调用代码

    //Ink类中即没有声明也没有实现receiveSomething和classMethod这两个方法
    Ink *inkInstance = [[Ink alloc]init];
    //调用实例方法
    [inkInstance performSelector:@selector(receiveSomething:) withObject:@"调用了实例方法 receiveSomething"];
    //调用了类方法
    [Ink performSelector:@selector(classMethod:) withObject:@"调用了类方法 classMethod"];

处理代码

#import "Ink.h"
#import<objc/runtime.h>
@implementation Ink
void messageSend (id self,SEL _cmd,id object) {
    if(strcmp(sel_getName(_cmd), "receiveSomething:") == 0) {
        NSLog(@"message for appendString:%@",object);
    }else if (strcmp(sel_getName(_cmd), "classMethod:") == 0) {
        NSLog(@"message for classMethod:%@",object);
    }
}
#pragma mark 第一步骤(所属类动态方法解析)
//实例调用方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(receiveSomething:)) {
        class_addMethod(self, sel, (IMP)messageSend, "v@:*");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
//类方法
+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(classMethod:)) {
        class_addMethod(object_getClass([self class]), sel, (IMP)messageSend, "v@:*");
        return YES;
    }
    return [super resolveClassMethod:sel];
}

由于Ink类没有声明和定义receiveSomething:classMethod:方法,所以运行时应该会报unrecognized selector错误,但是并没有。
对于receiveSomething:
我们重写了类方法+ (BOOL)resolveInstanceMethod:(SEL)sel,当找不到相关实例方法的时候就会调用该类方法去询问是否可以动态添加。
对于classMethod:
我们重写了类方法+ (BOOL)resolveClassMethod:(SEL)sel,当找不到相关实例方法的时候就会调用该类方法去询问是否可以动态添加。
如果返回True就会再次执行相关方法,接下来看一下如何给一个类动态添加一个方法,那就是调用runtime库中的class_addMethod方法,该方法的原型是

BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);

通过参数名可以看出第一个参数是需要添加方法的类,第二个参数是一个selector,也就是实例方法的名字,第三个参数是一个IMP类型的变量也就是函数实现,需要传入一个C函数,这个函数至少有两个参数,一个是id self一个是SEL _cmd,第四个参数是函数类型。这里需要注意的是,对于相关实例方法的时候,参数Class cls可以是self,即类对象,而对于找不到相关类方法的时候,参数Class cls必须为元类,即object_getClass([self class])

  • 消息转发(所属类动态方法解析)

当对象所属类不能动态添加方法后,runtime就会询问当前的接受者是否要进行消息转发

  • 快速转发

//实例方法调用会走这里
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(receiveSomething:)) {
        return [messages new];
    }
    return nil;
}
//类方法调用会走这里
+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(classMethod:)) {
        return [messages new];
    }
    return nil;
}
  • 标准(慢速)转发

//实例方法调用会走这里
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *sig = [super methodSignatureForSelector:aSelector];
    if (!sig) {
        sig = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
    }
    return sig;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    messages *me = [[messages alloc]init];
    if ([me respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:me];
    }
}
//类方法调用会走这里
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *sig = [super methodSignatureForSelector:aSelector];
    if (!sig) {
        sig = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
    }
    return sig;
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation {
    messages *me = [[messages alloc]init];
    if ([me respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:me];
    }
}

既然forwardingTargetForSelector可以实现消息转发,为什么还要使用forwardInvocation作为消息管理中心呢?

  • forwardingTargetForSelector使用简单,不需要重写methodSignatureForSelector,产生的消耗也比forwardInvocation低得多。
  • forwardingTargetForSelector无法获取当前的NSInvocation,或者说少了一些可以操作的值。

而methodSignatureForSelector则是获取到函数的签名,包含了该函数的返回值以及参数,只有methodSignatureForSelector返回不会为nil的时候,才会走forwardInvocation方法。

以上则为我所理解的runtime消息机制,有不理解和不足的的希望大家咨询和补充。谢谢。

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