13-Explore los principios subyacentes de iOS | Runtime [tres etapas de objc_msgSend (envío de mensajes, método de análisis dinámico, reenvío de mensajes), la esencia de super]

prefacio

Antes, cuando estábamos explorando los principios de la animación y el renderizado, publicamos varios artículos y les respondimos iOS动画是如何渲染,特效是如何工作的疑惑. Sentimos profundamente que los diseñadores de sistemas tienen una mente tan abierta al crear estos marcos de sistemas, y también深深意识到了解一门技术的底层原理对于从事该方面工作的重要性。

Así que decidimos 进一步探究iOS底层原理的任务. Después de la introducción del tiempo de ejecución es una explicación detallada, la estructura de clase y el caché de método cache_t en el artículo anterior , los exploraremos uno por uno: objc_msgSend, 消息转发, 动态方法解析,super的本质

1. objc_msgSend envío de mensajes

Después de un fragmento de código, convierta el código de llamada al método en código C++ para ver cómo se ve la naturaleza de la llamada al método.
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

[person test];
//  --------- c++底层代码
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("test"));

Se puede ver en el código fuente anterior que:

  • Las llamadas a métodos en el código subyacente de C++ en realidad se convierten en objc_msgSendfunciones
  • La llamada de método de OC también se llama 消息机制, dijo 给方法调用者发送消息.
  • Tome el código anterior como ejemplo, el código anterior es en realidad: Envíe un mensaje de prueba al objeto de instancia de persona:
    • Destinatario del mensaje: persona
    • nombre del mensaje: prueba

El proceso de invocación de métodos El proceso de invocación de métodos en realidad se divide en tres etapas:

  • Fase de envío de mensajes: responsable de encontrar métodos de la lista de caché y la lista de métodos de la clase y la clase principal;
  • Fase de análisis dinámico: si no se encuentra ningún método en la fase de envío del mensaje, entrará en la fase de análisis dinámico, que se encarga de agregar implementaciones de métodos dinámicamente;
  • Etapa de reenvío de mensajes: si no se implementa el método de análisis dinámico, se llevará a cabo la etapa de reenvío de mensajes y el mensaje se reenviará al receptor que puede procesar el mensaje para su procesamiento;
  • Si no se implementa el reenvío de mensajes, se informará un error de que no se puede encontrar el método y no se podrá reconocer el mensaje.unrecognzied selector sent to instance

A continuación, al leer runtimeel código fuente, exploramos cómo se implementan las tres etapas de la invocación del método OC:

1. 消息发送

runtime源码中搜索_objc_msgSend查看其内部实现,在objc-msg-arm64.s汇编文件可以知道_objc_msgSend函数的实现

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
    MESSENGER_START

    cmp	x0, #0			// nil check and tagged pointer check
    b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
    ldr	x13, [x0]		// x13 = isa
    and	x16, x13, #ISA_MASK	// x16 = class	
LGetIsaDone:
    CacheLookup NORMAL		// calls imp or objc_msgSend_uncached

上述汇编源码中会首先判断消息接受者reveiver的值。

  • 如果传入的消息接受者为nil则会执行LNilOrTagged
    • LNilOrTagged内部会执行LReturnZero
    • LReturnZero内部则直接return0
  • 如果传入的消息接受者不为nill则执行CacheLookup
    • CacheLookup内部对方法缓存列表进行查找
    • 如果找到则执行CacheHit,进而调用方法
    • 否则执行CheckMiss
      • CheckMiss内部调用__objc_msgSend_uncached
      • __objc_msgSend_uncached内会执行MethodTableLookup
      • MethodTableLookup也就是方法列表查找
      • MethodTableLookup内部的核心代码__class_lookupMethodAndLoadCache3
      • __class_lookupMethodAndLoadCache3也就是C语言函数_class_lookupMethodAndLoadCache3
  • C函数_class_lookupMethodAndLoadCache3函数内部则是对方法查找的核心代码

首先通过一张图看一下汇编语言中_objc_msgSend的运行流程

proceso de envío de mensajes

方法查找的核心函数就是_class_lookupMethodAndLoadCache3函数,接下来重点分析_class_lookupMethodAndLoadCache3函数内的源码。

1.1 _class_lookupMethodAndLoadCache3 函数

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
} 

