从源码看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;
}
总结
总的来说,消息转发的具体逻辑如下:
- 先判断obj是不是nil或者tagpointter
- 然后去缓存中找,缓存的逻辑在汇编代码中实现
- 如果缓存中没有的话,再去
__class_lookupMethodAndLoadCache3执行标准的查找流程,去当前类以及所有的父类中的类方法或者实例方法去找同名的,如果没有找到的话,则执行消息的转发,即执行reolve的逻辑 - 如果都没有的话,那么就报错。有的话,缓存方法,并返回