iOS runtime之objc_msgSend慢速查找流程

前言

前文iOS runtime之方法的本质objc_msgSend分析一探索了objc_msgSend的缓存查找(快速查找)流程,本文将接着探索没有缓存时的方法列表查找(慢速查找)流程。

1: __objc_msgSend_uncached流程分析

objc_msgSend根据sel在类或元类的缓存中查找IMP时,如果缓存中没有查找到相应的IMP,就会跳转MissLabelDynamic__objc_msgSend_uncached流程,在方法列表里面查找相应的IMP

objc4-818.2源码里全局搜索__objc_msgSend_uncached,然后按住command,鼠标左键点击文件名旁边的小箭头,收起所有文件,选择objc-msg-arm64.s文件点开,选择STATIC_ENTRY __objc_msgSend_uncached开始查看__objc_msgSend_uncached的汇编源码。

备注:支持__has_feature(ptrauth_calls)指针身份验证功能的情况(A12及之后的仿生芯片),本文不做分析。

图解:

image.png

1.1: __objc_msgSend_uncached汇编源码

    STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band p15 is the class to search
    
    // 方法列表查找_cmd对应的imp
    MethodTableLookup
    // x17 = IMP,调用IMP
    TailCallFunctionPointer x17

    END_ENTRY __objc_msgSend_uncached
复制代码
  • MethodTableLookup根据x1_cmd在类或元类的方法列表里查找对应的IMP
  • TailCallFunctionPointer调用查找到的IMP

1.2: MethodTableLookup汇编源码

.macro MethodTableLookup
	
    SAVE_REGS MSGSEND
    
    // /* method lookup */
    // enum {
    //     LOOKUP_INITIALIZE = 1,
    //     LOOKUP_RESOLVER = 2,
    //     LOOKUP_NIL = 4,
    //     LOOKUP_NOCACHE = 8,
    // };
    
    // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    // receiver and selector already in x0 and x1
    // x2 = x16 = class
    mov	x2, x16
    // x3 = LOOKUP_INITIALIZE | LOOKUP_RESOLVER = 3
    mov	x3, #3
    // 官方的注释说明了一切,调用lookUpImpOrForward函数
    // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    // bl:b:跳转 l:链接寄存器 //在跳转到之前_lookUpImpOrForward之前,
    // 将下一条指令的地址保存到lr寄存器中,既将(mov x17, x0)的指令地址保存在lr中
    // 当_lookUpImpOrForwar执行完以后,执行lr寄存器中的地址
    bl	_lookUpImpOrForward
    
    // x0即是第一个寄存器,同时也是返回值寄存器,此处是_lookUpImpOrForward的返回值IMP
    // IMP in x0
    // x17 = x0 = IMP
    mov	x17, x0

    RESTORE_REGS MSGSEND

.endmacro
复制代码
  • 官方注释说明了一切,x0receiverx1selectormov x2, x16,将x16(类或元类)赋值给x2mov x3, #3,将x3赋值为3,然后调用函数lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
  • mov x17, x0x0即是第一个寄存器,同时也是返回值寄存器,此处是_lookUpImpOrForward的返回值IMP,将查找到的IMP赋值给x17

1.3: TailCallFunctionPointer汇编源码

#if __has_feature(ptrauth_calls)
// JOP

.macro TailCallFunctionPointer
    // $0 = function pointer value
    braaz	$0
.endmacro
// JOP
#else
// not JOP
.macro TailCallFunctionPointer
    // $0 = function pointer value
    // 跳转$0,即调用IMP
    br	$0
.endmacro
// not JOP
#endif
复制代码
  • br $0,跳转$0,即调用IMP

1.4: __objc_msgSend_uncached流程图

__objc_msgSend_uncached流程图.jpg

2: lookUpImpOrForward函数流程

前面汇编源码里官方注释已经指明MethodTableLookup里的_lookUpImpOrForward调用的是lookUpImpOrForward函数,全局搜索_lookUpImpOrForward也只能在汇编文件里找到调用代码,没有实现代码,所以全局搜索lookUpImpOrForward函数,按住command点击文件名前面的小箭头折叠所有文件,然后再次点击objc-runtime-new.mm文件的小箭头,打开并找到lookUpImpOrForward函数查看相关源码。

image.png

2.1: lookUpImpOrForward函数源码解析

NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    // behavior = 3 = LOOKUP_INITIALIZE | LOOKUP_RESOLVER
    // 指定消息转发的forward_imp
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();
    
    // 判断类或元类是否初始化,如果没有初始化,
    // behavior = LOOKUP_INITIALIZE | LOOKUP_RESOLVER | LOOKUP_NOCACHE = 11
    // 二进制为1011
    if (slowpath(!cls->isInitialized())) {
        // The first message sent to a class is often +new or +alloc, or +self
        // which goes through objc_opt_* or various optimized entry points.
        //
        // However, the class isn't realized/initialized yet at this point,
        // and the optimized entry points fall down through objc_msgSend,
        // which ends up here.
        //
        // We really want to avoid caching these, as it can cause IMP caches
        // to be made with a single entry forever.
        //
        // Note that this check is racy as several threads might try to
        // message a given class for the first time at the same time,
        // in which case we might cache anyway.
        behavior |= LOOKUP_NOCACHE;
    }

    // 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();

    // We don't want people to be able to craft a binary blob that looks like
    // a class but really isn't one and do a CFI attack.
    //
    // To make these harder we want to make sure this is a class that was
    // either built into the binary or legitimately registered through
    // objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
    
    // 检查类是否注册(是否是被dyld加载的类),防止被伪装的类进行攻击
    checkIsKnownClass(cls);
    
    // 递归实现类、父类和元类
    // 初始化类和父类
    // 此处不分析,后面单独发文分析类的加载和初始化
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    // runtimeLock may have been dropped but is now locked again
    runtimeLock.assertLocked();
    curClass = cls;

    // The code used to lookup the class's cache again right after
    // we take the lock but for the vast majority of the cases
    // evidence shows this is a miss most of the time, hence a time loss.
    //
    // The only codepath calling into this without having performed some
    // kind of cache lookup is class_getInstanceMethod().
    // 获取锁后,代码再次查找类的缓存,但绝大多数情况下,证据表明大部分时间都未命中,因此浪费时间。
    // 唯一没有执行某种缓存查找的代码路径就是class_getInstanceMethod()。
    
    // unreasonableClassCount,类迭代上限,函数注释翻译得到
    // 死循环根据sel查找IMP,根据break,goto等语句退出
    for (unsigned attempts = unreasonableClassCount();;) {
        // 判断是否有共享缓存缓存优化,一般是系统的方法比如NSLog,一般的方法不会走
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
            /*
            支持共享缓存,再次查询共享缓存,目的可能在你查询过程中
            别的线程可能调用了这个方法,共享缓存中有了
            */
            // 根据sel在缓存中查找IMP
            imp = cache_getImp(curClass, sel);
            // 找到IMP就跳转done_unlock流程
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
            // curClass method list.
            // 二分查找法在curClass中根据sel查找IMP
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {                 // 找到了sel对于的方法
                imp = meth->imp(false); // 获取对于的IMP
                goto done;              // 跳转done流程
            }
            
            // 获取父类,父类为nil,走if里面的流程,不为nil,就继续下面流程
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                // 按照继承链(cls->supercls->nil)一直查找到nil都没有查找到sel对应的IMP
                // 动态方法决议也没起作用,就开始消息转发
                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.
        // 父类缓存中根据sel查找IMP
        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.
            
            // 如果父类返回的是forward_imp,停止查找,跳出循环
            // 但是不要缓存,首先调用此类的动态方法决议(下面的resolveMethod_locked)
            break;
        }
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            // 在父类缓存中根据sel找到了IMP,进入done流程,在此类中缓存它。
            goto done;
        }
    }

    // No implementation found. Try method resolver once.
    // 未找到实现。尝试一次动态方法决议。
    // behavior = 3 = 0011  LOOKUP_RESOLVER = 2 = 0010
    // 0011 & 0010 = 0010 = 2,条件成立
    // 再次判断behavior = 1 == 0001,0001 & 0010 = 0,条件不成立
    // 动态方法决议只执行一次
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        // behavior = 3 ^ 2 = 0011 ^ 0010 = 0001 = 1
        behavior ^= LOOKUP_RESOLVER;
        // 动态方法决议
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done: // 在本类或父类方法列表中或者父类缓存中根据sel找到了IMP
    // 不是LOOKUP_NOCACHE,即不是+new or +alloc, or +self等方法
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES // 支持共享缓存,相关处理
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls->cache.preoptFallbackClass();
        }
