iOS中objc_msgSend流程学习

一,前言

在iOS开发过程中,我们都知道不管是什么方法的执行对象的创建,以及代理Block的实现都离不开runtime,所以runtime可以说是iOS开发过程中的生命存在, 运行时 存在 动态决议 的作用,例如我们在一个类的声明中声明了相关的方法,但是并没有进行实现时,进行编译是不会有任何问题的,但是运行时就会报错,告知我们没有实现该方法。接下来我们就重点研究一下运行时为什么会只能的告诉我们没有实现该方法。

二,环境配置

首先我们在main.m 中声明一个类LGTeacher集成自NSObject,声明一个方法sayHello,再次声明一个类LGPerson继承自LGTeacher,在LGPerson中重写了父类的sayHello,以及从新声明了sayNB,
代码如下

@interface LGTeacher : NSObject
- (void)sayHello;
@end

@implementation LGTeacher
- (void)sayHello{
    NSLog(@"666");
}
@end

@interface LGPerson : LGTeacher
- (void)sayHello;
- (void)sayNB;
@end

@implementation LGPerson
- (void)sayNB{
    NSLog(@"666");
}
@end

此时我们在main.m 中进行创建相应的对象;并调用相应的方法:

       LGPerson *person = [LGPerson alloc];
        
        LGTeacher *teacher = [LGTeacher alloc];
        
        [teacher sayHello];
        
        [person sayNB];

