从main.m走进OC的方法查找

394 阅读11分钟

引入

Objective-C程序有三种途径和运行时系统交互

1.通过 Objective-C 源代码

2.通过Foundation框架中NSObject的方法

3.通过调用运行时系统给我们提供的API接口

image.png

我们很想看一看main.m文件中经过clang编译后会变成什么东西,能否从里面得到有用的东西 使用命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp,由于得到的文件非常庞大,故只截取比较重要的部分来参考

image.png 这里可以知道在Objective-C中方法的本质是objc_msgSend,并且默认带两个参数,第一个是方法接受者,第二个是方法编号,那么是如何通过sel找到方法的实现IMP

image.png

image.png 从这里可以知道Objective-C对象的本质是结构体,其模板是objc_object,每个对象都有一个isa

方法的调用

方法的调用这里分为三种:实例调用方法、调用方法、父类调用方法

  • 实例调用方法 objc_msgSend(p, @selector(run));
  • 调用方法 1.用指针的方法
id class = [Person class];
void *pointClass = &class;
[(__bridge id)pointClass walk];

2.消息发送

objc_msgSend(objc_getClass("Person"), @selector(walk));
  • 父类调用实例方法
Student *s = [Student new];
        
struct objc_super mySuper;
mySuper.receiver = s;
mySuper.super_class = class_getSuperclass([s class]);

objc_msgSendSuper(&mySuper, @selector(run));
  • 父类调用类方法
struct objc_super myClassSuper;
myClassSuper.receiver = [s class];
myClassSuper.super_class = class_getSuperclass(object_getClass([Student class]));
        
objc_msgSendSuper(&myClassSuper, @selector(walk));

之前的文章有分析过,这里直接说结论:实例方法存储于类中,类方法存储于元类中同样以实例方法的形式存在。那么发送消息的流程是如何呢?

方法的查找

objc_msgSend的两种方式

  • 快速的方式:通过查找缓存中的汇编
  • 慢速的方式:CC++配合汇编一起查找

image.png cache中包含selIMP的缓存,会去查找一张由selIMP组成的哈希表,如果能直接从里面找到就会直接返回,速度很快,但是如果缓存中没有的话,就会走入慢速的方式,并且找到后会存入这张哈希表中。

objc_msgSend

直接在arm-64的汇编文件中 查找objc_msgSend

image.png 1.LNilOrTagged这步在判断当前对象是否为Tagged-Point对象,此处先不对这个做解释 2.LGetIsaDoneIsa进行处理 3.CacheLookup NORMAL缓存中查找IMP

image.png

这里可以看出CacheLookup中又可以分为三种情况来讨论,既然是在缓存中查找无非是找的到或者找不到

CacheLookup

image.png

CacheHit

在缓存中命中

image.png

此时会直接call IMP

CheckMiss

在缓存中未曾找到 image.png 因为参数是NORMAL则会走入__objc_msgSend_uncached

image.png MethodTableLookup在方法列表中查找到后,直接调用函数指针

image.png

这里会从汇编直接回到C,以上是大致的流程分析,之后会对快速查找流程慢速查找流程做出详细分析。

CacheLookup快速查找流程

.macro CacheLookup
	//
	// Restart protocol:
	//
	//   As soon as we're past the LLookupStart$1 label we may have loaded
	//   an invalid cache pointer or mask.
	//
	//   When task_restartable_ranges_synchronize() is called,
	//   (or when a signal hits us) before we're past LLookupEnd$1,
	//   then our PC will be reset to LLookupRecover$1 which forcefully
	//   jumps to the cache-miss codepath which have the following
	//   requirements:
	//
	//   GETIMP:
	//     The cache-miss is just returning NULL (setting x0 to 0)
	//
	//   NORMAL and LOOKUP:
	//   - x0 contains the receiver
	//   - x1 contains the selector
	//   - x16 contains the isa
	//   - other registers are set as per calling conventions
	//
LLookupStart$1:

	// p1 = SEL, p16 = isa
/*
    找到定义 #define CACHE            (2 * __SIZEOF_POINTER__)
    p1 = SEL
    p16 = isa
    isa偏移16个字节正好是cache
    将 cache 存入 p11
    cache(mask高16位 + buckets低48位)
 */
	ldr	p11, [x16, #CACHE]				// p11 = mask|buckets
//64位真机
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
/*
    p11 存储 cache
    高16位置0,得到 buckets 存入 p10
 */
    and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
/*
    p11 存储 cache
    右移48位得到mask
    p1 存储 SEL
    _cmd & mask得到sel-imp的下标index(即搜索下标) 结果存入 p12
 */
    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

/*
    p12 存储 搜索下标
    p10 存储 buckets首地址
    LSL #(1+PTRSHIFT) 就是得到一个bucket占用的内存大小
    左移4位,相当于*16,一个bucket占有的内存16
    通过下标找到对应bucket存入 p12
    
 */
    add	p12, p10, p12, LSL #(1+PTRSHIFT)
   // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
/*
    p12 存储 查找到的bucket
    p17 存储 imp
    p9  存储 sel
 */
	ldp	p17, p9, [x12]		// {imp, sel} = *bucket
// 比较 sel 与 p1(传入的参数cmd)
1:	cmp	p9, p1			// if (bucket->sel != _cmd)
// 如果不相等跳转到 2f
	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
/*
    设置到最后一个bucket
    p12 buckets首地址
    p11 mask 右移44位,相当于mask*16,再用buckets首地址偏移,得到最后一个bucket
    
 */
   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)
#else
#error Unsupported cache mask storage for ARM64.
#endif

	// Clone scanning loop to miss instead of hang when cache is corrupt.
	// The slow path may detect any corruption and halt later.
    /*
     p12 存储 最后一个bucket
     p17 存储 imp
     p9  存储 sel
     */
	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