#endif
        // 将查询到的sel和IMP插入类的缓存  注意:插入的是消息接收者的类的缓存
        // 到这里就跟前面的cache_t探索的内容联系起来了
        // cache_t的读、写流程到这里就有了一个闭环
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
 done_unlock:
    // 解锁
    runtimeLock.unlock();
    /*
     如果(behavior & LOOKUP_NIL)成立
     imp == forward_imp,没有找到IMP,且动态方法决议没起作用
     直接返回nil
    */
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}
复制代码

2.1.1: 慢速查找流程分析

慢速查找流程

  • 检查receiver(消息接收者)的类是否注册,如果没注册直接报错。
  • 判断cls(类或元类)是否实现和初始化,如果没有就递归实现和初始化cls以及相关的继承链isa指向链中的父类元类(原因:因为方法查找时本类找不到,会向父类查找,直到没有父类为止;同时类方法存在元类中)。

递归循环查找

查找receiver(消息接收者)的类

  • 判断是否有共享缓存,因为可能在查询过程中,所查的方法被调用缓存了,如果有就直接从共享缓存中取,没有就开始在receiver(消息接收者)的类中查询。
  • 采用二分查找法在receiver(消息接收者)的类的方法列表中根据sel查找对应的IMP
  • 如果receiver(消息接收者)的类的方法列表中没找到IMP,就获取父类,开始递归父类查找。

查找父类(或父元类)的缓存(curClass = superclass

  • 如果父类链中有一个循环了,就报错并停止。
  • 父类缓存中根据sel查找IMP,如果在父类缓存中没找到了IMP,就开始查找父类方法列表。

查询父类(或父元类)的方法列表(curClass = superclass

  • 采用二分查找法在父类的方法列表中根据sel查找IMP,没找到就继续获取父类的父类,先查找缓存,再查找方法列表,一直递归到父类为nil为止。

方法存在

  • 如果在本类或父类链方法列表中或者父类缓存中根据sel找到了IMP,就跳出循环,判断是否需要插入缓存(+new or +alloc, or +self等方法不需要),需要的话就将IMPsel插入receiver(消息接收者)的类的缓存中。

动态方法决议

  • 如果父类链缓存中返回的是消息转发或者递归完所有父类之后都没有找到对应的IMP,则跳出循环,判断是否执行过动态方法决议,没有就执行一次动态方法决议,动态方法决议会再次调用慢速查找流程(后续不会再执行动态方法决议)。

消息转发

  • 如果一直查找到父类为nil都没有查找到对应的IMP,且动态方法决议也没起作用,就将消息转发的forward_imp插入缓存中,开始消息转发流程。

2.1.2: lookUpImpOrForward流程图

lookUpImpOrForward流程图.jpg

2.2: 实现和初始化类

static Class
realizeAndInitializeIfNeeded_locked(id inst, Class cls, bool initialize)
{
    runtimeLock.assertLocked();
    // !cls->isRealized()小概率发生 cls->isRealized()大概率是YES
    // 判断类是否实现 目的是实现isa指向链和父类链相关的类。
    if (slowpath(!cls->isRealized())) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
        // runtimeLock may have been dropped but is now locked again
    }
    
    // 判断类是否初始化,没有就先初始化
    if (slowpath(initialize && !cls->isInitialized())) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
        // runtimeLock may have been dropped but is now locked again

        // 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
    }
    return cls;
}
复制代码
  • 递归实现类以及类的isa指向链和父类链相关的类。
  • 初始化类和父类链相关的类(一直递归到nil)。