1.2 lookUpImpOrForward 函数

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    // initialize = YES , cache = NO , resolver = YES
    IMP imp = nil;
    bool triedResolver = NO;
    runtimeLock.assertUnlocked();

    // 缓存查找, 因为cache传入的为NO, 这里不会进行缓存查找, 因为在汇编语言中CacheLookup已经查找过
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    runtimeLock.read();
    if (!cls->isRealized()) {
        runtimeLock.unlockRead();
        runtimeLock.write();
        realizeClass(cls);
        runtimeLock.unlockWrite();
        runtimeLock.read();
    }
    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
    }

 retry:    
    runtimeLock.assertReading();

    // 防止动态添加方法,缓存会变化,再次查找缓存。
    imp = cache_getImp(cls, sel);
    // 如果查找到imp, 直接调用done, 返回方法地址
    if (imp) goto done;

    // 查找方法列表, 传入类对象和方法名
    {
        // 根据sel去类对象里面查找方法
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            // 如果方法存在,则缓存方法,
            // 内部调用的就是 cache_fill 上文中已经详细讲解过这个方法,这里不在赘述了。
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            // 方法缓存之后, 取出imp, 调用done返回imp
            imp = meth->imp;
            goto done;
        }
    }

    // 如果类方法列表中没有找到, 则去父类的缓存中或方法列表中查找方法
    {
        unsigned attempts = unreasonableClassCount();
        // 如果父类缓存列表及方法列表均找不到方法,则去父类的父类去查找。
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // 查找父类的缓存
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // 在父类中找到方法, 在本类中缓存方法, 注意这里传入的是cls, 将方法缓存在本类缓存列表中, 而非父类中
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    // 执行done, 返回imp
                    goto done;
                }
                else {
                    // 跳出循环, 停止搜索
                    break;
                }
            }
            
            // 查找父类的方法列表
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                // 同样拿到方法, 在本类进行缓存
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                // 执行done, 返回imp
                goto done;
            }
        }
    }
    
    // ---------------- 消息发送阶段完成 ---------------------

    // ---------------- 进入动态解析阶段 ---------------------
    // 上述列表中都没有找到方法实现, 则尝试解析方法
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        triedResolver = YES;
        goto retry;
    }

    // ---------------- 动态解析阶段完成 ---------------------

    // ---------------- 进入消息转发阶段 ---------------------
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();
    // 返回方法地址
    return imp;
} 

1.3 getMethodNoSuper_nolock 函数

方法列表中查找方法

getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();
    assert(cls->isRealized());
    // cls->data() 得到的是 class_rw_t
    // class_rw_t->methods 得到的是methods二维数组
    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
         // mlists 为 method_list_t
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }
    return nil;
} 

上述源码中:

  • getMethodNoSuper_nolock函数中通过遍历方法列表拿到method_list_t
  • 最终通过search_method_list函数查找方法

1.3.1 search_method_list函数

static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
    // 如果方法列表是有序的,则使用二分法查找方法,节省时间
    if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        // 否则则遍历列表查找
        for (auto& meth : *mlist) {
            if (meth.name == sel) return &meth;
        }
    }
    return nil;
} 

1.3.2 findMethodInSortedMethodList函数内二分查找实现原理

static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
    assert(list);

    const method_t * const first = &list->first;
    const method_t *base = first;
    const method_t *probe;
    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;
    // >>1 表示将变量n的各个二进制位顺序右移1位,最高位补二进制0。
    // count >>= 1 如果count为偶数则值变为(count / 2)。如果count为奇数则值变为(count-1) / 2 
    for (count = list->count; count != 0; count >>= 1) {
        // probe 指向数组中间的值
        probe = base + (count >> 1);
        // 取出中间method_t的name,也就是SEL
        uintptr_t probeValue = (uintptr_t)probe->name;
        if (keyValue == probeValue) {
            // 取出 probe
            while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                probe--;
            }
           // 返回方法
            return (method_t *)probe;
        }
        // 如果keyValue > probeValue 则折半向后查询
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
} 

至此为止,消息发送阶段已经完成。\

2. 总结

我们通过一张图来看一下_class_lookupMethodAndLoadCache3函数内部消息发送的整个流程:

imagen.png

如果消息发送阶段没有找到方法,就会进入动态解析方法阶段

二、动态方法解析

1. 了解方法的动态解析

当在本类cache包括class_rw_t中都找不到方法时会向上找父类的cache包括class_rw_t,若一直找不到方法,就会进入动态方法解析阶段.

我们来看一下动态解析阶段源码:

动态解析的方法

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    } 

_class_resolveMethod函数内部,根据类对象或元类对象做不同的操作

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
} 

从上述代码可以发现:

  • 动态解析方法之后,会将triedResolver = YES;
  • 那么下次就不会在进行动态解析阶段了,之后会重新执行retry,会重新对方法查找一遍
  • 也就是说:
    • 无论我们是否实现动态解析方法
    • 无论动态解析方法是否成功,retry之后都不会在进行动态的解析方法

2. 如何动态解析方法

  • 动态解析对象方法时,会调用+(BOOL)resolveInstanceMethod:(SEL)sel方法;
  • 动态解析类方法时,会调用+(BOOL)resolveClassMethod:(SEL)sel方法。

这里以实例对象为例通过代码来看一下动态解析的过程

@implementation Person
- (void) other {
    NSLog(@"%s", __func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    // 动态的添加方法实现
    if (sel == @selector(test)) {
        // 获取其他方法 指向method_t的指针
        Method otherMethod = class_getInstanceMethod(self, @selector(other));
        
        // 动态添加test方法的实现
        class_addMethod(self, sel, method_getImplementation(otherMethod), method_getTypeEncoding(otherMethod));
        
        // 返回YES表示有动态添加方法
        return YES;
    }
    
    NSLog(@"%s", __func__);
    return [super resolveInstanceMethod:sel];
}

@end 
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        [person test];
    }
    return 0;
}
// 打印结果
// -[Person other] 

