
前言
想要成為一名
iOS開發(fā)高手,免不了閱讀源碼。以下是筆者在OC源碼探索中梳理的一個小系列——類與對象篇,歡迎大家閱讀指正,同時也希望對大家有所幫助。
在Objective-C中,當(dāng)編譯器遇到一個方法調(diào)用時,它會將方法的調(diào)用變成以下函數(shù)中的一個:
objc_msgSend、objc_msgSend_stret、objc_msgSendSuper和objc_msgSendSuper_stret。
發(fā)送給對象的父類的消息(使用super關(guān)鍵字時)是使用objc_msgSendSuper發(fā)送的,其他消息是使用objc_msgSend發(fā)送的。如果是以數(shù)據(jù)結(jié)構(gòu)體作為返回值的方法,則是使用objc_msgSendSuper_stret或objc_msgSend_stret發(fā)送的。
上面四個函數(shù)都用于發(fā)送消息,做一些準(zhǔn)備工作,繼而進(jìn)行方法的查找、解析和轉(zhuǎn)發(fā)。本文的主題是方法的查找,筆者將從方法調(diào)用開始,一步一步詳細(xì)解讀objc_msgSend函數(shù)的實(shí)現(xiàn)以及方法的查找流程。
下面直接進(jìn)入正題。
需要注意的是,筆者用的源碼是 objc4-756.2。
1 objc_msgSend解析
1.1 舉個栗子
嗯,一個簡單的例子,代碼如下
@interface Person : NSObject
- (void)personInstanceMethod1;
@end
@implementation Person
- (void)personInstanceMethod1 {
NSLog(@"%s", __FUNCTION__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [Person alloc];
[person personInstanceMethod1];
}
return 0;
}
用clang命令重新編譯main.m文件
clang -rewrite-objc main.m -o main.cpp
打開main.cpp文件
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
Person *person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("personInstanceMethod1"));
}
return 0;
}
去除強(qiáng)轉(zhuǎn)后就容易分辨多了
Person *person = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
objc_msgSend(person, sel_registerName("personInstanceMethod1"));
發(fā)現(xiàn)+alloc和personInstanceMethod1這兩個方法的調(diào)用,實(shí)際上就是調(diào)用objc_msgSend函數(shù)。
1.2 方法調(diào)用的本質(zhì)
可以在arm64.s文件中找到objc_msgSend函數(shù)的說明
id objc_msgSend(id self, SEL _cmd, ...)
其中,第一個參數(shù)self是調(diào)用者本身,它也是接收者;第二個參數(shù)_cmd是方法編號;剩下的可變參數(shù)列表是方法自己的參數(shù)。
簡單說明一下:
id指的是OC對象,每個對象在內(nèi)存的結(jié)構(gòu)都是不確定的,但其首地址指向的是對象的isa,通過isa,在運(yùn)行時就能獲取到objc_classobjc_class表示對象的Class,它的結(jié)構(gòu)在編譯后就確定了SEL表示選擇器,通??衫斫鉃橐粋€字符串。OC在運(yùn)行時維護(hù)著一張SEL表,將字符串相同的方法名映射到唯一一個SEL上
- 任意類的相同方法名映射的
SEL都相同(可以把SEL近似地等同于方法名)- 可以通過
sel_registerName(char *name)這個C函數(shù)得到SEL,OC也提供了一個語法糖@selector用來方便的調(diào)用該函數(shù)IMP是一個函數(shù)指針。OC中的方法最終都會轉(zhuǎn)換成純C的函數(shù),IMP表示的就是這些函數(shù)的地址。
從上面的例子可以得出一個結(jié)論:方法調(diào)用的本質(zhì)是通過objc_msgSend函數(shù),向調(diào)用者發(fā)送名為SEL的消息,找到具體的函數(shù)地址IMP,進(jìn)而執(zhí)行該函數(shù)。
也就是說,下面的兩段代碼實(shí)際上效果是相同的
要想使用 objc_msgSend 函數(shù),需要改一處設(shè)置。如下圖
2 方法的查找
方法的查找,也叫消息的查找,它的準(zhǔn)備工作是從objc_msgSend開始,準(zhǔn)備就緒后,才會展開查找。
2.1 objc_msgSend源碼分析
以arm64架構(gòu)為例,objc_msgSend的源碼以及解析如下
// ENTRY 表示函數(shù)入口
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
// p0存儲的是objc_msgSend的第一個參數(shù)(即接收者)
// 在此對接收者進(jìn)行非空判斷
cmp p0, #0 // nil check and tagged pointer check
// 是否支持 Tagged Pointer,64位CPU架構(gòu)下為1
#if SUPPORT_TAGGED_POINTERS
// 64位,且 p0 <= 0(le即less or equal),跳轉(zhuǎn)到 LNilOrTagged
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
// 32位,且 p0 == 0(eq即equal),跳轉(zhuǎn)到 LReturnZero
b.eq LReturnZero
#endif
// 讀取接收者(實(shí)例對象、類對象、元類對象)的isa到p13
ldr p13, [x0] // p13 = isa
// 根據(jù)isa得到class
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
// p0 == 0,即接收者為nil,跳轉(zhuǎn)到 LReturnZero
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 表示函數(shù)結(jié)束
END_ENTRY _objc_msgSend
可以看出,objc_msgSend主要是獲取接收者的isa。
思考:
objc_msgSend為什么要用匯編編寫?
2.2 GetClassFromIsa_p16
以下兩種情況下會執(zhí)行GetClassFromIsa_p16:
- 當(dāng)系統(tǒng)是64位架構(gòu),接收者不是
Tagged Pointer對象,isa也不是nonpointer的; - 當(dāng)系統(tǒng)不是64位架構(gòu),且接收者的
isa非空
以上兩種情況下會來到GetClassFromIsa_p16,其源碼如下
.macro GetClassFromIsa_p16 /* src */
#if SUPPORT_INDEXED_ISA // armv7k or arm64_32
// Indexed isa
mov p16, $0 // 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__
// 64-bit packed isa
and p16, $0, #ISA_MASK
#else
// 32-bit raw isa
mov p16, $0
#endif
.endmacro
由上文可知p16 = class,$0是接收者isa,64位架構(gòu)下通過isa & ISA_MASK得到真正的isa,其值是類或元類,這取決于接收者是實(shí)例對象還是類對象。
也就是說,objc_msgSend的主要作用是拿到接收者的isa信息,如果有,則執(zhí)行CacheLookup:
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
2.3 CacheLookup
經(jīng)過
objc_msgSend一番操作,此時p1 = SEL,p16 = isa,
CacheLookup的作用就是在緩存中查找方法實(shí)現(xiàn),它有三種模式:NORMAL、GETIMP和LOOKUP。
先來看看CacheLookup源碼
.macro CacheLookup
// p1 = SEL, p16 = isa
ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
and w11, w11, 0xffff // p11 = mask
#endif
and w12, w1, w11 // x12 = _cmd & mask
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
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: // 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
CacheLookup操作的是類的cache成員變量,它的結(jié)構(gòu)是cache_t,主要用于緩存調(diào)用的方法(想了解cache_t的請戳 OC源碼分析之方法的緩存原理,這里不作贅述)。
對CacheLookup的分析如下:
- 關(guān)于
ldp p10, p11, [x16, #CACHE]
找到CACHE的定義處
#define CACHE (2 * __SIZEOF_POINTER__)
#define CLASS __SIZEOF_POINTER__
64位CPU架構(gòu)下,指針的長度為8字節(jié),所以CACHE為16字節(jié)。對于cache_t結(jié)構(gòu),其中buckets指針為8字節(jié),存放在p10;4字節(jié)的mask和4字節(jié)的occupied共同存放在p11,p11的低32位(即w11)存放的是mask。
- 找到目標(biāo)
bucket
#if __LP64__ // arm64
...
#define PTRSHIFT 3
...
#else // arm64_32
...
#define PTRSHIFT 2
...
通過sel & mask哈希計(jì)算得出索引值,再取到對應(yīng)的bucket,接著將bucket的imp和sel分別存入p17、p9。
思考:為什么索引值要左移
1 + PTRSHIFT位?
CacheHit、CheckMiss和JumpMiss
當(dāng)找到bucket后,接下來的流程如下:
- 如果
bucket的sel不等于方法的sel,則執(zhí)行{imp, sel} = *--bucket,也就是遍歷buckets中的每個bucket,分別與方法的sel作對比 - 如果
bucket的sel等于方法的sel,則執(zhí)行CacheHit,即直接返回并執(zhí)行imp; - 如果找到buckets的第一個
bucket,則執(zhí)行JumpMiss - 如果
bucket的sel等于0,即該bucket是空桶,則執(zhí)行CheckMiss
接下來看一下CacheHit、CheckMiss和JumpMiss這三個函數(shù)的源碼
// CacheHit: x17 = cached IMP, x12 = address of cached IMP, x1 = SEL
.macro CacheHit
.if $0 == NORMAL
TailCallCachedImp x17, x12, x1 // authenticate and call imp
.elseif $0 == GETIMP
mov p0, p17
cbz p0, 9f // don't ptrauth a nil imp
AuthAndResignAsIMP x0, x12, x1 // 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 // authenticate imp and re-sign as IMP
ret // return imp via x17
.else
.abort oops
.endif
.endmacro
// CheckMiss
.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
// JumpMiss
.macro JumpMiss
.if $0 == GETIMP
b LGetImpMiss
.elseif $0 == NORMAL
b __objc_msgSend_uncached
.elseif $0 == LOOKUP
b __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
關(guān)于這三個函數(shù),總結(jié)如下:
-
CacheHit函數(shù)在NORMAL模式下,會把找到的IMP返回并調(diào)用;在GETIMP、LOOKUP這兩種模式下僅僅是返回IMP,并沒有調(diào)用。 -
JumpMiss、CheckMiss這兩個函數(shù)在三種模式下的行為基本一致:- 在
NORMAL模式下,均調(diào)用__objc_msgSend_uncached; - 在
GETIMP模式下,均調(diào)用LGetImpMiss,返回nil; - 在
LOOKUP模式下,均調(diào)用__objc_msgLookup_uncached;
- 在
2.4 __objc_msgSend_uncached 和 __objc_msgLookup_uncached
同樣看源碼
// __objc_msgSend_uncached
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
// __objc_msgLookup_uncached
STATIC_ENTRY __objc_msgLookup_uncached
UNWIND __objc_msgLookup_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search
MethodTableLookup
ret
END_ENTRY __objc_msgLookup_uncached
發(fā)現(xiàn)這兩個函數(shù)的主要工作就是調(diào)用MethodTableLookup
2.5 MethodTableLookup
源碼如下
.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)]
// receiver and selector already in x0 and x1
mov x2, x16
bl __class_lookupMethodAndLoadCache3
// 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
MethodTableLookup函數(shù)主要保存部分寄存器的參數(shù),然后就是調(diào)用_class_lookupMethodAndLoadCache3函數(shù)。來到這里,就意味著消息的緩存查找流程正式結(jié)束,接下來就要去方法列表中查找了。在方法列表中的查找流程是C\C++實(shí)現(xiàn)的,效率低于緩存查找,因此這個流程也叫做消息的慢速查找流程。
2.6 _class_lookupMethodAndLoadCache3
_class_lookupMethodAndLoadCache3是個簡單的C\C++函數(shù),它只有一行代碼,即調(diào)用lookUpImpOrForward函數(shù)
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
接下來重點(diǎn)分析lookUpImpOrForward函數(shù)。
2.7 lookUpImpOrForward
既然是從_class_lookupMethodAndLoadCache3過來的,顯然initialize和resolver這兩個參數(shù)的值是YES(resolver這個標(biāo)志決定是否進(jìn)行后面的動態(tài)方法解析),cache則是NO。下面分析一下lookUpImpOrForward的源碼
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
// 如果是從緩存過來,cache為NO;消息解析和轉(zhuǎn)發(fā)的時候,需要過一遍緩存,此時為YES;
// 對緩存進(jìn)行查找是沒有加鎖的,進(jìn)而提高緩存查找的性能
if (cache) {
// cache_getImp 也是匯編編寫的
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.
// 加鎖,防止多線程操作,保證方法查找以及緩存填充(cache-fill)的原子性,
// 以及確保加鎖之后的代碼不會有新方法添加導(dǎo)致緩存被沖洗(flush).
runtimeLock.lock();
checkIsKnownClass(cls);
// 如果類還沒有realize,需要先進(jìn)行realize,加載信息(屬性、方法、協(xié)議等的attach)
// 一般懶加載的類會走此方法
if (!cls->isRealized()) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
// runtimeLock may have been dropped but is now locked again
}
// 如果類未初始化,需要初始化
if (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
}
retry:
runtimeLock.assertLocked();
// Try this class's cache.
// 在當(dāng)前類的緩存中查找
imp = cache_getImp(cls, sel);
if (imp) goto done;
// Try this class's method lists.
// 在當(dāng)前類的方法列表中查找
{
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();
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) {
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.
// 在【類...根類】的【緩存+方法列表】中都沒找到IMP,進(jìn)行方法解析
if (resolver && !triedResolver) {
runtimeLock.unlock();
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.
// 消息的轉(zhuǎn)發(fā)
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlock();
return imp;
}
簡單梳理一下。對于某個cls來說,查找IMP總是會先從cls的緩存中開始(調(diào)用cache_getImp函數(shù));如果沒找到,才會去cls的方法列表中查找,也就是調(diào)用getMethodNoSuper_nolock函數(shù)。如果依然沒找到,會去cls的父類(父類的父類,一直到根類)重復(fù)【緩存+方法列表】這個查找流程,直到找到為止。如果到根類后依然未找到,則會進(jìn)入方法解析甚至消息轉(zhuǎn)發(fā)的流程。
需要注意的是,如果在當(dāng)前類的緩存中沒找到,但是在其方法列表(或其“父類...根類”的緩存或方法列表)中找到了IMP,需要進(jìn)行一次是否是消息轉(zhuǎn)發(fā)的判斷,如果不是消息轉(zhuǎn)發(fā),那么就對當(dāng)前類的緩存進(jìn)行填充操作,方便下次的調(diào)用;如果是消息轉(zhuǎn)發(fā),就退出循環(huán)。
lookUpImpOrForward 這個函數(shù)可以說是消息的調(diào)度中心,它不僅包含消息的查找,還囊括了消息的解析和轉(zhuǎn)發(fā)。礙于篇幅,本文僅介紹其消息查找方面的內(nèi)容,其余內(nèi)容將另啟一文詳細(xì)說明。
接下來分析一下cache_getImp和getMethodNoSuper_nolock這兩個函數(shù)。
2.8 cache_getImp
cache_getImp也是匯編編寫的,其源碼如下:
STATIC_ENTRY _cache_getImp
GetClassFromIsa_p16 p0
CacheLookup GETIMP
LGetImpMiss:
mov p0, #0
ret
END_ENTRY _cache_getImp
主要是處理isa,進(jìn)而在緩存中查找IMP,需要注意的是,這次的緩存查找模式是GETIMP(GETIMP模式下,如果CacheLookup查找失敗會執(zhí)行LGetImpMiss)。
關(guān)于
GetClassFromIsa_p16和CacheLookup前面已做過解析。
2.9 getMethodNoSuper_nolock
調(diào)用getMethodNoSuper_nolock函數(shù)的目的是檢索cls的方法列表,查找名為sel的方法,找到則返回這個方法(method_t結(jié)構(gòu),該結(jié)構(gòu)含有IMP)。其源碼為:
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
assert(cls->isRealized());
// fixme nil cls?
// fixme nil sel?
for (auto mlists = cls->data()->methods.beginLists(),
end = cls->data()->methods.endLists();
mlists != end;
++mlists)
{
method_t *m = search_method_list(*mlists, sel);
if (m) return m;
}
return nil;
}
cls->data()->methods是method_array_t結(jié)構(gòu),它可能是一維數(shù)組,也可能是二維數(shù)組,對其從beginLists()到endLists()進(jìn)行迭代,確保了每次迭代時總能得到一維的method_t數(shù)組,接下來就是調(diào)用search_method_list函數(shù),對這個數(shù)組進(jìn)行檢索。
2.10 search_method_list
static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
int methodListIsFixedUp = mlist->isFixedUp();
int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
// 一般來說,mlist是有序的,由此對其進(jìn)行二分查找
return findMethodInSortedMethodList(sel, mlist);
} else {
// Linear search of unsorted method list
// mlist是無序的,只好遍歷匹配
for (auto& meth : *mlist) {
if (meth.name == sel) return &meth;
}
}
#if DEBUG
// sanity-check negative results
if (mlist->isFixedUp()) {
for (auto& meth : *mlist) {
if (meth.name == sel) {
_objc_fatal("linear search worked when binary search did not");
}
}
}
#endif
return nil;
}
__builtin_expect()表示對結(jié)果的期望:多數(shù)都是true,也就是說,這里通常會執(zhí)行findMethodInSortedMethodList函數(shù),對mlist進(jìn)行二分查找;否則將遍歷查找。
需要注意的是,當(dāng)方法列表的結(jié)構(gòu)發(fā)生改變的時候,就會觸發(fā)對列表的排序(注意是方法所在的列表,而不是rw中所有的方法列表,即如果是二維數(shù)組,只需要重新排列當(dāng)前方法所在的列表即可)。在以下函數(shù)的調(diào)用中,會觸發(fā)對mlist的排序:
methodizeClassattachCategoriesaddMethodaddMethods
2.11 log_and_fill_cache
log_and_fill_cache函數(shù)主要是將找到的IMP填充到緩存中,方便下次的調(diào)用。下面的三種情況會調(diào)用這個函數(shù):
- 在類(或轉(zhuǎn)發(fā)類)的緩存中沒找到
IMP,但是在其方法列表中找到了; - 在類(該類不是轉(zhuǎn)發(fā)類)的“父類...根類”的緩存中找到了
IMP; - 在類的“父類...根類”的方法列表中找到了
IMP
static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
if (objcMsgLogEnabled) {
bool cacheIt = logMessageSend(implementer->isMetaClass(),
cls->nameForLogging(),
implementer->nameForLogging(),
sel);
if (!cacheIt) return;
}
#endif
cache_fill (cls, sel, imp, receiver);
}
void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
#if !DEBUG_TASK_THREADS
mutex_locker_t lock(cacheUpdateLock);
cache_fill_nolock(cls, sel, imp, receiver);
#else
_collecting_in_critical();
return;
#endif
log_and_fill_cache函數(shù)比較簡單,它調(diào)用了cache_fill函數(shù),而cache_fill函數(shù)又調(diào)用了cache_fill_nolock函數(shù),也就是說,填充緩存的關(guān)鍵函數(shù)是cache_fill_nolock。
關(guān)于
cache_fill_nolock函數(shù)的解析,感興趣的同學(xué)請戳 OC源碼分析之方法的緩存原理,筆者有詳細(xì)分析,這里就不作贅述。
3 總結(jié)
來到這里,已經(jīng)把消息的查找介紹完畢,是時候總結(jié)一下了。
3.1 方法調(diào)用的本質(zhì)
- 方法的調(diào)用會被編譯器翻譯成
objc_msgSend、objc_msgSendSuper、objc_msgSend_stret和objc_msgSendSuper_stret這四個函數(shù)之一,這四個函數(shù)都是由匯編代碼實(shí)現(xiàn)的。- 如果是以數(shù)據(jù)結(jié)構(gòu)體作為返回值的方法,最終會轉(zhuǎn)換成相應(yīng)的
objc_msgSend_stret或objc_msgSendSuper_stret函數(shù) - 使用
super關(guān)鍵字調(diào)用方法時,是使用objc_msgSendSuper發(fā)送的 - 其他的方法調(diào)用是使用
objc_msgSend函數(shù)發(fā)送消息的。
- 如果是以數(shù)據(jù)結(jié)構(gòu)體作為返回值的方法,最終會轉(zhuǎn)換成相應(yīng)的
- 方法的調(diào)用是通過
objc_msgSend(或objc_msgSendSuper,或objc_msgSend_stret,或objc_msgSendSuper_stret)函數(shù),向調(diào)用者發(fā)送名為SEL的消息,找到具體的函數(shù)地址IMP,進(jìn)而執(zhí)行該函數(shù)。
3.2 方法的查找
方法的查找流程如下:
- 從
objc_msgSend源碼開始,會先去 類(實(shí)例方法)\元類(類方法) 的緩存中查找,如果找到IMP則返回并調(diào)用,否則會去 類\元類 的方法列表中查找 - “步驟1”中的 “緩存+方法列表” 的查找方案,會遍歷類的繼承體系(類、類的父類、...、根類),分別進(jìn)行查找,直至找到
IMP為止。- 如果在當(dāng)前類的緩存中沒找到,但是在其方法列表(或其“父類...根類”的緩存或方法列表)中找到了IMP,需要進(jìn)行一次是否是消息轉(zhuǎn)發(fā)的判斷,如果不是消息轉(zhuǎn)發(fā),那么就對當(dāng)前類的緩存進(jìn)行填充操作,方便下次調(diào)用時的查找;如果是消息轉(zhuǎn)發(fā),則不會緩存到當(dāng)前類中
- 如果遍歷結(jié)束后依然未找到
IMP,則會啟動消息的解析或轉(zhuǎn)發(fā)。
4 問題討論
4.1 objc_msgSend為什么要用匯編編寫?
A:原因大致有以下幾點(diǎn)
-
C語言是靜態(tài)語言,無法實(shí)現(xiàn)參數(shù)個數(shù)、類型未知的情況下跳轉(zhuǎn)到另一個任意的函數(shù)實(shí)現(xiàn)的功能;而匯編的寄存器可以做到這一點(diǎn) - 匯編執(zhí)行效率比C語言的高
- 使用匯編可以有效防止系統(tǒng)函數(shù)被hook,因此更為安全。
4.2 為什么索引值要左移1 + PTRSHIFT位?
A:這個筆者沒有在objc4-756.2源碼中找到答案,但是在objc4-779.1源碼版本的cache_t結(jié)構(gòu)中,存在關(guān)于這個問題的解釋。其部分源碼如下:
// How much the mask is shifted by.
static constexpr uintptr_t maskShift = 48;
// Additional bits after the mask which must be zero. msgSend
// takes advantage of these additional bits to construct the value
// `mask << 4` from `_maskAndBuckets` in a single instruction.
static constexpr uintptr_t maskZeroBits = 4;
// The largest mask value we can store.
static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
// The mask applied to `_maskAndBuckets` to retrieve the buckets pointer.
static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1;
總的來說,在64位中,mask的偏移值為48,也就是最高的16位存儲mask;接著的44位是buckets指針地址,最低的4位是附加位,即 buckets的有效指針地址僅僅是64位中的[4, 47]位。在CacheLookup源碼中,由_cmd & mask哈希運(yùn)算可以得到索引值(索引值 < ((1 << 16) - 1)),如果想得到這個位置的bucket,其索引值必須左移4位后,才能與buckets指針地址相加得到正確的bucket地址。
5 參考資料
6 PS
- 源碼工程已放到
github上,請戳 objc4-756.2源碼 - 你也可以自行下載 蘋果官方objc4源碼 研究學(xué)習(xí)。
- 轉(zhuǎn)載請注明出處!謝謝!