OC底层原理

方法的本质

2019-12-29  本文已影响0人  只写Bug程序猿

探索方法的本质

一个最基本的方法调用代码

void run(){
    NSLog(@"%s",__func__);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [[LGPerson alloc] init];;
        [person sayNB];
    }
    return 0;
}

方法的调用底层到底是个什么东西呢
我们可以利用clang的一些命令 clang -rewrite-objc main.m -o main.cpp,将main文件编译成c之后的代码

clang之后的代码
其中有个问题
run()方法直接调用,而我们的oc方法被编译成一个objc_msgSend函数(runtime里的消息发送机制)
run函数在编译器就确定了函数的调用与实现,
因此,oc的方法的本质就是objc_msgSend(或者objc_msgSendSuper等函数)的调用
objc_msgSend两个参数,第一个参数是对象是哪个对象的操作,第二个参数就是sel也就是方法,通过sel找到imp的实现,完成方法的调用.就叫做消息发送机制.

objc_msgSend流程

方法的查找流程分为两种

然后debug->debug WorkFlow ->Always Show Disassembly

查看汇编
然后进入汇编
汇编
然后按住cotrol + stepIn
源码位置
可以看到objc_msgSendlibobjc

打开源码搜索objc_msgSend,我们直接看汇编,找到.s文件,现在架构大部分都是arm64,所以我们直接看objc-msg-arm64.s文件
看汇编重要的一点事看ENTRY表示入口,如下图

找到文件 image.png
// person - isa - 类
    ldr p13, [x0]       // p13 = isa
GetClassFromIsa_p16 p13     // p16 = class

p13为isa,因为x0为首地址,[]就是首地址的值,首地址就是isa指针
GetClassFromIsa_p16是一个宏,下边是实现

.macro GetClassFromIsa_p16 /* src */

#if SUPPORT_INDEXED_ISA
    // Indexed isa
    mov p16, $0         // optimistically set dst = src
    tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f  // done if not non-pointer isa
    // isa in p16 is indexed
    adrp    x10, _objc_indexed_classes@PAGE
    add x10, x10, _objc_indexed_classes@PAGEOFF
    ubfx    p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
    ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:

#elif __LP64__
    // 64-bit packed isa
    and p16, $0, #ISA_MASK

#else
    // 32-bit raw isa
    mov p16, $0

#endif

.endmacro

SUPPORT_INDEXED_ISAindexIsa一般不常用,watchos开发是indexIsa
and p16, $0, #ISA_MASK这句才是重点,$0是传进来的参数,也就是p13 isa 拿$0与isa_mask进行&运算得到类,在前两篇文章中介绍了对象和类之间是怎么联系起来的,所以我们的p16是一个类
拿到isa之后进行下边操作

LGetIsaDone:
    CacheLookup NORMAL  

CacheLookup是一个宏定义,下边是实现代码

.macro CacheLookup
    // p1 = SEL, p16 = isa
    ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
    and w11, w11, 0xffff    // p11 = mask
#endif
    and w12, w1, w11        // x12 = _cmd & mask
    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
    add p12, p12, w11, UXTW #(1+PTRSHIFT)
                                // p12 = buckets + (mask << 1+PTRSHIFT)

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.

    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:  // double wrap
    JumpMiss $0
    
.endmacro
1:   cmp    p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp

b.ne 2f如果bucket的sel != _cmd找2,否则直接命中返回$0
快速查找流程结束,如果没有命中就进入慢速查找流程

2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f              //进行3主要是为了保存一份方便下次查找
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

3:  // wrap: p12 = first bucket, w11 = mask   
    add p12, p12, w11, UXTW #(1+PTRSHIFT)
                                // p12 = buckets + (mask << 1+PTRSHIFT)

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.

    ldp p17, p9, [x12]      // {imp, sel} = *bucket

CheckMiss也是一个宏

.macro 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

我们这里传进来的是normal,那么找__objc_msgSend_uncached
我们搜索__objc_msgSend_uncached找到他的入口

STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band p16 is the class to search
    
    MethodTableLookup
    TailCallFunctionPointer x17

    END_ENTRY __objc_msgSend_uncached

来看一下MethodTableLookup是什么东西
他也是一个宏定义

.macro MethodTableLookup
...
bl  __class_lookupMethodAndLoadCache3
...

这里只展示了一个最重要的一行代码,bl跳转到__class_lookupMethodAndLoadCache3
我们根据以往经验,汇编会自动在前边加一个_,那么我们去掉一个下划线全局搜_class_lookupMethodAndLoadCache3

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

来一个demo


@interface LGPerson : NSObject

- (void)sayNB;
+ (void)sayHappay;

@end
@implementation LGPerson

- (void)sayNB{
    NSLog(@"%s",__func__);
}

+ (void)sayHappay{
    NSLog(@"%s",__func__);
}
@end
@interface LGStudent : LGPerson
- (void)sayHello;
+ (void)sayObjc;
@end
@implementation LGStudent
- (void)sayHello{
    NSLog(@"%s",__func__);
}

+ (void)sayObjc{
    NSLog(@"%s",__func__);
}
@end
@interface NSObject (LG)
- (void)sayMaster;
+ (void)sayEasy;
@end
@implementation NSObject (LG)

- (void)sayMaster{
    NSLog(@"%s",__func__);
}
+ (void)sayEasy{
    NSLog(@"%s",__func__);
}

@end

LGStuednt继承与LGPerson,LGPerson继承与NSObject
然后调用

