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 或者直接导入 #import 注意 Xcode 6 之后代码检查 单独使用会报错 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
@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消息机制,有不理解和不足的的希望大家咨询和补充。谢谢。

你可能感兴趣的:(iOS runtime 消息机制)