上述代码中可以看出,person在调用test方法时经过动态解析成功调用了other方法。

通过上面对消息发送的分析我们得知:

  • 当本类和父类cacheclass_rw_t中都找不到方法时,就会进行动态解析的方法
  • 也就是说会自动调用类的resolveInstanceMethod:方法进行动态查找
  • 因此我们可以在resolveInstanceMethod:方法内部使用class_addMethod动态的添加方法实现

这里需要注意class_addMethod用来向具有给定方法名称实现的类添加新方法

  • class_addMethod将添加一个方法实现的覆盖,但是不会替换已有的实现
  • 也就是说如果上述代码中已经实现了-(void)test方法,则不会再动态添加方法,这点在上述源码中也可以体现,因为一旦找到方法实现就直接return imp并调用方法了,不会再执行动态解析方法了。

2.1 动态添加方法

class_addMethod 函数

我们来看一下class_addMethod函数的参数分别代表什么。

    /** 
     第一个参数: cls:给哪个类添加方法
     第二个参数: SEL name:添加方法的名称
     第三个参数: IMP imp: 方法的实现,函数入口,函数名可与方法名不同(建议与方法名相同)
     第四个参数: types :方法类型,需要用特定符号,参考API
     */
class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char *types) 

上述参数上文中已经详细讲解过,这里不再赘述。

需要注意的是我们在上述代码中通过class_getInstanceMethod获取Method的方法