2.3: 二分查找法

ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
    ASSERT(list);

    auto first = list->begin(); // 第一个method的位置
    auto base = first;
    decltype(first) probe;
    
    // 将key直接转换成uintptr_t值,因为修复过后的method_list_t中的元素是排过序的
    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;
    
    // count = 数组的个数,count >> 1 相当于 count / 2 取整
    // count >>= 1 = (count = count >> 1) = (count = count / 2)
    /*
    案例一:例如 count = 8  需要查找的sel的index为2
     1(第一次).count = 8
     2(第二次).count = (8 >>= 1) = 4
    */
    /*
    案例二:例如 count = 8  需要查找的sel的index为7
     1(第一次).count = 8
     2(第二次).count = (7 >>= 1) = 3(下面count--了)
     3(第三次).count = (2 >>= 1) = 1(下面count--了)
    */
    for (count = list->count; count != 0; count >>= 1) {
        
        
        // 内存平移,获取探查值(中间值)
        /*
        案例一:
         1.probe = 首地址(0) + (count / 2) = 0 + 4
         2.prebe = 0 + (4 / 2) = 2
        */
        /*
        案例二:
         1.probe = 首地址(0) + (count / 2) = 0 + 4
         2.probe = 5 + (3 / 2) = 6
         3.probe = 7 + (1 / 2) = 7
        */
        probe = base + (count >> 1);
        
        // 获取探查的sel的uintptr_t值
        uintptr_t probeValue = (uintptr_t)getName(probe);
        
        /*
        案例一:
         1.key = 2,probe = 4,不相等
         2.key = 2,prebe = 2,相等,返回method_t *
        */
        /*
        案例二:
         1.key = 7,probe = 4,不相等
         2.key = 7,prebe = 6,不相等
         3.key = 7,probe = 7,相等,返回method_t *
        */
        if (keyValue == probeValue) { // 如果目标sel的uintptr_t值和探查sel的uintptr_t值匹配成功
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            
            // 探查值不是第一个 && 上一个sel的uintptr_t值也等于keyValue
            // 说明有分类同名方法覆盖,后去分类的方法,多个分类取最后编译的
            while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
                probe--;
            }
            // 返回
            return &*probe;
        }
        
        // 如果keyValue > 探查值
        /*
         案例一:
         1. 2 不大于 4,不进入,继续循环
        */
        /*
         案例二:
         1. 7 大于 4,进入
         2. 7 大于 6,进入
        */
        if (keyValue > probeValue) { 
            /*
             案例二:
             1.base = 4 + 1 = 5,count-- = 8-- = 7
             2.base = 6 + 1 = 7,count-- = 3-- = 2
            */
            base = probe + 1;
            count--;
        }
    }
    
    return nil; // 查询完没找到就返回nil
}
复制代码

备注:方法列表里面的方法是经过fixupMethodList函数按选择器地址排序过的。

  • 二分查找法就是每次取范围内的中间值探查值与要查找的目标进行比较,相等,如果没有分类同名方法,就直接返回查找到的方法。
  • 如果分类有同名方法,就返回分类的方法,如果有多个分类同名方法,就返回最后加载的分类的同名方法
  • 不相等就一直缩小查找的范围继续查找,如果到最后都没有找到就返回nil

2.4: 二分查找法案例图解

二分查找法图解.001.jpeg

二分查找法图解.002.jpeg

备注:base作为探查范围的起始index,count为每次探查的个数。

2.5: cache_getImp

慢速查找流程中,当类支持共享缓存或者每次获得父类后,都会调用cache_getImp开始快速查找流程来查询缓存,按住commandcontrol鼠标左键点击查看,发现在C++文件里cache_getImp只有声明:

image.png

由前文iOS runtime之方法的本质objc_msgSend分析一经验可知快速查找流程是使用汇编源码实现的,全局搜索cache_getImp,进入objc-msg-arm64.s查看相关定义:

    // 传入参数 p0 = class p1 = sel
    STATIC_ENTRY _cache_getImp
    // // 将class存入p16
    GetClassFromIsa_p16 p0, 0
    CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant

// 如果没有找到缓存,直接返回nil,p0是第一个寄存器,也是返回值寄存器
LGetImpMissDynamic:
    mov	p0, #0
    ret

LGetImpMissConstant:
    mov	p0, p2
    ret

    END_ENTRY _cache_getImp
复制代码

GetClassFromIsa_p16宏定义此流程传入参数src = class, needs_auth = 0,因为class在之前的慢速查找流程中已经验证过了,所以这里直接赋值p16 = class就可以了。

// 传入参数 src = class, needs_auth = 0
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */

#if SUPPORT_INDEXED_ISA  // armv7k or arm64_32
	// Indexed isa
	mov	p16, \src			// 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__  // true arm64
.if \needs_auth == 0 // _cache_getImp takes an authed class already
    // 走这里
    // _cache_getImp拿到的是已经验证过的class
    // 直接赋值 p16 = class
	mov	p16, \src
.else
	// 64-bit packed isa
	ExtractISA p16, \src, \auth_address
.endif
#else
	// 32-bit raw isa
	mov	p16, \src

#endif

.endmacro
复制代码

