经过上面的学习咱们知道,当调用办法[person test],便是给person目标发送test音讯,然后经过isa->superclass-> superclass……寻找类目标,找到类目标之后,还会先经过@selector(test)&_mask生成索引,经过索引直接在cache里的散列表里边取办法,假如cache里边没有办法,再遍历methods数组……

OC的办法调用:音讯机制,便是给办法调用者发送音讯,其实都是转换为objc_msgSend函数的调用。

比方:

[person personTest];

转成C++代码便是:

objc_msgSend(person, sel_registerName("personTest"));

曾经咱们讲过sel_registerName(“personTest”)和@selector(personTest)回来的都是SEL,并且打印发现他们的地址的确也相同,所以上面的代码也能够写成:

objc_msgSend(person, @selector(personTest));

音讯接收者:person 音讯称号:personTest 意思便是给person目标发送personTest音讯。

同理,类办法调用:

[MJPerson initialize];

转成C++代码:

(objc_getClass("MJPerson"), sel_registerName("initialize"));

也能够写成:

objc_msgSend([MJPerson class], @selector(initialize));

意思便是给MJPerson类目标发送initialize音讯。

一. objc_msgSend的履行流程

objc_msgSend的履行流程能够分为3大阶段:

  1. 音讯发送:便是依据isa、superclass寻找办法
  2. 动态办法解析:答应开发者动态创立新的办法
  3. 音讯转发:转发给另外一个目标调用这个办法

objc_msgSend内部的这三个阶段阅历完还找不到办法就报错:unrecognized selector sent to instance/class。

由于源码解析比较麻烦,我放到objc_msgSend源码解析这篇文章里边了,强烈建议先看一下,这样就很容易理解下面的流程,下面直接说结论。

##一. 音讯发送

iOS-Runtime3-objc_msgSend底层调用流程

##二. 动态办法解析

iOS-Runtime3-objc_msgSend底层调用流程

下面经过动态增加办法来验证阶段二,动态办法解析。

1. 实例目标的动态办法解析

先创立MJPerson目标,只声明办法,不完结办法: 调用代码:

MJPerson *person = [[MJPerson alloc] init];
[person test];
[MJPerson test];

发现会报错:

unrecognized selector sent to instance 0x10074a610
unrecognized selector sent to class 0x100001118

下面动态增加实例办法,动态增加实例办法需求咱们完结resolveInstanceMethod办法。 在MJPerson.m增加如下代码:

- (void)other
{
    NSLog(@"%s", __func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(test)) {
        // 获取其他办法
        Method method = class_getInstanceMethod(self, @selector(other));
        // 动态增加test办法的完结
        class_addMethod(self, sel,
                        method_getImplementation(method),
                        method_getTypeEncoding(method));
        // 回来YES代表有动态增加办法
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

打印:

-[MJPerson other]

能够发现办法增加成功。调用test,可是test没找到,会进入resolveInstanceMethod里边动态增加办法,办法增加完结会从头走音讯发送流程,然后就找到了other,调用other,然后打印-[MJPerson other]。

弥补:Method便是method_t

或许你不知道Method是什么,其实Method便是在iOS-底层-Runtime2里边讲过的method_t,method_t的结构体是这样的:

struct method_t {
    SEL sel;
    char *types;
    IMP imp;
};

所以,上面的代码就能修改为:

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(test)) {
        // 获取其他办法
        struct method_t *method = (struct method_t *)class_getInstanceMethod(self, @selector(other));
        //Method简直等价于曾经讲的method_t,可打印验证
        NSLog(@"%s, %s, %p",method->sel,method->types,method->imp);
        // 动态增加test办法的完结
        class_addMethod(self, sel, method->imp, method->types);
        // 回来YES代表有动态增加办法
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

履行代码,打印结果如下:

other, v16@0:8, 0x100000dd0
-[MJPerson other]

能够发现,函数名、函数编码(参数、回来值)、函数地址都打印出来了,other办法也调用了,阐明动态办法增加成功,Method和method_t没啥区别。

2. 类目标的动态办法解析

那么假如调用的是类办法呢? 需求在resolveClassMethod办法里边,动态增加类办法。

MJPerson *person = [[MJPerson alloc] init];
//[person test];
[MJPerson test];

动态增加类办法:

//动态增加c言语函数的完结
void c_other(id self, SEL _cmd)//这也验证了OC的办法都有两个隐式参数(id self, SEL _cmd)
{
    NSLog(@"c_other - %@ - %@", self, NSStringFromSelector(_cmd));
}
//动态增加类办法
+ (BOOL)resolveClassMethod:(SEL)sel
{
    if (sel == @selector(test)) {
        // 第一个参数是object_getClass(self)
        //c言语的函数地址便是函数名
        //object_getClass(self)获取元类目标
        class_addMethod(object_getClass(self), sel, (IMP)c_other, "v16@0:8");
        return YES;
    }
    return [super resolveClassMethod:sel];
}

**注意:**动态增加实例办法传入到class_addMethod函数中的是当前类目标self,动态增加类办法传入到class_addMethod函数中的是元类目标object_getClass(self)。

上面代码运转后,打印:

c_other - MJPerson - test

能够发现增加成功,上面不光验证了类办法能够增加成功,并且验证了,还能够运用c言语函数作为他们的完结。

上面代码Demo地址:动态办法解析

##三. 音讯转发

iOS-Runtime3-objc_msgSend底层调用流程

关于音讯转发的逻辑如上图,下面进行验证。

1. 实例目标的音讯转发

① forwardingTargetForSelector回来目标

在MJPerson.m里边完结如下代码:

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (aSelector == @selector(test)) {
        // objc_msgSend([[MJCat alloc] init], aSelector)
        return [[MJCat alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

履行:

MJPerson *person = [[MJPerson alloc] init];
[person test];

打印:

-[MJCat test]

能够发现,person目标没完结test办法,会调用MJCat的test办法。

那假如forwardingTargetForSelector回来值为空呢?

② methodSignatureForSelector回来办法签名

//办法签名:回来值类型、参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(test)) {
        return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
    }
    return [super methodSignatureForSelector:aSelector];
}
 /*
  NSInvocation封装了一个办法调用,包括:办法调用者、办法名、办法参数
    anInvocation.target 办法调用者
    anInvocation.selector 办法名
    [anInvocation getArgument:NULL atIndex:0] 获取参数
  */
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
//    anInvocation.target = [[MJCat alloc] init];
//    [anInvocation invoke];
     //上面下面都能够了
    [anInvocation invokeWithTarget:[[MJCat alloc] init]];
}

上面的代码,咱们在回来办法签名之后,把anInvocation.target给修改为MJCat,所以最终还是会调用MJCat的test办法。

上面的代码,假如仅仅是想要调用MJCat的test办法,在forwardingTargetForSelector里边修改其实更简单。

在forwardInvocation做更多操作:

[person test]履行的代码其实便是forwardInvocation办法里边履行的代码,所以咱们能够在forwardInvocation办法里边做任何操作,比方只打印,获取参数,获取回来值。

首要,MJPerson只声明不完结test办法,MJCat完结test办法,如下

@implementation MJCat
- (int)test:(int)age
{
    return age * 2;
}
@end

在MJPerson.m里边完结如下代码:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(test:)) {
        //直接回来办法签名
        //return [NSMethodSignature signatureWithObjCTypes:"v20@0:8i16"];
        //办法签名省掉数字
        return [NSMethodSignature signatureWithObjCTypes:"i@:I"];
        //拿到MJCat的test办法签名来用
        //return [[[MJCat alloc] init] methodSignatureForSelector:aSelector];
    }
    return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    //参数顺序:receiver(也便是self)、selector、other arguments
    int age;
    [anInvocation getArgument:&age atIndex:2];
    NSLog(@"%d", age + 10);
    //打印:15 + 10 = 25;
    /*
     anInvocation.target 是 [[MJCat alloc] init]
     anInvocation.selector 是 test:
     anInvocation的参数:15
     */
    [anInvocation invokeWithTarget:[[MJCat alloc] init]];
    int ret;
    [anInvocation getReturnValue:&ret];
    NSLog(@"%d", ret);
    //打印:15 * 2 = 30;
}