LGStudent *student = [[LGStudent alloc] init];
        // 对象方法
        // 自己有 - 返回自己
        [student sayHello];
        // 自己没有 - 老爸 -
        // [person sayNB]; // CACHE
        
        [student sayNB];
        // 自己没有 - 老爸没有 - NSObject
        [student sayMaster];
        // 自己没有 - 老爸没有 - NSObject 没有
        // unrecognized selector sent to instance 0x103000450
       [student performSelector:@selector(saySomething)];

类方法的调用

// 类方法
        // 自己有 - 返回自己
        [LGStudent sayObjc];
        // 自己没有 - 老爸 -
        [LGStudent sayHappay];
        // 自己没有 - 老爸没有 - NSObject
        [LGStudent sayEasy];
        // 自己没有 - 老爸没有 - NSObject 没有

这里会有一个问题如果我这样调用会不会崩溃

 [LGStudent performSelector:@selector(sayMaster)];

会打印

-[NSObject(LG) sayMaster]

发现不会蹦,因为类方法存在元类里边,我们调用类方法

isa流程图.png
最后找到sayMaster,所以不会崩溃
看下源码来验证下到底是不是这个流程
我们上边看汇编,如果缓存没有命中就来到慢速查找流程,也就是_class_lookupMethodAndLoadCache3方法
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search to make
    // method-lookup + cache-fill atomic with respect to method addition.
    // Otherwise, a category could be added but ignored indefinitely because
    // the cache was re-filled with the old value after the cache flush on
    // behalf of the category.

    runtimeLock.lock();
    checkIsKnownClass(cls);

    if (!cls->isRealized()) {
        realizeClass(cls);
    }

    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlock();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.lock();
        // If sel == initialize, _class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }

    
 retry:    
    runtimeLock.assertLocked();

    // Try this class's cache.

    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // Try this class's method lists.
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // Try superclass caches and method lists.
    {
        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.");
            }
            
            // 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;
                }
                else {
                    // Found a forward:: entry in a superclass.
                    // Stop searching, but don't cache yet; call method 
                    // resolver for this class first.
                    break;
                }
            }
            
            // Superclass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.lock();
        // 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;
    }
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);
 done:
    runtimeLock.unlock();
    return imp;
}

因为是慢速查找,这里传进来的cache为NO,所以下边这几行代码不看,

if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

这里加了一把锁,是防止多线程情况下产生错乱,比如同时调用a方法和b方法,那么这里在调用a的时候返回一个b的imp,会产生问题
checkIsKnownClass,是判断类是否合法

 if (!cls->isRealized()) {
        realizeClass(cls);
    }

这行代码是拿到父类元类,以及类的data里边的rw里的ro里的methodlist等等一系列信息,是为方法查找做准备条件,这里不做重点研究.

// Try this class's method lists.
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

先在本类里边找,getMethodNoSuper_nolock

getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    assert(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?

    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

拿到类的data里的methodList进行循环遍历,
search_method_list进行二分法查找,查找速度更快,下边为查找算法,这里不做过多研究

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;
    
    for (count = list->count; count != 0; count >>= 1) {
        probe = base + (count >> 1);
        
        uintptr_t probeValue = (uintptr_t)probe->name;
        
        if (keyValue == probeValue) {
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                probe--;
            }
            return (method_t *)probe;
        }
        
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}

通过一些列算法找到方法,然后看

if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }

如果找到了method那么下次还慢速查找么,苹果爸爸肯定不会这样蠢的,如果找到就调用log_and_fill_cache进行缓存,下次直接利用汇编快速查找,缓存的操作跟我们研究cache_t结构时是一模模一样样的.
goto done 然后查找结束

如果找不到呢,那么就去找父类Try superclass caches and method lists.根据代码注释我们也可以看出来接下来要找父类了.
如果找父类,就不开始跳汇编了,因为刚开始父类元类等条件我们已经准备好了,那么我们现在直接找父类的cache.

// Superclass cache.
            imp = cache_getImp(curClass, sel);

如果找到imp,直接goto done直接返回,如果找不到,然后找到父类的方法列表进行查找(流程同在本类中查找流程)
如果都找不到方法呢,直接报错我们很熟悉的一个错误+[LGStudent sayLove]: unrecognized selector sent to class 0x1000012e8.
imp = (IMP)_objc_msgForward_impcache;看这句代码.发现点不进去.那么按照国际惯例,全局搜索
然后选择objc_msg_arm64.s没错又是恶心人的汇编,为什么找他呢,因为其他地方都是调用,没有实现,按照经验STATIC_ENTRY __objc_msgForward_impcache ,是不是很熟悉

STATIC_ENTRY __objc_msgForward_impcache

    // No stret specialization.
    b   __objc_msgForward

    END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward

    adrp    x17, __objc_forward_handler@PAGE
    ldr p17, [x17, __objc_forward_handler@PAGEOFF]
    TailCallFunctionPointer x17
    
    END_ENTRY __objc_msgForward

__objc_forward_handler是个啥玩意儿啊,来搜一下(经验告诉我搜不到的时候去掉一个下划线),

void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

发现他是个这么个玩意儿objc_defaultForwardHandler,点进去看一下我的天啊,出现了好熟悉的代码

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);
}

这不就是经常出现的报错原因么
但是找不到方法直接报错,体验很不好,有没有什么方法补救一下呢?
苹果爸爸告诉我们,可以的,在给你一次机会.可以利用消息转发机制
https://www.jianshu.com/p/03383d2d395d

上一篇下一篇

猜你喜欢

热点阅读