// 获取其他方法 指向method_t的指针
Method otherMethod = class_getInstanceMethod(self, @selector(other)); 
  • 其实Method是objc_method类型结构体,可以理解为其内部结构同method_t结构体相同
  • 前文中提到过method_t是代表方法的结构体,其内部包含SEL、type、IMP
  • 我们通过自定义method_t结构体,将objc_method强转为method_t查看方法是否能够动态添加成功:
    struct method_t {
        SEL sel;
        char *types;
        IMP imp;
    };
    
    - (void) other {
        NSLog(@"%s", __func__);
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel
    {
        // 动态的添加方法实现
        if (sel == @selector(test)) {
            // Method强转为method_t
            struct method_t *method = (struct method_t *)class_getInstanceMethod(self, @selector(other));
    
            NSLog(@"%s,%p,%s",method->sel,method->imp,method->types);
    
            // 动态添加test方法的实现
            class_addMethod(self, sel, method->imp, method->types);
    
            // 返回YES表示有动态添加方法
            return YES;
        }
    
        NSLog(@"%s", __func__);
        return [super resolveInstanceMethod:sel];
    } 
    

查看打印内容

动态解析方法[3246:1433553] other,0x100000d00,v16@0:8
动态解析方法[3246:1433553] -[Person other] 

可以看出确实可以打印出相关信息,那么我们就可以理解为:

  • objc_method内部结构同method_t结构体相同,可以代表类定义中的方法

另外上述代码中我们通过method_getImplementation函数和method_getTypeEncoding函数获取方法的imptype。当然我们也可以通过自己写的方式来调用,这里以动态添加有参数的方法为例。

+(BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(eat:)) {
        class_addMethod(self, sel, (IMP)cook, "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
void cook(id self ,SEL _cmd,id Num)
{
    // 实现内容
    NSLog(@"%@的%@方法动态实现了,参数为%@",self,NSStringFromSelector(_cmd),Num);
} 

上述代码中当调用eat:方法时,动态添加了cook函数作为其实现并添加id类型的参数。

2.2 动态解析类方法

当动态解析类方法的时候,就会调用+(BOOL)resolveClassMethod:(SEL)sel函数
而我们知道类方法是存储在元类对象里面的,因此cls第一个对象需要传入元类对象以下代码为例:

void other(id self, SEL _cmd)
{
    NSLog(@"other - %@ - %@", self, NSStringFromSelector(_cmd));
}

+ (BOOL)resolveClassMethod:(SEL)sel
{
    if (sel == @selector(test)) {
        // 第一个参数是object_getClass(self),传入元类对象。
        class_addMethod(object_getClass(self), sel, (IMP)other, "v16@0:8");
        return YES;
    }
    return [super resolveClassMethod:sel];
} 

我们在上述源码的分析中提到过:

  • 无论我们是否实现了动态解析的方法,系统内部都会执行retry对方法再次进行查找
  • 那么如果我们实现了动态解析方法,此时就会顺利查找到方法,进而返回imp对方法进行调用
  • 如果我们没有实现动态解析方法。就会进行消息转发。

3. 总结

接下来看一下动态解析方法流程图示

imagen.png

七、消息转发

1. 消息转发

如果我们自己也没有对方法进行动态的解析,那么就会进行消息转发

imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst); 

自己没有能力处理这个消息的时候,就会进行消息转发阶段,会调用_objc_msgForward_impcache函数。

通过搜索可以在汇编中找到__objc_msgForward_impcache函数实现:

  • __objc_msgForward_impcache函数中调用__objc_msgForward进而找到__objc_forward_handler
objc_defaultForwardHandler(id self, SEL sel)
{
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

我们发现这仅仅是一个错误信息的输出。
其实消息转发机制是不开源的,但是我们可以猜测其中可能拿返回的对象调用了objc_msgSend,重走了一遍消息发送动态解析消息转发的过程。最终找到方法进行调用。

我们通过代码来看一下

  • 首先创建Car类继承自NSObject,并且Car有一个- (void) driving方法,
  • Person类实例对象失去了驾车的能力,并且没有在开车过程中动态的学会驾车,那么此时就会将开车这条信息转发给Car
  • Car实例对象来帮助person对象驾车
#import "Car.h"
@implementation Car
- (void) driving
{
    NSLog(@"car driving");
}
@end

--------------

#import "Person.h"
#import <objc/runtime.h>
#import "Car.h"
@implementation Person
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    // 返回能够处理消息的对象
    if (aSelector == @selector(driving)) {
        return [[Car alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

--------------

#import<Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Person *person = [[Person alloc] init];
        [person driving];
    }
    return 0;
}

// 打印内容
// 消息转发[3452:1639178] car driving

由上述代码可以看出:

  • 当本类没有实现方法,并且没有动态解析方法,就会调用forwardingTargetForSelector函数,进行消息转发
  • 我们可以实现forwardingTargetForSelector函数,在其内部将消息转发给可以实现此方法的对象

如果forwardingTargetForSelector函数返回为nil或者没有实现的话

  • 就会调用methodSignatureForSelector方法,用来返回一个方法签名(这是我们正确跳转方法的最后机会)
  • 如果methodSignatureForSelector方法返回正确的方法签名就会调用forwardInvocation方法
  • forwardInvocation方法内提供一个NSInvocation类型的参数
    • NSInvocation封装了一个方法的调用,包括方法的调用者,方法名,以及方法的参数
    • forwardInvocation函数内修改方法调用对象即可
  • 如果methodSignatureForSelector返回的为nil,就会来到doseNotRecognizeSelector:方法内部
  • 程序crash提示无法识别选择器unrecognized selector sent to instance

我们通过以下代码进行验证

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    // 返回能够处理消息的对象
    if (aSelector == @selector(driving)) {
        // 返回nil则会调用methodSignatureForSelector方法
        return nil; 
        // return [[Car alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

// 方法签名:返回值类型、参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(driving)) {
       // return [NSMethodSignature signatureWithObjCTypes: "v@:"];
       // return [NSMethodSignature signatureWithObjCTypes: "v16@0:8"];
       // 也可以通过调用Car的methodSignatureForSelector方法得到方法签名,这种方式需要car对象有aSelector方法
        return [[[Car alloc] init] methodSignatureForSelector: aSelector];

    }
    return [super methodSignatureForSelector:aSelector];
}

//NSInvocation 封装了一个方法调用,包括:方法调用者,方法,方法的参数
//    anInvocation.target 方法调用者
//    anInvocation.selector 方法名
//    [anInvocation getArgument: NULL atIndex: 0]; 获得参数
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
//   anInvocation中封装了methodSignatureForSelector函数中返回的方法。
//   此时anInvocation.target 还是person对象,我们需要修改target为可以执行方法的方法调用者。
//   anInvocation.target = [[Car alloc] init];
//   [anInvocation invoke];
    [anInvocation invokeWithTarget: [[Car alloc] init]];
}

// 打印内容
// 消息转发[5781:2164454] car driving 

2.总结

上述代码中可以发现方法可以正常调用。接下来我们来看一下消息转发阶段的流程图

imagen.png

3.NSInvocation

  • methodSignatureForSelector方法中返回的方法签名
  • forwardInvocation中被包装成NSInvocation对象
  • NSInvocation提供了获取和修改方法名参数返回值等方法,也就是说,在forwardInvocation函数中我们可以对方法进行最后的修改。

同样上述代码,我们为driving方法添加返回值和参数,并在forwardInvocation方法中修改方法的返回值及参数。

#import "Car.h"
@implementation Car
- (int) driving:(int)time
{
    NSLog(@"car driving %d",time);
    return time * 2;
}
@end

#import "Person.h"
#import <objc/runtime.h>
#import "Car.h"

@implementation Person
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    // 返回能够处理消息的对象
    if (aSelector == @selector(driving)) {
        return nil;
    }
    return [super forwardingTargetForSelector:aSelector];
}

// 方法签名:返回值类型、参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(driving:)) {
         // 添加一个int参数及int返回值type为 i@:i
         return [NSMethodSignature signatureWithObjCTypes: "i@:i"];
    }
    return [super methodSignatureForSelector:aSelector];
}