关于回来办法签名、获取参数、获取回来值可看注释。

2. 类目标的音讯转发

刚才讲的是实例目标的音讯转发,假如是类目标需求完结+号最初的办法,如下:

+ (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (aSelector == @selector(test)) return [MJCat class];
    //+[MJCat test]
    return [super forwardingTargetForSelector:aSelector];
}

回来MJCat类目标,当调用[MJPerson test],会打印:+[MJCat test],调用了MJCat的test办法。

上面的代码假如回来的是实例目标呢?如下:

+ (id)forwardingTargetForSelector:(SEL)aSelector
{
    // objc_msgSend([[MJCat alloc] init], @selector(test))
    // [[[MJCat alloc] init] test]
    if (aSelector == @selector(test)) return [[MJCat alloc] init];
    //-[MJCat test]
    return [super forwardingTargetForSelector:aSelector];
}

能够发现,回来MJCat实例目标,当调用[MJPerson test],会打印:-[MJCat test],调用了MJCat实例目标的目标办法,为什么会这样呢?

经过源码分析可知,forwardingTargetForSelector办法内部便是给回来的目标发送test音讯,所以回来的是实例目标便是给实例目标发送音讯,天然便是如下:

objc_msgSend([[MJCat alloc] init], @selector(test))

同样,假如上面办法回来为空,也会走以下代码:

+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(test)) return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    return [super methodSignatureForSelector:aSelector];
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation
{
    NSLog(@"1123");
}

打印:

1123

弥补:@synthesize、@dynamic

首要在MJPerson.h写如下代码:

@property (assign, nonatomic) int age;

咱们都知道,编译器会自动生成_age成员变量、setter和getter的声明、setter和getter的完结。

可是在很久曾经,Xcode还没这么智能的时分,假如只写上面那句还不可,因为@property只负责生成settetr和getter办法的声明。还要在.m文件中运用@synthesize。

@synthesize age = _age, height = _height;

这时分编译器才会生成_age成员变量、setter和getter的完结。

假如运用@dynamic便是提醒编译器不要自动生成成员变量,不要自动生成setter和getter的完结,等到运转时再增加办法完结。

MJPerson.m代码如下:

@dynamic age;
void setAge(id self, SEL _cmd, int age)
{
    NSLog(@"age is %d", age);
}
int age(id self, SEL _cmd)
{
    return 120;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(setAge:)) {
        class_addMethod(self, sel, (IMP)setAge, "v@:i");
        return YES;
    } else if (sel == @selector(age)) {
        class_addMethod(self, sel, (IMP)age, "i@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

履行代码:

MJPerson *person = [[MJPerson alloc] init];
person.age = 20;
NSLog(@"%d", person.age);

打印:

age is 20
120

能够发现,运用@dynamic就没有生成_age成员变量、setter和getter的完结,这时分咱们自己经过Runtime动态增加了setter和getter的完结才完结了如上打印。

总结:@synthesize自动生成_age成员变量、setter和getter的完结,@dynamic不自动生成_age成员变量、setter和getter的完结,正好是反过来的

Demo地址:音讯转发

面试题:

  1. 讲一下OC的音讯机制

OC中的办法调用其实都是转成了objc_msgSend函数的调用,给receiver(办法调用者)发送了一条音讯(selector办法名)。 objc_msgSend底层有3大阶段:音讯发送(当前类、父类中查找)、动态办法解析、音讯转发。

  1. 说一下音讯转发流程

当音讯发送和动态办法解析都没找到办法就会进入音讯转发阶段 ① 首要会调用+或-最初的forwardingTargetForSelector办法,假如这个办法回来值不为空,就给回来值发送SEL音讯:objc_msgSend(回来值, SEL)。 ② 假如这个办法的回来值为空,就会调用+或-最初的methodSignatureForSelector办法,假如这个办法回来值不为空,就会再调用+或-最初的forwardInvocation办法,咱们能够在forwardInvocation里边办法做任何咱们想做的事。 ③ 假如这个办法的回来值为空,就会调用doesNotRecognizeSelector,报错unrecognized selector sent to instance/class。