从源码看objc_msgSend流程到底都干了什么

767 阅读6分钟

从源码看objc_msgSend流程到底都干了什么

首先我们都知道,在object-c中,不管是方法调用,还是属性调用,等等,都是消息的传递。也就是说,其实他们都使用了objc_msgSend方法,而objc_msgSend是由汇编实现的,我们暂且不表。那么消息传递的流程是怎么样的内。

[[NSObject new] isKindOfClass:[NSObject class]];

上面这段代码的是怎么产生最后的结果的呢?如果发送消息的receiver是nil,程序会不会奔溃呢?

带着这些问题,我们来看下它的汇编实现:

真的,看到它的汇编实现的时候,真的就是看天书一样,尤其是,对于我们这种没怎么学过汇编的来说,但是,代码的注释,非常完善,所以其实看注释,然后跟着流程往下走,就能一窥究竟了。如,主体流程应该是这样的(如果有理解上的偏差,望指点):

_objc_entryPoints:
	PTR   _cache_getImp // 从缓存中找
	PTR   _objc_msgSend // 当前类中找
	PTR   _objc_msgSendSuper // 去父类中找
	PTR   _objc_msgSendSuper2
	PTR   _objc_msgLookup // 消息转发, forwardMethod
	PTR   _objc_msgLookupSuper2
	PTR   0

.private_extern _objc_exitPoints
_objc_exitPoints:
	PTR   LExit_cache_getImp
	PTR   LExit_objc_msgSend
	PTR   LExit_objc_msgSendSuper
	PTR   LExit_objc_msgSendSuper2
	PTR   LExit_objc_msgLookup
	PTR   LExit_objc_msgLookupSuper2
	PTR   0

这里应该是整个方法查找过程的入口,看函数名我们就知道大致的实现流程,我们的重点在后续的,c语言实现的代码逻辑上。

首先看_cache_getImp的实现:

	STATIC_ENTRY _cache_getImp

	GetClassFromIsa_p16 p0 // cls = p16
	CacheLookup GETIMP // GETIMP = 1, 通过缓存去找imp

LGetImpMiss:
	mov	p0, #0
	ret

	END_ENTRY _cache_getImp

然后第一步先判断,传入的对象是否是nil,如果非空的话,就会去类对应的buckets去找,是否有这个imp:

.macro CacheLookup
// p1 = SEL, p16 = isa
ldp	p10, p11, [x16, #CACHE]	// p10 = buckets, p11 = occupied|mask, #CACHE = 16
#if !__LP64__
	and	w11, w11, 0xffff	// p11 = mask
#endif
	and	w12, w1, w11		// x12 = _cmd & mask, p1 = SEL, 得到函数在buckets哈希表中的key
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
		             // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT)) // key左移四位,获得_bucket的地址

	ldp	p17, p9, [x12]		// {imp, sel} = *bucket bucke_t: (_imp, _key)
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

不断的循环,查找整个buckets,如果没有的话,最后会进入:

__class_lookupMethodAndLoadCache3

这个方法是用c语言实现的,会比较清晰一些。其实看上面的代码,我们要先去看对应的数据结构,比如buckets其实一个struct结构:

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits; 
}

struct bucket_t {
	private:
	    // IMP-first is better for arm64e ptrauth and no worse for arm64.
	    // SEL-first is better for armv7* and i386 and x86_64.
	#if __arm64__
	    MethodCacheIMP _imp;
	    cache_key_t _key;
	#else
	    cache_key_t _key;
	    MethodCacheIMP _imp;
	#endif
	
	public:
	    inline cache_key_t key() const { return _key; }
	    inline IMP imp() const { return (IMP)_imp; }
	    inline void setKey(cache_key_t newKey) { _key = newKey; }
	    inline void setImp(IMP newImp) { _imp = newImp; }
	
	    void set(cache_key_t newKey, IMP newImp);
};

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
}

所以cache所在的位置应该是class的首地址 + 16位的偏移,因为isa跟superclass都是8个字节.

__class_lookupMethodAndLoadCache3

我们接下来看看,如果没有找到这个方法的缓存的话,程序会做什么事情呢?

/***********************************************************************
* _class_lookupMethodAndLoadCache.
* Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp().
* This lookup avoids optimistic cache scan because the dispatcher 
* already tried that.
**********************************************************************/
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}


/***********************************************************************
* lookUpImpOrForward.
* The standard IMP lookup. 
* initialize==NO tries to avoid +initialize (but sometimes fails)
* cache==NO skips optimistic unlocked lookup (but uses cache elsewhere)
* Most callers should use initialize==YES and cache==YES.
* inst is an instance of cls or a subclass thereof, or nil if none is known. 
*   If cls is an un-initialized metaclass then a non-nil inst is faster.
* May return _objc_msgForward_impcache. IMPs destined for external use 
*   must be converted to _objc_msgForward or _objc_msgForward_stret.
*   If you don't want forwarding at all, use lookUpImpOrNil() instead.
**********************************************************************/
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();
    // 判断在运行时,这个cls是否是合法的。
    /**
      * isKnownClass
      * Return true if the class is known to the runtime (located within the
      * shared cache, within the data segment of a loaded image, or has been
      * allocated with obj_allocateClassPair).
     */
    checkIsKnownClass(cls);

    // 判断cls是否被分配内存空间,如果没有的话,则分配class_rw_t的d可读写空间
    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();
        // 不断的向上从父类中的方法缓存中查找,是否有这个imp,直到父类为空。我们都知道NSObject的super class = nil
        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) {
                // _objc_msgForward_impcache 一个标志符,表示在父类的缓存中,停止查找
                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;
            }
        }
    }

    // No implementation found. Try method resolver once.
    // 如果当前类以及对应的所有父类中,都找不到sel的话,就进入派发
    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;
    }

    // No implementation found, and method resolver didn't help. 
    // Use forwarding.

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

 done:
    runtimeLock.unlock();

    return imp;
}

总结

总的来说,消息转发的具体逻辑如下:

  1. 先判断obj是不是nil或者tagpointter
  2. 然后去缓存中找,缓存的逻辑在汇编代码中实现
  3. 如果缓存中没有的话,再去__class_lookupMethodAndLoadCache3执行标准的查找流程,去当前类以及所有的父类中的类方法或者实例方法去找同名的,如果没有找到的话,则执行消息的转发,即执行reolve的逻辑
  4. 如果都没有的话,那么就报错。有的话,缓存方法,并返回