//NSInvocation 封装了一个方法调用,包括:方法调用者,方法,方法的参数
- (void)forwardInvocation:(NSInvocation *)anInvocation
{    
    int time;
    // 获取方法的参数,方法默认还有self和cmd两个参数,因此新添加的参数下标为2
    [anInvocation getArgument: &time atIndex: 2];
    NSLog(@"修改前参数的值 = %d",time);
    time = time + 10; // time = 110
    NSLog(@"修改前参数的值 = %d",time);
    // 设置方法的参数 此时将参数设置为110
    [anInvocation setArgument: &time atIndex:2];
    
    // 将tagert设置为Car实例对象
    [anInvocation invokeWithTarget: [[Car alloc] init]];
    
    // 获取方法的返回值
    int result;
    [anInvocation getReturnValue: &result];
    NSLog(@"获取方法的返回值 = %d",result); // result = 220,说明参数修改成功
    
    result = 99;
    // 设置方法的返回值 重新将返回值设置为99
    [anInvocation setReturnValue: &result];
    
    // 获取方法的返回值
    [anInvocation getReturnValue: &result];
    NSLog(@"修改方法的返回值为 = %d",result);    // result = 99
}

#import<Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        // 传入100,并打印返回值
        NSLog(@"[person driving: 100] = %d",[person driving: 100]);
    }
    return 0;
} 
消息转发[6415:2290423] 修改前参数的值 = 100
消息转发[6415:2290423] 修改前参数的值 = 110
消息转发[6415:2290423] car driving 110
消息转发[6415:2290423] 获取方法的返回值 = 220
消息转发[6415:2290423] 修改方法的返回值为 = 99
消息转发[6415:2290423] [person driving: 100] = 99

从上述打印结果可以看出:
forwardInvocation方法中可以对方法的参数及返回值进行修改。

并且我们可以发现,在设置tagert为Car实例对象时,就已经对方法进行了调用,而在forwardInvocation方法结束之后才输出返回值。

通过上述验证我们可以知道只要来到forwardInvocation方法中,我们便对方法调用有了绝对的掌控权,可以选择是否调用方法,以及修改方法的参数返回值等等。

4. 类方法的消息转发

类方法消息转发同对象方法一样,同样需要经过消息发送,动态方法解析之后才会进行消息转发机制。

我们知道类方法是存储在元类对象中的,元类对象本来也是一种特殊的类对象。需要注意的是,类方法的消息接受者变为元类对象

当类对象进行消息转发时,对调用相应的+号的forwardingTargetForSelector、methodSignatureForSelector、forwardInvocation方法,需要注意的是+号方法仅仅没有提示,而不是系统不会对类方法进行消息转发。

下面通过一段代码查看类方法的消息转发机制。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [Person driving];
    }
    return 0;
}

#import "Car.h"
@implementation Car
+ (void) driving;
{
    NSLog(@"car driving");
}
@end

#import "Person.h"
#import <objc/runtime.h>
#import "Car.h"

@implementation Person

+ (id)forwardingTargetForSelector:(SEL)aSelector
{
    // 返回能够处理消息的对象
    if (aSelector == @selector(driving)) {
        // 这里需要返回类对象
        return [Car class]; 
    }
    return [super forwardingTargetForSelector:aSelector];
}
// 如果forwardInvocation函数中返回nil 则执行下列代码
// 方法签名:返回值类型、参数类型
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(driving)) {
        return [NSMethodSignature signatureWithObjCTypes: "v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

+ (void)forwardInvocation:(NSInvocation *)anInvocation
{
    [anInvocation invokeWithTarget: [Car class]];
}

// 打印结果
// 消息转发[6935:2415131] car driving 

上述代码中同样可以对类对象方法进行消息转发。需要注意的是类方法的接受者为类对象。其他同对象方法消息转发模式相同。

总结

OC中的方法调用其实都是转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名)。
方法调用过程中也就是objc_msgSend底层实现分为三个阶段:消息发送、动态方法解析、消息转发
本文主要对这三个阶段相互之间的关系以及流程进行的探索。上文中已经讲解的很详细,这里不再赘述。

八、super的本质

首先来看一道面试题。 下列代码中Person继承自NSObjectStudent继承自Person,写出下列代码输出内容。

#import "Student.h"
@implementation Student
- (instancetype)init
{
    if (self = [super init]) {
        NSLog(@"[self class] = %@", [self class]);
        NSLog(@"[self superclass] = %@", [self superclass]);
        NSLog(@"----------------");
        NSLog(@"[super class] = %@", [super class]);
        NSLog(@"[super superclass] = %@", [super superclass]);

    }
    return self;
}
@end 

直接来看一下打印内容

Runtime-super[6601:1536402] [self class] = Student
Runtime-super[6601:1536402] [self superclass] = Person
Runtime-super[6601:1536402] ----------------
Runtime-super[6601:1536402] [super class] = Student
Runtime-super[6601:1536402] [super superclass] = Person 

上述代码中可以发现无论是self还是super调用classsuperclass的结果都是相同的。

为什么结果是相同的?
super关键字在调用方法的时候底层调用流程是怎样的?

我们通过一段代码来看一下super底层实现,为Person类提供run方法,Student类中重写run方法,方法内部调用[super run];,将Student.m转化为c++代码查看其底层实现。

- (void) run
{
    [super run];
    NSLog(@"Student...");
} 

上述代码转化为c++代码

static void _I_Student_run(Student * self, SEL _cmd) {
    
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Student"))}, sel_registerName("run"));
    
    
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_jm_dztwxsdn7bvbz__xj2vlp8980000gn_T_Student_e677aa_mi_0);
} 

