iOS源码解析:runtime objc_msgSend()消息机制的完整过程

栏目: Objective-C · 发布时间: 7年前

内容简介:iOS方法调用的过程我们都很清楚,比如下面这个方法调用:这个方法调用过程是首先通过person对象的isa指针找到Person类的类对象,由于实例方法存储在类对象中,所以我们就去Person类对象中查找这个test方法如果找到了那就拿来调用,如果没有找到,那就通过Person类对象的superclass指针找到Person类的父类的类对象,去这里查找这个test,如果还没找到则继续沿着继承链往上找,如果最终还是没有找到就会报

iOS源码解析:runtime isa,class底层结构窥探 一>

iOS方法调用的过程我们都很清楚,比如下面这个方法调用:

[person test];

这个方法调用过程是首先通过person对象的isa指针找到Person类的类对象,由于实例方法存储在类对象中,所以我们就去Person类对象中查找这个test方法如果找到了那就拿来调用,如果没有找到,那就通过Person类对象的superclass指针找到Person类的父类的类对象,去这里查找这个test,如果还没找到则继续沿着继承链往上找,如果最终还是没有找到就会报 unrecognized selector sent to instance 0x60000001b830 这个经典的错误。

这样回答方法的调用过程也没有问题,但是显得浅显了一些,还不足以应付面试。下面我们就一起探讨一下iOS中方法的调用过程。

首先把 [person test] 转化为c++的源码:

((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("test"));

化简一下:

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

sel_registerName() 我在上一篇文章已经说过,它就是传入方法名,返回SEL, sel_registerName("test") 就等价于 @selector(test) 这句代码就是给消息接收者发送SEL消息,所以接下来的问题就变成了去探究 objc_msgSend()这个函数的调用过程。

objc_msgSend()的执行流程可以分为三个阶段

  • 消息发送

  • 动态方法解析

  • 消息转发

下面通过源码逐一分析。

首先我们在runtime的源码中搜索 objc_msgSend ,我们发现搜索的结非常多,那我们要找的是它的实现,最终我们在 objc-msg-arm64.s 这样一个汇编文件中找到 objc_msgSend() 的实现。runtime的源码基本都是由c,c++,汇编语言组成,并且很多经常使用的都是由汇编语言给出的。

一 消息发送

objc-msg-arm64.s 中。第304-346行是 objc_msgSend() 的实现。

//从这里开始
304 ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
    MESSENGER_START
//x0寄存器,消息接收者
308 cmp x0, #0          // nil check and tagged pointer check
309 b.le    LNilOrTagged        //  b是跳转,le是小于等于,也就是x0小于等于0时,跳转到LNilOrTagged,x0是objc_msgSend()传入的第一个参数,也就是消息接收者
    ldr x13, [x0]       // x13 = isa
    and x16, x13, #ISA_MASK // x16 = class  
LGetIsaDone:
313 CacheLookup NORMAL      // 缓存查找

315 LNilOrTagged:
316 b.eq    LReturnZero     // 如果消息接收者为空,直接退出这个函数

    // tagged
    mov x10, #0xf000000000000000
    cmp x0, x10
    b.hs    LExtTag
    adrp    x10, _objc_debug_taggedpointer_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
    ubfx    x11, x0, #60, #4
    ldr x16, [x10, x11, LSL #3]
    b   LGetIsaDone

LExtTag:
    // ext tagged
    adrp    x10, _objc_debug_taggedpointer_ext_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
    ubfx    x11, x0, #52, #8
    ldr x16, [x10, x11, LSL #3]
    b   LGetIsaDone

LReturnZero:
    // x0 is already zero
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    MESSENGER_END_NIL
    ret

346 END_ENTRY _objc_msgSend
//结束
  • 1.首先从308行开始, cmp x0, #0 这里x0是寄存器,里面是消息接收者。 b.le LNilOrTagged ,b是跳转的意思,le是如果x0小于等于0,总体意思是若x0小于等于0,则跳转到 LNilOrTagged 。这里意思就是如果消息接收者是nil,则跳转到 LNilOrTagged ,我们看315行的 LNilOrTagged ,执行 b.eq LReturnZero 就是直接退出程序。

  • 2.判断完了消息接收者是否为nil之后,汇编代码继续执行,到313行 CacheLookup NORMAL ,通过字面意思可以知道这是从缓存中查找方法的实现,我们复制一下 CacheLookup 然后去本文件中搜索一下:

    iOS源码解析:runtime objc_msgSend()消息机制的完整过程

    66394697-9FCF-4D47-AE77-96BCB4A9D558.png

  • 3.在缓存中找到了方法那就直接调用,这没什么好说的,下面看一下从缓存中没有找到方法怎么办。没有找到方法则会执行 CheckMiss ,我们搜索一下它的实现。

    iOS源码解析:runtime objc_msgSend()消息机制的完整过程

    9FAC4F96-8798-4867-BC3E-EFC2ABB94AB1.png

再搜索一下 __objc_msgSend_uncached :

iOS源码解析:runtime objc_msgSend()消息机制的完整过程

7CD09FA0-B0BC-4AE3-AE6C-DBB6DE15FACE.png

通过 MethodTableLookup 这个字面名称我们就大概知道这是从方法列表中去查找方法。我们再查看一下它的结构:

iOS源码解析:runtime objc_msgSend()消息机制的完整过程

66A23B20-BCEA-4707-B938-475CB43BBEB3.png

然后我们在本文件中搜索 __class_lookupMethodAndLoadCache3 发现没有它的定义,然后我们再在整个文件中搜索,发现还是没有,这个时候我们去掉开头的一个下划线再搜索,发现有了结果,这是因为汇编的函数比c++的多一个下划线。

  • 4.我们在 objc-runtime-new.mm 这个文件中找到了 _class_lookupMethodAndLoadCache3 的实现:

    iOS源码解析:runtime objc_msgSend()消息机制的完整过程

    A2A6063B-72D4-46CD-B491-CD08D3B44D02.png

主要就是实现了 lookUpImpOrForward() 这个方法,然后我们再查找一下这个方法:

iOS源码解析:runtime objc_msgSend()消息机制的完整过程

1730123B-56F0-48C0-BEA1-C50F1E37626B.png

  • 5.我们具体看一下是怎么从类对象中查找方法的,这个主要是在 getMethodNoSuper_nolock() 这个方法。

    iOS源码解析:runtime objc_msgSend()消息机制的完整过程

    9E58326A-4974-4949-8BE7-99444E5004B5.png

    总结一下消息发送的过程就是下图:

    iOS源码解析:runtime objc_msgSend()消息机制的完整过程

    5EE45D9F-8DA7-400D-A3C7-FAE7E9F212F2.png

二 动态方法解析

在自己的类对象的缓存和方法列表中都没有找到方法,并且在父类的类对象的缓存和方法列表中都没有找到方法时,这时候就会启动动态方法解析。

我们再找到 lookUpImpOrForward 这个方法。在这个方法中前半部分是在自己的类对象以及父类对象中查找方法,后半部分就是处理在自己的类对象和父类对象中都找到不这个方法:

iOS源码解析:runtime objc_msgSend()消息机制的完整过程

91172CDB-1A7E-49AE-A9E5-FD3DEB951ECE.png

然后我们查看一下 _class_resolveMethod() 的实现:

iOS源码解析:runtime objc_msgSend()消息机制的完整过程

AEC48C28-DC5C-433A-8DB1-6628F2E51479.png

其实实现很简单,就是判断是类对象还是元类对象,如果是类对象则说明调用的实例方法,则调用类的 resolveInstanceMethod: 方法,如果是元类对象,则说明是调用的类方法,则调用类的 resolveClassMethod: 方法。

那下面就用实例来演示一下动态方法解析的过程。

首先在main.m文件中创建person对象并调用test方法:

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

虽然在Person.h文件中声明了test方法,但是在Person.m文件中并没有实现test.m文件。所以运行代码的话应该会崩溃,我们运行代码:

果然崩溃了,并且打印了经典错误: unrecognized selector sent to instance 0x60400000e3e0

程序崩溃很容易理解,因为在第一步查找方法中,在自己的类对象以及父类的类对象中都没有找到这个方法,所以转向动态方法解析,动态方法解析我们什么也没做,所以会转向消息转发,消息转发我们也什么都没做,所以最后产生崩溃。接下来我们实现一下动态方法解析。

动态方法解析是当第一步中方法查找失败时会进行的,当调用的是对象方法时,动态方法解析是在 resolveInstanceMethod: 方法中实现的,当调用的是类方法时,动态方法解析是在 resolveClassMethod: 方法中实现的。利用动态方法解析和runtime,我们可以给一个没有实现的方法添加方法实现。

与动态添加方法实现相关的runtime的API是

/** 
 * Adds a new method to a class with a given name and implementation.
*/
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                const char * _Nullable types)

我们看注释就是可以知道,这个方法是给一个给定的方法名也就是SEL添加方法的实现。

@cls : 给哪个类对象添加方法
@name : SEL类型的,给哪个方法名添加方法实现
@imp : IMP类型的,要把哪个方法实现添加给给定的方法名
@types :在讲method_t的结构时讲过这个,就是表示返回值和参数类型的字符串,比如"v16@0:8"

我现在在Person.m文件中实现了test2方法:

- (void)test2{

    NSLog(@"测试动态方法解析");
}

那我想要把这个方法的方法实现添加到Person类中,我就需要调用runtime的 class_addMethod 这个API,这些参数中,cls可以传self,name可以传@selector(test),types可以传 "v16@0:8" ,最难的就是imp应该传什么。我们需要获取test2函数的imp,这个应该怎么获取呢?

runtime中也有相对应的API:

Method _Nullable
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)

这个API返回的就是一个代表方法的Method。我们可以通过

IMP _Nonnull
method_getImplementation(Method _Nonnull m) 

这个runtime的API通过Method结构获取方法的IMP,所以最终的代码就是这样:

+ (BOOL)resolveInstanceMethod:(SEL)sel{

    if (sel == @selector(test)) {
        Method method = class_getInstanceMethod(self, @selector(test2));
        class_addMethod(self, sel, method_getImplementation(method), "v16@0:8");
        return YES;
    }

    return [super resolveInstanceMethod:sel];
}

- (void)test2{

    NSLog(@"测试动态方法解析");
}

这样当第一步方法查找找不到方法时,就会进行第二步动态方法解析,由于调用的是对象方法,所以会执行 resolveInstanceMethod: 方法中的代码,在这个方法中,使用runtime的API,给类对象中动态添加了test方法的实现,这个实现是test2方法的实现。当动态方法解析结束后还会返回去进行方法查找,这次能够查找到test方法及其实现了,也就能够成功调用test方法了。

用一个图总结动态方法解析的整个过程:

iOS源码解析:runtime objc_msgSend()消息机制的完整过程

E45B746D-3235-489E-A6D5-CB53313C1F72.png

三 消息转发

我们再看一下动态方法解析的过程:

iOS源码解析:runtime objc_msgSend()消息机制的完整过程

918365CA-0287-4562-AEB8-E3217F7545C1.png

进行动态方法解析结束之后,会从头开始再进行消息发送这一步,如果在动态方法解析的时候有动态添加方法实现,那么就能找到方法实现并返回方法实现,不再执行下面的代码;如果在动态方法解析的时候没有做什么事,那么就不能找到方法实现,这时候由于 triedResolver 标志位已经置为YES,也就不会再进入动态消息解析,而是会进入消息转发。

消息转发通俗地讲就是本类没有能力去处理这个消息,那么就转发给其他的类,让其他类去处理。

接下来我们看一下进行消息转发的函数 _objc_msgForward_impcache 的具体实现,去文件中搜索,在汇编中找到了它的实现:

iOS源码解析:runtime objc_msgSend()消息机制的完整过程

91474E51-E36E-4186-A8ED-655B30AD797A.png

然后我们去查找 __objc_forward_handler 的实现,但是找到了半天好像并不能找到其实现,这个函数有可能并不是开源的,那我们这条路就行不通了。

网上有人写了 __forwarding__ 这个函数的实现的伪代码,我们可以拿来学习一下。为什么要学习这个函数呢?因为当 [person test] 崩溃时调用栈是这样的:

iOS源码解析:runtime objc_msgSend()消息机制的完整过程

2BEAA7F7-8ADB-4CD5-BE3E-5D331193A9B4.png

我们来看一下 __forwarding__ 函数的第一步:

iOS源码解析:runtime objc_msgSend()消息机制的完整过程

E876DC6C-6E13-4F6A-8913-1DB21965BB8F.png

下面用例子说明一下:

在Person.h中声明了test方法,但是Person.m中并没有去实现。那这个时候用Person对象去调用test方法就会产生崩溃。这个时候在Student.m文件中实现一个test方法,并且在Person.m中通过 forwardingTargetForSelector:

方法把消息转发对象设置为Student对象:

// Student.m
- (void)test{

    NSLog(@"转发给student处理");
}
// Person.m
- (id)forwardingTargetForSelector:(SEL)aSelector{

    if (aSelector == @selector(test)) {
        return [[Student alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

这样的话person对象就成功把 @selector(test) 这个消息转发给student对象让它去处理,自己不管了。相当于是调用了objc_msgSend(student, @selector(test))。我们可以从另外一个角度去验证这个问题,使一个没有实现test方法的类的对象成为消息转发对象:

// Person.m
- (id)forwardingTargetForSelector:(SEL)aSelector{

    if (aSelector == @selector(test)) {
        return [[NSObject alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

这个NSObject类是没有实现test方法的,我们看一下运行结果:

-[NSObject test]: unrecognized selector sent to instance 0x600000013810

我们看到,现在直接是在NSObject这个类中没有找到test方法了。

现在有一个问题了,如果 - (id)forwardingTargetForSelector:(SEL)aSelector 返回为空或者压根就没有实现,程序又会如何继续呢?我们还是从伪码中查找答案:

iOS源码解析:runtime objc_msgSend()消息机制的完整过程

CE05D612-CAEA-4A2A-A3C3-9A0773F5684A.png

下面用代码实例来讲解:

Person.h中有 - (void)testAge:(int)age; 但是在Person.m中并没有实现。

现在在main.m中去调用这个方法:

[person testAge:10];

这个时候会产生崩溃,因为在消息发送阶段没有找到该方法的实现,而动态方法解析和消息转发阶段则什么都没有做,所以就崩溃了。

第一阶段消息发送结束后会进行第二阶段动态消息解析,动态消息解析依赖于+ (BOOL)resolveInstanceMethod:(SEL)sel这个函数,当这个函数也没有动态添加方法实现时,就会进入第三阶段-消息转发。

消息转发首先依赖于 - (id)forwardingTargetForSelector:(SEL)aSelector 这个方法,若是这个方法直接返回了一个消息转发对象,则会通过objc_msgSend()把这个消息转发给消息转发对象了。若是这个方法没有实现或者实现了但是返回值为空,则会跑去执行后面的 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 这个函数以及 - (void)forwardInvocation:(NSInvocation *)anInvocation 这个函数。

现在我们在第二阶段动态方法解析阶段没有做任何处理,在 - (id)forwardingTargetForSelector:(SEL)aSelector 这个函数中也不做处理。那么代码就会执行到 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 这个函数,在这个函数中我们要返回一个方法签名:

Person.m
//方法签名:返回值类型,参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{

    if(aSelector == @selector(testAge:)){

        return [NSMethodSignature signatureWithObjCTypes:"v20@0:8i16"];
    }

    return [super methodSignatureForSelector:aSelector];
}

我们想一下,要完整的表征person对象调用 - (void)testAge:(int)age 这个过程,我们就需要知道方法调用者,方法名,方法参数。而在Person.m中我们肯定知道方法调用者是person对象,方法名也知道是"testAge:",那么现在不知道的就是方法参数了,那么这个方法签名就是表征这个方法参数的,包括返回值和参数,这样方法调用者,方法名和方法参数就都知道了。

然后看 - (void)forwardInvocation:(NSInvocation *)anInvocation 的实现:

//NSInvocation封装了一个方法调用,包括:方法调用者,方法名,方法参数
@  anInvocation.target 方法调用者
@   anInvocation.selector 方法名
@   [anInvocation getArgument:NULL atIndex:0];
- (void)forwardInvocation:(NSInvocation *)anInvocation{

    NSLog(@"%@ %@", anInvocation.target, NSStringFromSelector(anInvocation.selector));
    int age;
    [anInvocation getArgument:&age atIndex:2];
    NSLog(@"%d", age);
    //这行代码是把方法的调用者改变为student对象
    [anInvocation invokeWithTarget:[[Student alloc] init]];

}

在这个方法中有一个NSInvocation类型的anInvocation参数,这个参数就是表征一个方法调用的,我们可以通过这个参数获取person对象调用 - (void)testAge:(int)age 方法这个过程中的方法调用者,方法名,方法参数。然后我们可以通过修改方法调用者来达到消息转发的效果,这里是把方法调用者修改为了student对象。这样就完成了成功转发消息给student对象。

那么我们思考一个问题,在第三阶段消息转发阶段为什么会有三个函数这个复杂?如果我们想要转发消息,那么直接在 - (id)forwardingTargetForSelector:(SEL)aSelector 去返回一个消息转发对象就可以了呀。设计三个函数的好处就是,当来到 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 这个方法时,如果这个方法返回为空,那么走到这里直接结束方法调用,产生崩溃,而如果返回不为空,那么就会继续去调用 - (void)forwardInvocation:(NSInvocation *)anInvocation 这个方法, 那么来到这个里面,我们就可以为所欲为,即使我们什么也不做,运行程序也不会崩溃了,我们可以在这个方法里面为方法指定新的调用者,也即是进行消息转发,也可以做一些其他的操作,都可以,这就是这样设计的一个好处,我们可以在这个方法里面做一切我们想做的。

总结一下消息准发的过程就是:

iOS源码解析:runtime objc_msgSend()消息机制的完整过程

570A2126-DDF8-4103-8B9E-AF6B8FEABF44.png


以上所述就是小编给大家介绍的《iOS源码解析:runtime objc_msgSend()消息机制的完整过程》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Math Adventures with Python

Math Adventures with Python

Peter Farrell / No Starch Press / 2018-11-13 / GBP 24.99

Learn math by getting creative with code! Use the Python programming language to transform learning high school-level math topics like algebra, geometry, trigonometry, and calculus! In Math Adventu......一起来看看 《Math Adventures with Python》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

html转js在线工具
html转js在线工具

html转js在线工具