CacheLookup流程在前文iOS runtime之方法的本质objc_msgSend分析一中已经分析过了,此处只是传入参数不一样CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant

  • 如果缓存没有命中就执行LGetImpMissDynamic流程。
// 如果没有找到缓存,直接返回nil,p0是第一个寄存器,也是返回值寄存器
LGetImpMissDynamic:
	mov	p0, #0
	ret
复制代码
  • 如果缓存命中就走CacheHit宏定义里面的GETIMP流程。
// 传入参数Mode($0) = GETIMP
.macro CacheHit
.if $0 == NORMAL
    TailCallCachedImp x17, x10, x1, x16	// authenticate and call imp
.elseif $0 == GETIMP // 走这里
    // p0 = p17(imp)
    mov	p0, p17
    // cbz 比较,为零则跳转;
    // 如果imp = 0则调整9流程,return 0
    cbz	p0, 9f			// don't ptrauth a nil imp
    // imp ^= class,imp解码
    AuthAndResignAsIMP x0, x10, 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, x10, x1, x16	// authenticate imp and re-sign as IMP
    cmp	x16, x15
    cinc	x16, x16, ne			// x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
    ret				// return imp via x17
.else
.abort oops
.endif
.endmacro
复制代码
  • p17 = imp,将p17赋值给p0
  • 如果imp = 0直接调9流程,return 0
  • AuthAndResignAsIMP宏定义将imp解码(cache_t::insert插入时会对imp进行编码),然后返回解码的imp
.macro AuthAndResignAsIMP
	// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = class
        // imp解码
        $0 = $0 ^ $3 = imp ^ class
	eor	$0, $0, $3
.endmacro
复制代码
  • 对从缓存中获取的imp进行解码。

2.6: 为什么缓存查找使用汇编,其他却用C++

  1. 汇编更加接近机器语言,速度更快,效率更高,最大化发挥缓存的优势和意义。
  2. 汇编更安全。
  3. C语言函数的参数必须明确指定,汇编可以动态指定,更加灵活。

3: unrecognized selector sent to xxx

3.1: 单身狗没有女朋友案例

首先来看一个案例:单身狗没有女朋友案例。

创建一个SDSingleDog类,声明一个girlfriend方法,不实现(单身狗没有女朋友)。

#import <Foundation/Foundation.h>
#import "SDSingleDog.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        SDSingleDog *singleDog = [SDSingleDog alloc];
        [singleDog girlfriend];
        
    }
    return 0;
}
复制代码

运行程序,打印输出,崩溃(所以没有女朋友是很崩溃的事)。

image.png

从这个案例可以看出,如果方法快速查找和方法慢速查找没有找到相应的方法、动态方法决议和消息转发都没有实现的情况下,就会抛出unrecognized selector sent to xxx的经典异常,

3.2: unrecognized selector源码探索

全局搜索unrecognized selector sent to,找到3个相关的函数和方法。

image.png

image.png

但是打上断点,发现程序不会进入这3个地方,查看方法注释和函数调用栈,发现实际调用的是CoreFoundation里面的方法,而CoreFoundation并未开源,所以后续分析消息转发时再来探索。

image.png

4: 类调用NSObject对象方法成功原因分析

4.1: 案例代码

@interface NSObject (Goddess)

- (void)kneelAndLick;

@end

@implementation NSObject (Goddess)

- (void)kneelAndLick
{
    NSLog(@"Single dog kneel and lick Goddess");
}

@end

#import <Foundation/Foundation.h>
#import "SDSingleDog.h"
#import "NSObject+Goddess.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // 类对象调用实例对象方法
        [SDSingleDog kneelAndLick];
        
    }
    return 0;
}

*********************** 打印输出 ***********************

2021-07-08 16:40:25.510538+0800 KCObjcBuild[4157:161599] Single dog kneel and lick Goddess
Program ended with exit code: 0
复制代码

4.2: 案例分析

  • OC底层没有所谓的对象方法和类方法之分,类也是元类的类对象,类方法是以对象方法的形式存在元类里面的。
  • 根元类的父类是根类,当根元类没有查找到目标方法后,就会查找到根类,然后就将根类的对象方法返回了。

5: 预告

方法的查找流程实际是个很复杂的流程。现在已经探讨了方法快速查找流程和慢速查找流程,后面还有动态方法决议,以及消息转发流程,敬请期待。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享