通过上述源码可以发现:

  • [super run];转化为底层源码内部其实调用的是objc_msgSendSuper函数
  • objc_msgSendSuper函数内传递了两个参数。__rw_objc_super结构体和sel_registerName("run")方法名
  • __rw_objc_super结构体内传入的参数是selfclass_getSuperclass(objc_getClass("Student"))也就是Student的父类Person

首先我们找到objc_msgSendSuper函数查看内部结构

OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0); 

可以发现objc_msgSendSuper中传入的结构体是objc_super,我们来到objc_super内部查看其内部结构。 我们通过源码查找objc_super结构体查看其内部结构。

// 精简后的objc_super结构体
struct objc_super {
    __unsafe_unretained _Nonnull id receiver; // 消息接受者
    __unsafe_unretained _Nonnull Class super_class; // 消息接受者的父类
    /* super_class is the first class to search */ 
    // 父类是第一个开始查找的类
}; 
  • objc_super结构体中可以发现receiver消息接受者仍然为self
  • superclass仅仅是用来告知消息查找从哪一个类开始。从父类的类对象开始去查找。

我们通过一张图看一下其中的区别。

La diferencia entre llamar a métodos desde self/super

从上图中我们知道 super调用方法的消息接受者receiver仍然是self,只是从父类的类对象开始去查找方法。

那么此时重新回到面试题,我们知道class的底层实现如下面代码所示

+ (Class)class {
    return self;
}

- (Class)class {
    return object_getClass(self);
} 

class内部实现是根据消息接受者返回其对应的类对象,最终会找到基类的方法列表中
selfsuper的区别仅仅是self从本类类对象开始查找方法
super从父类类对象开始查找方法,因此最终得到的结果都是相同的

另外我们在回到run方法内部,很明显可以发现,如果super不是从父类开始查找方法,从本类查找方法的话,就调用方法本身造成循环调用方法而crash。

同理superclass底层实现同class类似,其底层实现代码如下入所示

+ (Class)superclass {
    return self->superclass;
}

- (Class)superclass {
    return [self class]->superclass;
} 

因此得到的结果也是相同的。

1. objc_msgSendSuper2函数

上述OC代码转化为c++代码并不能说明super底层调用函数就一定objc_msgSendSuper

其实super底层真正调用的函数时objc_msgSendSuper2函数我们可以通过查看super调用方法转化为汇编代码来验证这一说法

- (void)viewDidLoad {
    [super viewDidLoad];
} 

通过断点查看其汇编调用栈

función objc_msgSendSuper2

上图中可以发现super底层其实调用的是objc_msgSendSuper2函数,我们来到源码中查找一下objc_msgSendSuper2函数的底层实现,我们可以在汇编文件中找到其相关底层实现。

ENTRY _objc_msgSendSuper2
UNWIND _objc_msgSendSuper2, NoFrame
MESSENGER_START

ldp	x0, x16, [x0]		// x0 = real receiver, x16 = class
ldr	x16, [x16, #SUPERCLASS]	// x16 = class->superclass
CacheLookup NORMAL

END_ENTRY _objc_msgSendSuper2 

通过上面汇编代码我们可以发现,其实底层是在函数内部调用的class->superclass获取父类,并不是我们上面分析的直接传入的就是父类对象。

其实_objc_msgSendSuper2内传入的结构体为objc_super2

struct objc_super2 {
    id receiver;
    Class current_class;
}; 

我们可以发现objc_super2中除了消息接受者receiver,另一个成员变量current_class也就是当前类对象。

与我们上面分析的不同_objc_msgSendSuper2函数内其实传入的是当前类对象,然后在函数内部获取当前类对象的父类,并且从父类开始查找方法。

我们也可以通过代码验证上述结构体内成员变量究竟是当前类对象还是父类对象。下文中我们会通过另外一道面试题验证。

2.isKindOfClass 与 isMemberOfClass

首先看一下isKindOfClass isKindOfClass对象方法底层实现

- (BOOL)isMemberOfClass:(Class)cls {
   // 直接获取实例类对象并判断是否等于传入的类对象
    return [self class] == cls;
}

- (BOOL)isKindOfClass:(Class)cls {
   // 向上查询,如果找到父类对象等于传入的类对象则返回YES
   // 直到基类还不相等则返回NO
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
} 

isKindOfClass isKindOfClass类方法底层实现

// 判断元类对象是否等于传入的元类元类对象
// 此时self是类对象 object_getClass((id)self)就是元类
+ (BOOL)isMemberOfClass:(Class)cls {
    return object_getClass((id)self) == cls;
}

// 向上查找,判断元类对象是否等于传入的元类对象
// 如果找到基类还不相等则返回NO
// 注意:这里会找到基类
+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
} 

通过上述源码分析我们可以知道。 isMemberOfClass 判断左边是否刚好等于右边类型。 isKindOfClass 判断左边或者左边类型的父类是否刚好等于右边类型。 注意:类方法内部是获取其元类对象进行比较

