簡介
在前面我們知道僻澎,當(dāng)我們使用xcrun 將文件編譯成cpp文件的時候 就可以看到方法的本質(zhì)就是消息废封,調(diào)用方法也就是發(fā)送消息州泊,這就有一個很重要的函數(shù) objc_msgSend, 下面我們就來看看 消息發(fā)送的實現(xiàn)
從下面的注釋,我們可以知道漂洋,objc_msgSend 就是發(fā)送一個消息給類的實例對象
可以從note 處看到 那么多方法都是基于 objc_msgSend實現(xiàn)的
/**
* Sends a message with a simple return value to an instance of a class.
*
* @param self A pointer to the instance of the class that is to receive the message.
* @param op The selector of the method that handles the message.
* @param ...
* A variable argument list containing the arguments to the method.
*
* @return The return value of the method.
*
* @note When it encounters a method call, the compiler generates a call to one of the
* functions \c objc_msgSend, \c objc_msgSend_stret, \c objc_msgSendSuper, or \c objc_msgSendSuper_stret.
* Messages sent to an object’s superclass (using the \c super keyword) are sent using \c objc_msgSendSuper;
* other messages are sent using \c objc_msgSend. Methods that have data structures as return values
* are sent using \c objc_msgSendSuper_stret and \c objc_msgSend_stret.
*/
OBJC_EXPORT id _Nullable
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
一 方法的快速查找
1. objc_msgSend 的具體實現(xiàn)
我們在前面的編譯的源碼中遥皂,全局搜索 objc_msgSend
,在objc-msg-arm64.s
文件中 我們可以看到這樣一段代碼
objc_msgSend
函數(shù)的是用匯編實現(xiàn)的,
ENTRY _objc_msgSend // ENTRY 表示函數(shù)的入口刽漂,是一個格式
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS //在arm64-asm.h 可以看到 當(dāng)arm64架構(gòu)時是true
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
// 根據(jù)isa演训, 獲取類對象
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached -- 調(diào)用imp方法實現(xiàn),或者調(diào)用objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
// tagged
adrp x10, _objc_debug_taggedpointer_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
ubfx x11, x0, #60, #4
ldr x16, [x10, x11, LSL #3]
adrp x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
cmp x10, x16
b.ne LGetIsaDone
// ext tagged
adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
ubfx x11, x0, #52, #8
ldr x16, [x10, x11, LSL #3]
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
END_ENTRY _objc_msgSend
2.CacheLookup 實現(xiàn)
我們詳細看一下 CacheLookup 怎么定義的
從下面的注釋中我們可以看到贝咙,這個方法的含義是 從類的緩存中獲取方法的imp
.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:
// 注釋
// 通過isa 偏移拿到類的方法緩存中的buckets样悟、以及occupied和mask
// p1 = SEL, p16 = isa
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
and p10, p11, #0x0000ffffffffffff // p10 = buckets
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
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// 在 bucket 中查找方法,也就是在類的方法緩存中查找方法庭猩,找到就返回
// 也就是 CacheHit 在上面窟她,可以看到方法實現(xiàn) ,也就是方法簽名蔼水,并且調(diào)用方法
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
// 方法沒有在緩存中找到震糖,此時調(diào)用 CheckMiss 函數(shù), 在CheckMiss函數(shù)實現(xiàn)中徙缴,又調(diào)用了 __objc_msgSend_uncached
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
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.
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
這個方法的主體就是查找 cache_t
试伙,并且查找cache_t
中是否有傳進來的方法嘁信。 如果緩存中存在就執(zhí)行 CacheHit
,不存在就執(zhí)行 CheckMiss
3. CacheHit
- 由于之前是normal 查找于样,所以就直接簽名并返回了imp疏叨,
/********************************************************************
*
* CacheLookup NORMAL|GETIMP|LOOKUP <function>
*
* Locate the implementation for a selector in a class method cache.
*
* When this is used in a function that doesn't hold the runtime lock,
* this represents the critical section that may access dead memory.
* If the kernel causes one of these functions to go down the recovery
* path, we pretend the lookup failed by jumping the JumpMiss branch.
*
* Takes:
* x1 = selector
* x16 = class to be searched
*
* Kills:
* x9,x10,x11,x12, x17
*
* On exit: (found) calls or returns IMP
* with x16 = class, x17 = IMP
* (not found) jumps to LCacheMiss
*
********************************************************************/
#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2
// CacheHit: x17 = cached IMP, x12 = address of cached IMP, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
TailCallCachedImp x17, x12, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP
mov p0, p17
cbz p0, 9f // don't ptrauth a nil imp
AuthAndResignAsIMP x0, x12, 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, x12, x1, x16 // authenticate imp and re-sign as IMP
ret // return imp via x17
.else
.abort oops
.endif
.endmacro
4. CheckMiss
可知,如果方法沒有在緩存中找到穿剖,就執(zhí)行了CheckMiss
蚤蔓,之前的類型是 NORMAl
,也就執(zhí)行了 __objc_msgSend_uncached
.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
二 方法的慢速查找
1. __objc_msgSend_uncached
全局搜索 糊余,可以看到這樣一段代碼秀又, 可以看到函數(shù)內(nèi)部 中有有一段說明 這不是一個C函數(shù)方法,可以在類中搜索贬芥,也就是MethodTableLookup
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
2. MethodTableLookup
可以看到 內(nèi)部調(diào)用了 lookUpImpOrForward
函數(shù)吐辙, 參數(shù)是 obj,sel蘸劈,class 等
.macro MethodTableLookup
// push frame
SignLR
stp fp, lr, [sp, #-16]!
mov fp, sp
// save parameter registers: x0..x8, q0..q7
sub sp, sp, #(10*8 + 8*16)
stp q0, q1, [sp, #(0*16)]
stp q2, q3, [sp, #(2*16)]
stp q4, q5, [sp, #(4*16)]
stp q6, q7, [sp, #(6*16)]
stp x0, x1, [sp, #(8*16+0*8)]
stp x2, x3, [sp, #(8*16+2*8)]
stp x4, x5, [sp, #(8*16+4*8)]
stp x6, x7, [sp, #(8*16+6*8)]
str x8, [sp, #(8*16+8*8)]
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
mov x2, x16
mov x3, #3
bl _lookUpImpOrForward
// IMP in x0
mov x17, x0
// restore registers and return
ldp q0, q1, [sp, #(0*16)]
ldp q2, q3, [sp, #(2*16)]
ldp q4, q5, [sp, #(4*16)]
ldp q6, q7, [sp, #(6*16)]
ldp x0, x1, [sp, #(8*16+0*8)]
ldp x2, x3, [sp, #(8*16+2*8)]
ldp x4, x5, [sp, #(8*16+4*8)]
ldp x6, x7, [sp, #(8*16+6*8)]
ldr x8, [sp, #(8*16+8*8)]
mov sp, fp
ldp fp, lr, [sp], #16
AuthenticateLR
.endmacro
3. lookUpImpOrForward
最終我們在 objc-runtime-new.mm
文件中找到函數(shù)實現(xiàn)
/***********************************************************************
* lookUpImpOrForward.
* The standard IMP lookup.
* Without LOOKUP_INITIALIZE: tries to avoid +initialize (but sometimes fails)
* Without LOOKUP_CACHE: skips optimistic unlocked lookup (but uses cache elsewhere)
* Most callers should use LOOKUP_INITIALIZE and LOOKUP_CACHE
* 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 LOOKUP_NIL.
**********************************************************************/
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
// 防止線程并發(fā)操作 加鎖
runtimeLock.assertUnlocked();
// 判斷緩存中是否存在昏苏,存在就返回imp
// 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);
// //判斷當(dāng)前類是否加載到內(nèi)存中(isa和rw信息是否存在)。
if (slowpath(!cls->isRealized())) {
//如果不存在進行類的加載威沫,并且會遞歸加載所有父類贤惯、元類。為了確定類的父類棒掠、isa鏈
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.
//使用二分法在當(dāng)前類的方法列表methodList中查找該方法
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp;
goto done;
}
//將當(dāng)前類切換為父類, 如果當(dāng)前類的父類是nil烟很,也就是在NSObject中都沒有找到則直接跳出循環(huán)
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.
// 父類鏈中存在循環(huán)颈墅,停止
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
//在父類的方法緩存中進行查找
imp = cache_getImp(curClass, sel);
//如果父類沒找到,首次會進入動態(tài)方法解析
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.
break;
}
// 如果方法找到了 就將其加入緩存
if (fastpath(imp)) {
// Found the method in a superclass. Cache it in this class.
goto done;
}
}
// No implementation found. Try method resolver once.
// 沒有找到方法實現(xiàn),調(diào)用一次方法動態(tài)解析
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;
}
4. 方法最后一次補救機會
我們可以看到雾袱,當(dāng)系統(tǒng)沒有找到imp 實現(xiàn)的時候恤筛, 就會跳出for循環(huán),進入這里的動態(tài)嘗試谜酒,這是慢速查找的補救措施叹俏,
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
這段代碼只執(zhí)行一次,
第一進入時:behavior=3, LOOKUP_RESOLVER = 2
,執(zhí)行與計算behavior & LOOKUP_RESOLVER = 2
僻族,此時 if 成立粘驰,
然后behavior
等于LOOKUP_RESOLVER
的反,
也就是 下一次計算時是 一個數(shù)的反 與上這個數(shù)述么,結(jié)果一定為0蝌数,所以這個這個方法只會走一次
如果動態(tài)解析查找失, 就會 拋出forward_imp
異常 度秘,就crash
了
補充 --- 動態(tài)方法決議 解析
resolveMethod_locked
接下看我們就看看 resolveMethod_locked
函數(shù)的執(zhí)行過程
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
// 動態(tài)方法決議: 重新開始查詢
if (! cls->isMetaClass()) { // 對象查詢顶伞,類查詢
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
else { // 元類查詢
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNil(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
// 再次調(diào)用
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
- 函數(shù)入?yún)?/li>
inst
: 表示真正持有sel
方法的類(對象方法存儲在類中饵撑,類方法 存放在 元類中)
sel
: 方法名
cls
: 方法接收的類,也就是調(diào)用方法的類
behavior
: 影響方法的調(diào)用
- 方法解析
- 加鎖 -> 判定 類是否加載 -> 解鎖
- 判定類是否是元類唆貌,如果不是元類滑潘。就代表方法是對象方法,所以進入對象方法查詢
- 如果是類方法锨咙,就調(diào)用
resolveClassMethod
查詢類方法语卤,然后lookUpImpOrNil
檢查是否找到imp
, 如果沒有找到就調(diào)用resolveInstanceMethod
意思就是酪刀,如果類方法沿著元類繼承鏈中查找粹舵,直到找到NSObject 元類
中都沒有找到,接著就是NSObject
本類中骂倘,其中存儲的是對象方法眼滤,所以需要使用resolveInstanceMethod
在查找一次
resolveInstanceMethod
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
// 獲取 resolveInstanceMethod 的方法名 SEL
SEL resolve_sel = @selector(resolveInstanceMethod:);
// 檢查 類對象的元類中是否有 resolveInstanceMethod 方法,如果沒有就 return; 但是在根元類中默認實現(xiàn)了該方法, 所以不會 return
if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
// Resolver not implemented.
return;
}
// 消息發(fā)送 調(diào)用一次 resolveInstanceMethod 方法
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);
// 再次查詢一次 imp, 意思就是 如果上面的 resolveInstanceMethod 方法 實現(xiàn)了 sel, 我們就能拿到 imp,并將其寫入 cls 的緩存
IMP imp = lookUpImpOrNil(inst, sel, cls);
// 日志收集
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
lookUpImpOrNil
/* method lookup */
enum {
LOOKUP_INITIALIZE = 1,
LOOKUP_RESOLVER = 2,
LOOKUP_CACHE = 4,
LOOKUP_NIL = 8,
};
static inline IMP
lookUpImpOrNil(id obj, SEL sel, Class cls, int behavior = 0)
{
// behavior = 0历涝, LOOKUP_CACHE = 4诅需, LOOKUP_NIL = 8
return lookUpImpOrForward(obj, sel, cls, behavior | LOOKUP_CACHE | LOOKUP_NIL);
}
內(nèi)部繼續(xù)調(diào)用了
lookUpImpOrForward
,然后behavior = 0|4|8 = 12
此時進入 lookUpImpOrForward
函數(shù)中, 以下條件就會成立睬关, 就會在緩存中查找一次
fastpath(behavior & LOOKUP_CACHE) = 12 & 4 = 4
if (fastpath(behavior & LOOKUP_CACHE)) {
imp = cache_getImp(cls, sel);
if (imp) goto done_nolock;
}
接著 (slowpath(behavior & LOOKUP_RESOLVER)) = 12 & 2 = 0
條件不成立诱担,就不會進入動態(tài)決議方法
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
所以 lookUpImpOrNil
中的 lookUpImpOrForward
會循環(huán)遍歷cls
繼承鏈的所有類的cache
和methodList
來尋找imp