Runtime(四)objc-msgSend

一、介绍与应用

1.1 objc_msgSend

Objective-C中调用方法,称为消息传递,消息有名称(name)选择子(selector),可以接收参数,而且可能还有返回值。
objc_msgSend其实就是消息传递在底层C语言的函数实现,在Objective-C中,大部分方法调用都是经过objc_msgSend来实现的。当然,除去load方法·等特殊情况。
一般,给对象发送消息如下:

id retrunValue = [someObject messageName:parameter];
复制代码

someObject是消息接收者,messageName是选择子选择子参数合起来称为消息。在底层,编译器收到之后,将其转换obj_msgSend函数,其函数声明如下:

void objc_msgSend(void /* id self, SEL op, ... */ )
复制代码
  • 第一个参数为消息接收者
  • 第二个参数为SEL

上面给对象发送之后转换即为:

id retrunValue = objc_msgSend(someObject, @selector(messageName:), parameter);
复制代码

objc_msgSend会根据接受者与选择子的类型来调用适当的方法。

1.2 应用

下面我们通过直接调用objc_msgSend方法,来看看它是如何调用的。

        NSMutableArray *array = [[NSMutableArray alloc] init];
        [array addObject:@"dog"];
        NSInteger index = [array indexOfObject:@"dog"];
        NSString *last = [array lastObject];
        [array removeLastObject];
复制代码

将上面方法,改为objc_msgSend调用如下:

NSMutableArray *array = ( (NSMutableArray * (*) (id, SEL)) objc_msgSend) ( (id)[NSMutableArray class], @selector(alloc) );
        array = ( (NSMutableArray * (*) (id, SEL)) objc_msgSend) ( (id)array, @selector(init));
        ( (void (*) (id, SEL, NSString *)) objc_msgSend) ( (id)array, @selector(addObject:), @"dog");
NSInteger index = ( (NSInteger (*) (id, SEL, NSString *)) objc_msgSend) ( (id)array, @selector(indexOfObject:), @"dog");
NSString *last = ( (NSString * (*) (id, SEL)) objc_msgSend) ( (id)array, @selector(lastObject));
        ( (void (*) (id, SEL)) objc_msgSend) ( (id)array, @selector(removeLastObject));
复制代码

上面代码,在这儿

1.3 为什么需要objc_msgSend

在C语言中,使用静态绑定(static binding)来实现函数调用,即在编译期就决定运行时所应调用的函数。
看一个实例:

#import <stdio.h>
void printHello() {
    printf("Hello world!\n");	
}

void printGoodbye() {
  printf("Good bye!\n");	
}

void doTheThing(int type) {
   if(type == 0){
      printHello();
   }else{
      printGoodbye();
   }
   return 0;
}
复制代码

如果不考虑内联,那么编译器在编译代码的时候就已经知道程序中有printHelloprintGoodbye的函数了,于是就直接生成调用这些函数的指令。而函数地址实际上是硬编码在指令之中。
若是将刚才的代码改成下面:

#import <stdio.h>
void printHello() {
    printf("Hello world!\n");	
}

void printGoodbye() {
    printf("Good bye!\n");	
}

void doTheThing(int type) {
    void (*fnc)();
    if(type == 0){
        fnc = printHello;
    }else{
        fnc = printGoodbye;
    }
    fnc();
    return 0;
}
复制代码

这时就用到了动态绑定,因为所要调用的函数直到运行期才能确定。编译器在这种情况下生成的指令与刚才不同,在第一个例子中,ifelse都有函数调用指令。而在第二个例子中,只有一个函数调用指令,不过待调用的函数地址无法硬编码在指令之中,而是要在运行期读取出来。
Objective-C,如果要向某对象发送消息,就会使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通的C语言函数,然而对象收到消息之后,调用哪个方法则由运行期决定,甚至可以在运行期改变。
objc_msgSend,就是承载Objective-C动态绑定机制的函数。

1.4 更多的objc_msgSend函数

类比objc_msgSend函数,还有几个类似的方法可以在<objc/message.h>头文件里找到:

//Sends a message with a data-structure return value to an instance of a class.
void objc_msgSend_stret(id self, SEL op, ...)
double objc_msgSend_fpret(id self, SEL op, ...)

id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
复制代码

关于上面三个函数,摘抄一段说明,没有去实证:

objc_msgSend_stret:如果待发送的消息要返回结构体,那么可交由此函数处理。只有当CPU的寄存器能够容纳得下消息返回类型时,这个函数才能处理此消息。若是返回值无法容纳于CPU寄存器中(比如说返回的结构体太大了),那么就由另一个函数执行派发。此时,那个函数会通过分配在栈上的某个变量来处理消息所返回的结构体。

objc_msgSend_fpret:如果消息返回的是浮点数,那么可交由此函数处理。在某些架构的CPU中调用函数时,需要对“浮点数寄存器”(floating-point register)做特殊处理,也就是说,通常所用的objc_msgSend在这种情况下并不合适。这个函数是为了处理x86等架构CPU中某些令人稍觉惊讶的奇怪状况。

objc_msgSendSuper:如果要给超类发消息,例如[super message:parameter],那么就交由此函数处理。也有另外两个与objc_msgSend_stret和objc_msgSend_fpret等效的函数,用于处理发给super的相应消息。

二、探索objc_msgSend

2.1 分阶段流程

2.2 源码导读

透过汇编的流程,对流程进行梳理:

接下来的进入源码:

其中,最后一步:_objc_msgForward_impcache又是汇编,但是有高手已经反编译出来了。
出于Hmmm, What’s that Selector?,拷了一份forwarding.c

三、消息发送

void objc_msgSend(id receiver, @selector(), ...)
复制代码

消息发送的第一个流程,就是消息发送,这一步主要在类及其父类的_方法缓存以及方法列表_中寻找是否有对应的方法。
首先,会根据消息接收者所属的类,查找类”方法列表“,若能找到与”选择子“名称相符的,即跳转其实现代码。否则,按接受者的继承体系继续向上查找,等找到合适的方法之后再跳转。
如果,最终未找到,那就执行动态方法解析的流程。
上面的过程会造成性能上的损失,鉴于此,objc_msgSend会在接受者第一次查找方法后,将该方法及其跳转地址缓存在哈希表中,每个类都有这样一块缓存,后面发送消息,会先哈希表搜寻,并实现快速跳转。当然,相对静态绑定这当然更慢。但是,实际上,这并不会造成程序的性能瓶颈所在。假如,真的是瓶颈,你大可以只编写纯C函数。
objc源码归纳出如下流程:

针对上面的流程,需要说明几点。

3.1 在方法列表中查找

在方法列表中查找方法,系统为了提高效率,做了如下区分:

  • 已排序的方法列表:二分查找
  • 未排序的方法列表:遍历
//查找类的某个分类或类的方法列表————一维数组
static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
    if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
        //已排序:二分查找
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        // 未排序:线性遍历寻找方法
        for (auto& meth : *mlist) {
            if (meth.name == sel) return &meth;
        }
    }
    return nil;
}
复制代码

3.2 在父类方法列表中查找

如果在父类方法列表中查找到方法,那么就缓存到当前receiverClass中。

   // Superclass cache. 从父类方法缓存中查找
    imp = cache_getImp(curClass, sel);
    if (imp) {
        //在父类缓存中找到方法
        if (imp != (IMP)_objc_msgForward_impcache) {
            // Found the method in a superclass. Cache it in this class.
            // 将父类缓存中的方法,缓存到自身类中,结束查找
            log_and_fill_cache(cls, imp, sel, inst, curClass);
            goto done;
        }
    }
复制代码

四、动态方法解析

假如在消息发送过程中,没有查找到方法,那么就会进入动态方法解析。
动态方法解析就是在运行时临时添加一个方法实现,来进行消息的处理。
添加方法的函数是:

/*
cls: 需要添加方法的对象
name: selector 方法名
imp: 对应的函数实现
types: 函数对应的编码
*/
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                const char * _Nullable types)
复制代码

对应的,下图是动态解析的一个流程:

动态方法解析的对应的示例代码

4.1 对象方法与类方法

动态方法解析,可以添加处理对象方法也可以处理类方法。
但是,注意区别的是,类方法,需要给其元类对象添加方法,而实例对象,是给其类对象添加方法。
这也很好理解,因为:

  • 调用对象方法,查找方法是去类对象方法列表;
  • 调用类方法,是去元类方法列表中找;

如下,是一个示例:

void notFound_eat(id self, SEL _cmd)
{
    NSLog(@"%@ - %@", self, NSStringFromSelector(_cmd));
    NSLog(@"current in method %s", __func__);
}

//对象方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(eat)) {
        // 注意添加到self,此处即类对象
        class_addMethod(self, sel, (IMP)notFound_eat, "v16@0:8");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

//类方法解析
+ (BOOL)resolveClassMethod:(SEL)sel
{
    if (sel == @selector(learn)) {
        // 第一个参数是object_getClass(self)
        class_addMethod(object_getClass(self), sel, (IMP)notFound_learn, "v16@0:8");
        return YES;
    }
    return [super resolveClassMethod:sel];
}
复制代码

4.2  class_addMethod

下面是class_addMethod添加的两种方式:

4.3 标记“已经动态解析”

这个标记有何作用?
因为动态解析之后,其实还是又重新走消息发送的阶段了。之所以加这个标记,是为了打破:
**消息发送->动态方法解析->消息发送->动态方法解析….**这个无限循环。
只会执行一次:消息发送->动态方法解析->消息发送->消息转发。

4.4  @dynamic的实现

动态方法解析,最佳的一个实践用例就是,@dynamic的实现。
@dynamic是告诉编译器不用自动生成getter和setter的实现,等到运行时再添加方法实现

五、消息转发

在消息发送——没有在缓存和方法列表中找到,也没有在动态方法解析时,添加方法。就会走到消息转发流程。
消息转发流程,分类两步:

  1. 寻找备援接收者
  2. 完整的消息转发

以下是流程图:

5.1 备援接收者

备援接收者,含义清晰,相当于“这条消息,我不想要接收,有个备份对象来接收”。
备援接收者,在下面方法实现:

- (id)forwardingTargetForSelector:(SEL)aSelector;
复制代码

在这一步,运行期系统会问它:是否把这条消息转给其他接收者来处理。
若方法能找到备援者对象,将其返回,否则返回nil。通过此方案,可以组合(composition)来模拟出多重继承(multiple inheritance)的某些特性。在一个对象内部,可能还有其他一系列对象,该对象可以经由此方法将能够处理某选择子的相关内部对象返回,如此一来,从外部看来,好像是该对象亲自来处理这些消息似的。
需要注意的是,这一步是不能改变消息内容的,如果要达到这个目的,就得通过完整的消息转发机制来做。

示例代码–03消息转发- 备援接收者

5.2. 完整的消息转发

在没有备援接收者的情况下,就会进入完整的消息转发流程中。
完整的消息转发,也分为两步:

  1. 获取方法签名
  2. 进行转发

示例代码—03消息转发- 实例方法

5.2.1 方法签名

方法签名,可以通过下面方式获取。

5.2.2 forwardInvocation

forwardInvocation方法,非常强大,可定制性程度极高,赋予了其极大的权限。
NSInvocation是一个封装了方法调用的类,把与尚未处理那条消息有关的全部细节都封于其中,此对象包含选择子目标(target)参数。在触发NSInvocation对象时,消息派发系统会将消息指派给目标对象。
当然,该方法也可以直接将消息转给备援接收者,但是在上一步中即可做到,所以一般到了这一步,都会修改消息内容,来做只有它能做的事。
但需要注意的时,在使用NSInvocation对象target时,targetassign类型。

5.3 类方法的消息转发

针对备援接收者及完整的消息转发流程,其实平时开发中,一般都认为只有对象方法可以实现消息转发
其实系统也支持对类方法的消息转发
只需要将智能提示后的对象方法前面-修改成+,即可实现类方法的消息转发

示例代码—03消息转发- 类方法

六、unrecognized selector

在历经千山万水之后,仍然走到这一步。
苍天绕过谁,那就抛出我们常见的错误吧!

-[**NSCFNumber lowercaseString]: unrecognized selector sent to instance 0x87

*** Terminating app due to uncaught exception ‘NSInvalidArgumentException’,reason:’- [**NSCFNumber lowercaseString]: unrecognized selector sent to instance 0x87

参考

链接

  1. objc源码
  2. Hmmm, What’s that Selector?

示例代码

  1. 01objc_msgSend
  2. 02动态方法解析
  3. 03消息转发- 备援接收者
  4. 03消息转发- 实例方法
  5. 03消息转发- 类方法
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享