我们查看以下代码

NSLog(@"%d",[Person isKindOfClass: [Person class]]);
NSLog(@"%d",[Person isKindOfClass: object_getClass([Person class])]);
NSLog(@"%d",[Person isKindOfClass: [NSObject class]]);

// 输出内容
Runtime-super[46993:5195901] 0
Runtime-super[46993:5195901] 1
Runtime-super[46993:5195901] 1 

分析上述输出内容: 第一个 0:上面提到过类方法是获取self的元类对象与传入的参数进行比较,但是第一行我们传入的是类对象,因此返回NO。

第二个 1:同上,此时我们传入Person元类对象,此时返回YES。验证上述说法

第三个 1:我们发现此时传入的是NSObject类对象并不是元类对象,但是返回的值却是YES。 原因是基元类的superclass指针是指向基类对象的。如下图13号线

isa, gráfico señalador de superclase

那么Person元类通过superclass指针一直找到基元类,还是不相等,此时再次通过superclass指针来到基类,那么此时发现相等就会返回YES了。

3. 复习

通过一道面试题对之前学习的知识进行复习。 问:以下代码是否可以执行成功,如果可以,打印结果是什么。

// Person.h
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
- (void)test;
@end

// Person.m
#import "Person.h"
@implementation Person
- (void)test
{
    NSLog(@"test print name is : %@", self.name);
}
@end

// ViewController.m
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    id cls = [Person class];
    void *obj = &cls;
    [(__bridge id)obj test];
    
    Person *person = [[Person alloc] init];
    [person test];
} 

这道面试题确实很无厘头的一道题,日常工作中没有人这样写代码,但是需要解答这道题需要很完备的底层知识,我们通过这道题来复习一下,首先看一下打印结果。

Runtime面试题[15842:2579705] test print name is : <ViewController: 0x7f95514077a0>
Runtime面试题[15842:2579705] test print name is : (null) 

通过上述打印结果我们可以看出,是可以正常运行并打印的,说明obj可以正常调用test方法,但是我们发现打印self.name的内容却是<ViewController: 0x7f95514077a0>。下面person实例调用test不做过多解释了,主要用来和上面方法调用做对比。

为什么会是这样的结果呢?首先通过一张图看一下两种调用方法的内存信息。

Información de memoria para los dos métodos de llamada

通过上图我们可以发现两种方法调用方式很相近。那么obj为什么可以正常调用方法?

3.1 obj为什么可以正常调用方法

首先通过之前的学习我们知道,person调用方法时首先通过isa指针找到类对象进而查找方法并进行调用。

person实例对象内实际上是取最前面8个字节空间也就是isa并通过计算得出类对象地址。

而通过上图我们可以发现,obj在调用test方法时,也会通过其内存地址找到cls,而cls中取出最前面8个字节空间其内部存储的刚好是Person类对象地址。因此obj是可以正常调用方法的。

3.2 为什么self.name打印内容为ViewController对象

问题出在[super viewDidLoad];这段代码中,通过上述对super本质的分析我们知道,super内部调用objc_msgSendSuper2函数。

我们知道objc_msgSendSuper2函数内部会传入两个参数,objc_super2结构体和SEL,并且objc_super2结构体内有两个成员变量消息接受者和其父类。

struct objc_super2 {
    id receiver; // 消息接受者
    Class current_class; // 当前类
};
}; 

通过以上分析我们可以得知[super viewDidLoad];内部objc_super2结构体内存储如下所示

struct objc_super = {
    self,
    [ViewController Class]
}; 

那么objc_msgSendSuper2函数调用之前,会先创建局部变量objc_super2结构体用于为objc_msgSendSuper2函数传递的参数。

3.3 局部变量由高地址向低地址分配在栈空间

我们知道局部变量是存储在栈空间内的,并且是由高地址向低地址有序存储。 我们通过一段代码验证一下。

long long a = 1;
long long b = 2;
long long c = 3;
NSLog(@"%p %p %p", &a,&b,&c);
// 打印内容
0x7ffee9774958 0x7ffee9774950 0x7ffee9774948 

通过上述代码打印内容,我们可以验证局部变量在栈空间内是由高地址向低地址连续存储的。

那么我们回到面试题中,通过上述分析我们知道,此时代码中包含局部变量以此为objc_super2 结构体clsobj。通过一张图展示一下这些局部变量存储结构。

estructura de almacenamiento de variables locales

上面我们知道当person实例对象调用方法的时候,会取实例变量前8个字节空间也就是isa来找到类对象地址。那么当访问实例变量的时候,就跳过isa的8个字节空间往下面去找实例变量。

那么当obj在调用test方法的时候同样找到cls中取出前8个字节,也就是Person类对象的内存地址,那么当访问实例变量_name的时候,会继续向高地址内存空间查找,此时就会找到objc_super结构体,从中取出8个字节空间也就是self,因此此时访问到的self.name就是ViewController对象

当访问成员变量_name的时候,test函数中的self也就是方法调用者其实是obj,那么self.name就是通过obj去找_name,跳过cls的8个指针,在取8个指针此时自然获取到ViewController对象