LLookupEnd$1:
LLookupRecover$1:
3:	// double wrap
	JumpMiss $0

.endmacro

整体流程大致分为以下几步

第一步 找到定义 #define CACHE (2 * SIZEOF_POINTER) p1 = SEL p16 = isa isa偏移16个字节正好是cachecache 存入 p11 cache(mask高16位 + buckets低48位)

第二步p11中即cache中取出maskbuckets

  • 由于cache中的结构是mask高16位,buckets低48位,故采用与0x0000ffffffffffff进行与运算清除高16位,直接得到buckets存入p10
  • 同样将cache右移48位直接得到高16位mask,将p1中的selmask进行与运算得到sel-imp的下标index(即搜索下标) 结果存入 p12,至于为什么要这样运算,因为存储时运用了同样的算法。

image.png

第三步 根据所得的搜索下标从找到buckets找到对应的bucket

-PTRSHIFT3,则1+PTRSHIFT4,对p12搜索下标左移4位,其实就是对应下标*16字节,那么一个bucket占用16字节空间,从buckets的首地址偏移即可得到对应的bucket

第四步 根据获取的bucket,取出其中的imp存入p17,取出sel存入p9

第五步进入第一层递归循环查找

  • 比较查找到的bucket中的selobjc_msgSend中的参数_cmd是否相等

  • 如果相等则命中,走入CacheHit,返回IMP

  • 若不相等,将再次分为两种情况来讨论

  • 如果一直找不到会走入CheckMiss,并且由于参数是NORMAL,会跳转到__objc_msgSend_uncached,至此进入慢速查找流程

  1. 如果bucketbuckets的首地址,即第一个bucket,就会通过p11中的 mask 右移44位,相当于mask*16,再用buckets首地址偏移,得到最后一个bucket,至此开始进行第二层的递归循环查找。

  2. 如果bucket不是buckets的首个元素,则持续向前查找,走入第一层的的递归循环查找。

第六步进入第二层递归循环查找

  • 比较查找到的bucket中的selobjc_msgSend中的参数_cmd是否相等

  • 如果相等则命中CacheHit,返回IMP

  • 如果不相等会一直向前偏移查找,直至bucketbuckets的首个元素,直接跳转至JumpMiss,同样跳转至__objc_msgSend_uncached,参数NORMAL

到这里快速查找流程分析完成,上面也分析到如果快速查找流程都没有找到的话,都会跳转至__objc_msgSend_uncached这里

image.png

MethodTableLookup在方法列表中查找到后,直接调用函数指针

image.png

从这里开始就将从汇编部分跳转至C、C++,也就是我们熟悉的地方

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 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.
    //
    // TODO: this check is quite costly during process startup.
    /*
     保证这个类是已经加载过的类
     */
    checkIsKnownClass(cls);
    /*
     判断类是否实现了,若没有则要实现,这里是确保父类链方法的调用
     */
    if (slowpath(!cls->isRealized())) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
        // runtimeLock may have been dropped but is now locked again
    }
    /*
        判断类是否初始化,如果没有要先初始化
     */
    if (slowpath((behavior & LOOKUP_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
    }

    runtimeLock.assertLocked();
    curClass = cls;

    // The code used to lookpu 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().

    //查找类的缓存
    
    
    for (unsigned attempts = unreasonableClassCount();;) {
        // curClass method list.
        //当前类方法列表(采用二分查找算法),如果找到,则返回,将方法缓存到cache中
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            imp = meth->imp;
            goto done;
        }
        //当前类 = 当前类的父类,并判断父类是否为nil
        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.
            // 如果在父类中找到了forward,则停止查找,且不缓存,首先调用此类的方法解析器
            break;
        }
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            //如果在父类中,找到了此方法,将其存储到cache中
            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;
}

同样这里也分为以下几步

第一步

cache中进行查找,如果找到直接返回,如果没有就进入慢速查找流程,其实这里会有疑问,因为走到这里的时候是已经进入了慢速查找流程,为什么还会去cache查找?其实这里是一个优化的地方,为的是防止在多线程的操作时,调用到此方法时,缓存已经有了就不必进入慢速查找流程了。

第二步先对cls进行判断

  • 判断cls是否为已知类,即保证cls是已经加载过的类

  • 判断类是否实现了,若没有则要实现,这里是确保父类链方法的调用,这里实现的目的是为了确定父类链、ro、以及rw等,方法后续数据的读取以及查找的循环

  • 判断类是否初始化,如果没有要先初始化

第三步 开始循环查找,依据类循环链(类--->父类--->根类--->nil)以及元类循环链(元类--->根源类--->根类--->nil)开始循环查找

  • 当前类方法列表(采用二分查找算法),如果找到,则返回,将方法缓存到cache

  • 当前类 = 当前类的父类,并判断父类是否为nil,如果为nil,imp = forward_imp,进行消息转发(第四步),并结束循环

  • 如果父类链中存在循环,则停止

  • 在父类缓存中查找

  1. 如果找到,返回imp,并写入cache
  2. 如果在父类中找到了forward,则停止查找,且不缓存,首先调用此类的方法解析器
  3. 如果未找到,则继续进入循环查找

第四步

判断是否执行过动态方法解析,如果没有就执行一次动态方法解析,如果已经执行过动态方法解析,则进行消息转发流程。

总结

  • 实例方法慢速查找的过程中,是在中查找,父类链类--父类--根类--nil
  • 类方法慢速查找的过程中,是在元类中查找,父类链元类--根源类--根类--nil
  • 如果快速查找慢速查找也没有找到方法实现,则尝试动态方法决议
  • 动态方法决议仍然没有找到,则进行消息转发