此时我们对该文件进行相应的clang命令,生成应的.cpp文件,对该文件进行查看,我们能发现在运行时runtime对我们的所有方法进行相关的处理;这就是runtime的作用;
以上的代码在运行时编译成

          LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
  
        LGTeacher *teacher = ((LGTeacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGTeacher"), sel_registerName("alloc"));

        ((void (*)(id, SEL))(void *)objc_msgSend)((id)teacher, sel_registerName("sayHello"));

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

也就是说我们在创建对象的时候存在两种对应的关系;

  • alloc 对应的是 sel_registerName("alloc")
  • 方法调用 对应是 sel_registerName("方法名"));

我们在控制台打印的结果是

2020-09-20 23:01:12.225822+0800 001-运行时感受[13893:304936] 666
2020-09-20 23:01:12.226333+0800 001-运行时感受[13893:304936] 666

然后我们在用runtime的形式进行创建方法,改写原来的方法调用过程;

       [teacher sayHello];
        
        objc_msgSend(teacher, sel_registerName("sayHello"));
        
        [person sayNB];
        
        objc_msgSend(person, sel_registerName("sayNB"));

我们能看到这两组打印结果完全是一样的
所以我们得出的结论是
[teacher sayHello]objc_msgSend(teacher, sel_registerName("sayHello")); 完全等价;那么为什么会是这样的,这就是我们接下来重点研究的对象objc_msgSend的查找流程

三,查找流程

objc_msgSend的方法查找过程中存在两种查找流程,一种是带缓存的,一种是不带缓存,也即是快速和慢速的两种情况,因为我们上一节学习了一个类的中的相关的cache_t的部分,并做了相关的详细的介绍;所以里边涉及了很多方法的存储和对应的imp的存储过程。那么接下来我们分别对两个查找流程进行一个学习和分析。

1、快速查找

我们知道不管我们是何种语言实现的代码,在底层都会编译为计算机能识别的语言,也就是二进制的代码;那么计算机为什么能将我们写的代码转换为二进制语言了,这就是计算机最高效的一种语言汇编,这就是计算机能快速识别我们代码的根本原因,那么我们就对汇编查找流程进行一个分析和学习吧;

首先引入一个相关汇编指令介绍


iOS中objc_msgSend流程学习_第1张图片
汇编部分指令.png

我们打开开源代码0bjc781,进行编译过后进入相关的汇编代码;进入汇编文件

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

    cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif
    ldr p13, [x0]       // p13 = isa
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend

汇编解析:

  • 1 ENTRY _objc_msgSend 进入汇编进行方法查找入口;
  • 2 NoFrame 进入无窗口调试模式;
  • 3 cmp p0, #0 进行判断类是否是空;如果是空这进入LReturnZero,否则进入 LNilOrTagged
  • 4 ldr p13, [x0][x0] 中的信息读取到p13 即将isa赋值给 p13, p13= 该类的isa
  • 5 获取isa 进行关联类对象,相当于alloc流程中的initWithIsa
  • 6 CacheLookup NORMAL, _objc_msgSend 获取完isa 后,进行正常的消息转发过程;

CacheLookup的定义是什么? 所以带着刨根问底的理念全局搜索这个关键字,

CacheLookup 的查找有三种格式

  • 1 NORMAL
  • 2 GETIMP
  • 3 LOOKUP
    接下来我们着重分析一下 NORMAL 其他两种情况剩余时间再去学习总结,也算是一个学习的过程
    我们找到相关的CacheLookup定义 ;代码如下

.macro CacheLookup

    
LLookupStart$1:

    // p1 = SEL, p16 = isa
    ldr p11, [x16, #CACHE]              // p11 = mask|buckets

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
    and p12, p1, p11, LSR #48       // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    and p10, p11, #~0xf         // p10 = buckets
    and p11, p11, #0xf          // p11 = maskShift
    mov p12, #0xffff
    lsr p11, p12, p11               // p11 = mask = 0xffff >> p11
    and p12, p1, p11                // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif


    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

    ldp p17, p9, [x12]      // {imp, sel} = *bucket
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

3:  // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
                    // p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    add p12, p12, p11, LSL #(1+PTRSHIFT)
                    // p12 = buckets + (mask << 1+PTRSHIFT)

.endmacro

汇编分析

  • 1 ldr p11, [x16, #CACHE] 我们找到#CACHE 的定义是

#define CACHE (2 * __SIZEOF_POINTER__)

__SIZEOF_POINTER__ 我们都知道是8,所以 #CACHE 是16;
根据汇编语句也就是 ldr p11, [x16, 16] 也就是 x16 平移16位也就是顺着ISA平移16位到Cache_t的位置,也就是正如备注说的 p11 = mask|buckets 也就相当于上次文章中介绍的找到相关的buckets的索引位置;index

的内部结构

  • 2 p10, p11, #0x0000ffffffffffff 通过掩码找出取出缓存中的buckets 也就是 p11 = mask|buckets;
  • 3 and p12, p1, p11, LSR #48 因为 p11, LSR #48就相当于
 static constexpr uintptr_t maskShift = 48;
    
    // Additional bits after the mask which must be zero. msgSend
    // takes advantage of these additional bits to construct the value
    // `mask << 4` from `_maskAndBuckets` in a single instruction.
    static constexpr uintptr_t maskZeroBits = 4;
    
    // The largest mask value we can store.
    static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
    
    // The mask applied to `_maskAndBuckets` to retrieve the buckets pointer.
    static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) -

取出mask 在和p12 进行按位与操作 也就是x12 = _cmd & mask

  • 4 add p12, p10, p12, LSL #(1+PTRSHIFT) 因为 p12, LSL #(1+PTRSHIFT) 我们找到PTRSHIFT 的定义如下

#define PTRSHIFT 3
也就是p12, LSL #4

也就是 p12左移4位也就是 左移16; p12 = *(buckets + index) *16 也就是找到相应位置的bucket;

  • 5 ldp p17, p9, [x12] // {imp, sel} = *bucket取出第四步骤中取出的buckect中的selimp

  • 6 拿到相应的Sel 和IMP 和我们调用的方法进行对比,如果没有找到则跳转到2 找到那么就进入CacheHit定义

.macro CacheHit
.if $0 == NORMAL
    TailCallCachedImp x17, x12, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP
    mov p0, p17
    cbz p0, 9f          // don't ptrauth a nil imp
    AuthAndResignAsIMP x0, x12, x1, x16 // authenticate imp and re-sign as IMP
9:  ret             // return IMP
.elseif $0 == LOOKUP
    // No nil check for ptrauth: the caller would crash anyway when they
    // jump to a nil IMP. We don't care if that jump also fails ptrauth.
    AuthAndResignAsIMP x17, x12, x1, x16    // authenticate imp and re-sign as IMP
    ret             // return imp via x17
.else
.abort oops
.endif
.endmacro
  • 7 再次没查找到的时候向前查找所有的buckets,如果都没找到,则进入 CheckMiss :定义
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz p9, LGetImpMiss
.elseif $0 == NORMAL
    cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

这就是整个快速查找过程,

2、慢速查找

在此过程中我们知道调用方法就是获取某个方法的IMP.所以我们找到项目的 class_getMethodImplementation 方法,在详细研究相关的流程情况,代码定义如下

IMP class_getMethodImplementation(Class cls, SEL sel)
{
    IMP imp;

    if (!cls  ||  !sel) return nil;

    imp = lookUpImpOrNil(nil, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);

    // Translate forwarding function to C-callable external version
    if (!imp) {
        return _objc_msgForward;
    }

    return imp;
}

再次寻根究底的探索相关的获取imp的重点方法lookUpImpOrNil;再次进入

static inline IMP
lookUpImpOrNil(id obj, SEL sel, Class cls, int behavior = 0)
{
    return lookUpImpOrForward(obj, sel, cls, behavior | LOOKUP_CACHE | LOOKUP_NIL);
}

再次顺腾摸瓜进入 lookUpImpOrForward

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;
    runtimeLock.assertUnlocked();
    // Optimistic cache lookup
    if (fastpath(behavior & LOOKUP_CACHE)) {
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;
    }

    runtimeLock.lock();

    checkIsKnownClass(cls);

    if (slowpath(!cls->isRealized())) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
     
    }

    if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
     
    }

    runtimeLock.assertLocked();
    curClass = cls;

    for (unsigned attempts = unreasonableClassCount();;) {
        // curClass method list.
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            imp = meth->imp;
            goto done;
        }

        if (slowpath((curClass = curClass->superclass) == nil)) {
            // No implementation found, and method resolver didn't help.
            // Use forwarding.
            imp = forward_imp;
            break;
        }

        // Halt if there is a cycle in the superclass chain.
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // Superclass cache.
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            // Found a forward:: entry in a superclass.
            // Stop searching, but don't cache yet; call method
            // resolver for this class first.
            break;
        }
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
    }

    // No implementation found. Try method resolver once.

    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
    log_and_fill_cache(cls, imp, sel, inst, curClass);
    runtimeLock.unlock();
 done_nolock:
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

代码解析过程;

  • 1 首先从缓存中取出相应的imp,我们知道快速查找流程就是没找到相应的缓存;所以此处不可能存在imp.所以直接将IMP设置为nil;
  • 2 再次检查是否是我们当前的类对象;
  • 3 如果当前类是否是运行相应的查找行为权限,如果有就继续查找;
  • 4 如果当前类中没找到相应的方法,继续从父类方法列表中去查找,直到找到NSObject 为止,
  • 5 如果找到;那么就将该方法列表缓存起来,为了下次能快速的查找,
  • 6 如果慢速都没找到,直到返回nil的时候,那么就要进行动态方法解析。

这就是所有慢速查找流程的核心,通过上述流程就能体现objec_msgSend的流程执行。

四,总结;

慢速查找的原理就是在c/c++层面去进行,只是做的事情是找到就进行缓存操作,也是反复的递归找出我们所需的方法实现imp ,如果没找到包括快速和慢速都没有,那么后期将会继续判断是否允许动态解析,方法决议等,如果不允许,则程序就会报错,告知我们这个方法没有实现,这就是这个objc_msgSend的流程,

你可能感兴趣的:(iOS中objc_msgSend流程学习)