因此上述代码中cls就相当于isaisa下面的8个字节空间就相当于_name成员变量。因此成员变量_name的访问到的值就是cls地址后向高地址位取8个字节地址空间存储的值。

为了验证上述说法,我们做一个实验,在cls后高地址中添加一个string,那么此时cls下面的高地址位就是string。以下示例代码

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSString *string = @"string";
    
    id cls = [Person class];
    void *obj = &cls;
    [(__bridge id)obj test];
    
    Person *person = [[Person alloc] init];
    [person test];
} 

此时的局部变量内存结构如下图所示

estructura de memoria variable local

此时在访问_name成员变量的时候,越过cls内存往高地址找就会来到string,此时拿到的成员变量就是string了。 我们来看一下打印内容

Runtime面试题[16887:2829028] test print name is : string
Runtime面试题[16887:2829028] test print name is : (null) 

再通过一段代码使用int数据进行试验

- (void)viewDidLoad {
    [super viewDidLoad];

    int a = 3;
    
    id cls = [Person class];
    void *obj = &cls;
    [(__bridge id)obj test];
    
    Person *person = [[Person alloc] init];
    [person test];
}
// 程序crash,坏地址访问 

我们发现程序因为坏地址访问而crash,此时局部变量内存结构如下图所示

estructura de memoria variable local

当需要访问_name成员变量的时候,会在cls后高地址为查找8位的字节空间,而我们知道int占4位字节,那么此时8位的内存空间同时占据int数据及objc_super结构体内,因此就会造成坏地址访问而crash。

我们添加新的成员变量进行访问

// Person.h
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *nickName;
- (void)test;
@end
------------
// Person.m
#import "Person.h"
@implementation Person
- (void)test
{
    NSLog(@"test print name is : %@", self.nickName);
}
@end
--------
//  ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];

    NSObject *obj1 = [[NSObject alloc] init];
    
    id cls = [Person class];
    void *obj = &cls;
    [(__bridge id)obj test];
    
    Person *person = [[Person alloc] init];
    [person test];
} 

我们看一下打印内容

// 打印内容
// Runtime面试题[17272:2914887] test print name is : <ViewController: 0x7ffc6010af50>
// Runtime面试题[17272:2914887] test print name is : (null) 

可以发现此时打印的仍然是ViewController对象,我们先来看一下其局部变量内存结构

estructura de memoria variable local

首先通过obj找到clscls找到类对象进行方法调用,此时在访问nickName时,obj查找成员变量,首先跳过8个字节的cls,之后跳过name所占的8个字节空间,最终再取8个字节空间取出其中的值作为成员变量的值,那么此时也就是self了。

总结:这道面试题虽然很无厘头,让人感觉无从下手但是考察的内容非常多。 1. super的底层本质为调用objc_msgSendSuper2函数,传入objc_super2结构体,结构体内部存储消息接受者和当前类,用来告知系统方法查找从父类开始。

2. 局部变量分配在栈空间,并且从高地址向低地址连续分配。先创建的局部变量分配在高地址,后续创建的局部变量连续分配在较低地址。

3. 方法调用的消息机制,通过isa指针找到类对象进行消息发送。

4. El puntero almacena la dirección del primer byte de la variable de instancia En el ejemplo anterior, el personpuntero almacena la dirección del puntero dentro de la variable de instancia isa.

5. Acceda a la esencia de la variable miembro, encuentre la dirección de la variable miembro y saque el valor de la variable miembro almacenada en la dirección de acuerdo con la cantidad de bytes ocupados por la variable miembro.

3.4 Verificar los parámetros de estructura pasados ​​en objc_msgSendSuper2

Usamos el siguiente código para verificar los problemas restantes anteriores

- (void)viewDidLoad {
    [super viewDidLoad];
    id cls = [Person class];
    void *obj = &cls;
    [(__bridge id)obj test];
} 

La estructura de memoria variable local del código anterior se ha analizado anteriormente, y la estructura de memoria real debe ser como se muestra en la siguiente figura

estructura de memoria variable local

A través del análisis de las preguntas de la entrevista anterior, ahora queremos verificar objc_msgSendSuper2los parámetros de estructura pasados ​​en la función. Solo necesitamos obtener clsla dirección y luego retroceder 8 direcciones para obtener la objc_superestructura en la estructura self. Después de retroceder 8 direcciones es current_classla dirección de memoria. A través del contenido impreso current_class, puede saber si objc_msgSendSuper2el objeto de clase actual o el objeto de clase principal se pasan a la función.

Demostremos que es UIViewControllero ViewControllerpuede

La clase actual se pasa a la estructura.

Como se puede ver en la figura anterior, el contenido final impreso es, de hecho, el objeto de clase actual. Por lo tanto objc_msgSendSuper2, el objeto de la clase actual en realidad se pasa dentro de la función, y su clase principal se obtiene dentro de la función, lo que le indica al sistema que comience a buscar desde el método de la clase principal.

Supongo que te gusta

Origin juejin.im/post/7116147057739431950
Recomendado
Clasificación