1、類中方法的存儲
cache_t
中的方法存儲
cache_t cache
方法緩存中,方法的存儲是以SEL
和IMP
的形式。
class_data_bits_t
中方法存儲
在類objc_class
的學習中,明白了實例對象的方法是存放在類的class_data_bits_t bits;
中。取方法的時候,使用class_data_bits_t
中的class_rw_t* data()
。在這個地方,如果是在編譯期,或者說在系統調用運行時的realizeClass
方法之前,data()
是class_ro_t
結構體的形式,保存了類中的方法列表、屬性列表、成員變量列表等;在系統運行了runtime的realizeClass
方法后,則會生成class_rw_t
結構體,并更新class_rw_t* data()
的地址,換成class_rw_t
結構體的地址。
class_rw_t
:method_array_t
-->method_list_t
-->method_t
.
class_ro_t
:method_list_t
-->method_t
.
方法以 method_t 結構保存,iOS14 以上該結構發生巨大變化。
在 64 位的系統上會占用 24 字節,name、types、imp 分別占用 64 bit 大小,與之前一樣。
- 取值時,在地址后取三個64位的數據,就能得到方法的name、type、imp
但是 struct small 占用 12 字節,name、types、imp 分別占用 32 bit 大小。 - 這種情況下,name、types、imp存儲的是地址的偏移量。當前地址 + 存儲的偏移量才是真正的存儲地址。
name、types、imp 分別指向方法的 名稱、參數數據、函數指針,蘋果考慮到鏡像中的方法都是固定的,不會跑到其他鏡像中去。其實不需要 64 位尋址的指針,只需要 32 位即可 (多余 32 位尋址,可執行文件在內存中要超過 4G)。small 結構里面的數據,都是相對地址偏移,不是內存中的具體位置。如果要還原,需要進行計算。
該部分主要是以參考學習--iOS 恢復調用棧(適配iOS14),如需詳細了解,請移步該文章。
2、消息機制
(1)、方法調用的編譯過程
當我們在程序中調用方法時,格式為[obj method]
。經過編譯過程,會轉換為objc_msgSend
(調用父類方法,會轉換為objc_msgSendSuper
)。
/**
* 發送消息給類的實例對象,并有一個簡單的返回信息
*
* @param self 接受這個消息的類的實例對象的指針
* @param op 處理消息的方法的selector.
* @note 當發生方法調用的時候,編譯器會把方法調用編譯為objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, objc_msgSendSuper_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);
/**
* 給一個成員變量的父類發送一條消息
*
* @param super 一個objc_super類型的結構體的指針,用該值判定把消息發送給誰
* @param op 處理這條消息的方法的selector
* @see objc_msgSend
*/
OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
(2)、方法的查找過程
方法的查找過程,大概可以分為三個步驟:
a、 查找類的方法緩存cache_t
b、查找類的方法列表cls->data()->methods()
c、遞歸調用上一級父類,按照a和b的方式,在父類的緩存和方法列表中查找
d、在類及其父類中,沒有找到IMP,嘗試方法的動態解析過程
下面追個對每個步驟進行說明:
a、 查找類的方法緩存cache_t
在類的緩存中查詢使用的是匯編語言,查詢過程中更加的快捷高效。
類的方法緩存,查詢流程如圖所示:
_objc_msgSend
:方法的實現代碼(這里以——arm64——
系統進行說明)
//——arm64——
//入口
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
//p0是objc_msgSend的第一個參數-消息接收者receiver,判斷是不是nil或者tagged pointer
//cmp語句:比較指令,其內部就是進行減法運算,但不影響值。使用b語句進行跳轉
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS //tagged pointer true amr64
//上面cmp的語句的結果,如果是小于等于(le)0,那么消息接受這是空或者是tagged pointer,
//則執行跳轉(b)標號為LNilOrTagged的程序
b.le LNilOrTagged // (MSB tagged pointer looks negative)
//b.le這句也包含著,如果cmp的結果大于0,則繼續執行下面的代碼
#else
//cmp指令為0的情況下,即消息接受者為nil,跳轉LReturnZero
b.eq LReturnZero
#endif
//ldr:讀取指令,從寄存器讀取內容的指令
//x0是self,類偏移0的位置存儲的是isa,即把self的isa給到p13
ldr p13, [x0] // p13 = isa
//執行 GetClassFromIsa_p16 參數為(isa,1,self)。
//內部調用ExtractISA,本質是isa & isa_Mask來獲得類地址
//從isa中獲取cls
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
LGetIsaDone://通過上面的過程,根據isa獲取到了cls
//去cls的緩存cache中查詢IMP
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
//對cmp結果判斷,小于0或者等于0的情況下,都進來
//再次對cmp的結果進行判斷,是不是等于0,即消息接受者為nil。是的話,返回為空
b.eq LReturnZero // nil check
//下面的情況就是接受者的地址指針為tagged pointer
//內存平移獲取index,再去找到cls
GetTaggedClass
//找到了cls,跳轉到LGetIsaDone標號,執行CacheLookup
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
根據上述匯編,轉的偽代碼
_objc_msgSend(void){}
//檢查p0(消息接受者)是不是nil或者tagged pointer。
//result等于0,p0是nil;result<0,p0是tagged pointer
result = p0 - #0;
//如果p0是nil的話,清理寄存器,直接返回空
if(result == 0){
//調用LReturnZero
//返回空
}
//p0不是nil的情況下,是否為tagged pointer對于獲取cls的方式是不一樣的,所以需要對tagged pointer進行判斷。
if(true amr64){
if(result < 0){//tagged pointer
//調用GetTaggedClass方法,根據isa獲得cls
GetTaggedClass;
//調用LGetIsaDone標號,進入CacheLookup方法。在CacheLookup方法中,查詢類的cache
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
}else if(result > 0){{
//調用GetClassFromIsa_p16方法,根據isa獲得cls
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
//執行到LGetIsaDone標號,進入CacheLookup方法。在CacheLookup方法中,查詢類的cache
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
}
}else{
//調用GetClassFromIsa_p16方法,根據isa獲得cls
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
//執行到LGetIsaDone標號,進入CacheLookup方法。在CacheLookup方法中,查詢類的cache
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
}
}
根據isa獲取cls的方法和過程
下面是根據已經找到的cls,去類中cache中,查詢緩存方法的流程,調用的是
CacheLookup
方法。
//在類的方法緩存中通過sel去查找imp
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
// cacheLookup的mode,有NORMAL | GETIMP | LOOKUP
// GETIMP:
// 緩存不存在的話,返回null,x0設置為0
// The cache-miss is just returning NULL (setting x0 to 0)
//
// NORMAL and LOOKUP:
// - x0 contains the receiver
// - x1 contains the selector 即方法的SEL
// - x16 contains the isa 即cls
// - other registers are set as per calling conventions
//
mov x15, x16 // stash the original isa
LLookupStart\Function:
// p1 = SEL, p16 = isa
/*****vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv********/
//以下是不同系統中,buckets和mask的取值方式,準備相關數據
//最終獲得的結果信息如下
// p10 = buckets
// p11 = mask(arm64真機是_bucketsAndMaybeMask)
// p12 = index
/***************************************************************/
//arm64 64 OSX/SIMULATOR
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
//類的isa地址偏移16個字節,(即isa的8個字節、superClass的8個字節),就得到cache的地址,也就是_bucketsAndMaybeMask
//存入到p10
ldr p10, [x16, #CACHE] // p10 = mask|buckets
//也就是_bucketsAndMaybeMask低48位是存放方法的空間首地址;高16位是存放mask的空間的首地址
//p10的第48位,即mask的首地址,存入到p11
lsr p11, p10, #48 // p11 = mask
//_bucketsAndMaybeMask和#0xffffffffffff(即_bucketsAndMaybeMask的低48位)進行與操作,把高16位置0,得到方法的首地址
//方法的首地址存放到p10
and p10, p10, #0xffffffffffff // p10 = buckets
//x12 = cmd & mask w1為第二個參數cmd(self,cmd...),w11也就是p11
and w12, w1, w11 // x12 = _cmd & mask
//arm64 64 真機
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//類的isa地址偏移16個字節,(即isa的8個字節、superClass的8個字節),就得到cache的地址,也就是_bucketsAndMaybeMask
//存入到p11,即這里的p11是_bucketsAndMaybeMask
ldr p11, [x16, #CACHE] // p11 = mask|buckets
////arm64 + iOS + !模擬器 + 非mac應用
#if CONFIG_USE_PREOPT_CACHES
//iphone 12以后指針驗證
#if __has_feature(ptrauth_calls)
//tbnz 測試位不為0則跳轉
tbnz p11, #0, LLookupPreopt\Function
//_bucketsAndMaybeMask和#0xffffffffffff(即_bucketsAndMaybeMask的低48位)進行與操作,把高16位置0,得到方法的首地址
//方法的首地址存放到p10
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else
//p10 = _bucketsAndMaybeMask & 0x0000fffffffffffe = buckets
and p10, p11, #0x0000fffffffffffe // p10 = buckets
//p11 第0位不為0則跳轉 LLookupPreopt\Function。
tbnz p11, #0, LLookupPreopt\Function
#endif
//eor 邏輯異或(^) 格式為:EOR{S}{cond} Rd, Rn, Operand2
//p12 = selector ^ (selector >> 7) select 右移7位&自己給到p12
eor p12, p1, p1, LSR #7
//p12 = p12 & (_bucketsAndMaybeMask >> 48) = p12 & mask = buckets中的下標
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
//_bucketsAndMaybeMask和#0xffffffffffff(即_bucketsAndMaybeMask的低48位)進行與操作,把高16位置0,得到方法的首地址
//方法的首地址存放到p10
and p10, p11, #0x0000ffffffffffff // p10 = buckets
//p12 = selector & (_bucketsAndMaybeMask >>48) = sel & mask = buckets中的下標
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
//arm64 32
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
ldr p11, [x16, #CACHE] // p11 = mask|buckets
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
/***************************************************************/
//以上是不同系統中,buckets和mask的取值方式
//最終獲得的結果信息如下
// p10 = buckets
// p11 = mask(arm64真機是_bucketsAndMaybeMask)
// p12 = index
/****^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^*****/
// p13(bucket_t) = buckets + 下標 << 4 PTRSHIFT arm64 為3.
// 4位為16字節,所以 buckets + 下標 *16 = buckets + index *16 也就是直接平移到了第index個元素的地址。
//這里p13為buckets中間的一個bucket的寄存器
//所以這里的do-while是一個p13不斷前向遍歷的過程,遍歷到bucket的首地址是停止。即這個do-while是遍歷的buckets的前半部分
add p13, p10, p12, LSL #(1+PTRSHIFT) // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// do {
//ldp 寄存器1 寄存器2 寄存器3 #xx:讀取寄存器3 --> 內存地址 --> 內存數據,按字節順序放入寄存器1和寄存器2中。然后寄存器3的內存地址 - xx
//把x13(bucket)的imp和sel讀取出來,依次放入p17、p9寄存器中,并且x13的內存地址減小一個bucket的大小
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
cmp p9, p1 // if (sel != _cmd) {
b.ne 3f // scan more
// } else {
2: CacheHit \Mode // hit: call or return imp
// }
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
cmp p13, p10 // } while (bucket >= buckets)
//b.hs指令是判斷是否無符號小于
b.hs 1b
//在buckets的前半部分[0 - index]沒有找到。繼續在后半部分機進行查找(index - (buckets.count-)]
/*****vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv********/
//以下是不同系統中根據mask獲取最后一個bucket地址,即準備數據
//p13 = 最后一個bucket的地址
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
add p13, p10, w11, UXTW #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//p13 = buckets + (mask >> 44) 這里右移44位,少移動4位就不用再左移了。
//因為maskZeroBits的存在 就找到了mask對應元素的地址 這里沒搞明白
add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
// p13 = buckets + (mask << 1+PTRSHIFT)
// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
add p13, p10, p11, LSL #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
/****^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^*****/
// 根據上面 p13 = 最后一個bucket的地址,也是查詢的有邊界
//p12 = buckets + (p12<<4) index對應的bucket_t ,也就上此查詢的開始地址,用來做查詢的左邊界
//(p12 p13];
add p12, p10, p12, LSL #(1+PTRSHIFT) // p12 = first probed bucket
// do {
4: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
cmp p9, p1 // if (sel == _cmd)
//如果查詢到了,跳到2標號,cacheHit
b.eq 2b // goto hit
//循環條件,p12 p13都有值,且p13 > p12
cmp p9, #0 // } while (sel != 0 &&
ccmp p13, p12, #0, ne // bucket > first_probed)
//符合循環條件,繼續循環執行
b.hi 4b
//遍歷全部的buckets,沒找到imp,跳轉到 __objc_msgSend_uncached
LLookupEnd\Function:
LLookupRecover\Function:
b \MissLabelDynamic
/*其他代碼*/
.endmacro
根據上述流程寫出的偽代碼,如下:
//CacheLookup的偽代碼
CacheLookup(Mode, Function, MissLabelDynamic, MissLabelConstant){
//1、各個系統架構下,獲取到buckets、mask、并且處理后得到遍歷的標識位index等信息,
p10 = buckets;
p11 = mask;//(arm64真機是_bucketsAndMaybeMask)
p12 = index;
/***2、buckets中前半部分的遍歷操作[0, index]***/
// p13 = buckets + index *16;
//獲取到第index個bucket的地址,并且賦值到p13寄存器中
p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT));
do{
//把p13存放的bucket的imp賦值給p17
p17 = p13.imp;
//把p13存放的bucket的sel賦值給p9
p9 = p13.sel;
//即p13的地址向前移一個bucket大小,即p13改為前一個bucket的地址
p13 = p13 - BUCKET_SIZE;
//p1是要查找的方法的SEL
//查找到了方法的IMP。跳轉到CacheHit 傳值為 NORMAL
if(p9 == p1){
//調用cacheHit方法
CacheHit NORMAL
break;
}
//沒有找到的情況下
if(p9 == nil){
//證明cache有問題,跳轉到 __objc_msgSend_uncached
__objc_msgSend_uncached
break;
}
}while(p13 >= p10);//即前向遍歷,p13的地址到了第一個bucket(p10)的地址
/***3、在buckets的前半部分沒有找到方法的IMP,需要查找buckets的后半部分(index, buckets.count]***/
//各個系統架構下,取得mask位的地址,獲得buckets最后的bucket的地址,賦值給p13.用來做查詢的右邊界
p13 = buckets.count - 1;
//p12 index對應的bucket_t ,也就上此查詢的開始地址,用來做查詢的左邊界
p12 = buckets + (p12<<4);
do{
//把p13存放的bucket的imp賦值給p17
p17 = p13.imp;
//把p13存放的bucket的sel賦值給p9
p9 = p13.sel;
//即p13的地址向前移一個bucket大小,即p13改為前一個bucket的地址
p13 = p13 - BUCKET_SIZE;
//p1是要查找的方法的SEL
//查找到了方法的IMP。跳轉到CacheHit 傳值為 NORMAL
if(p9 == p1){
//調用cacheHit方法
CacheHit NORMAL
break;
}
}while((p9 != nil) && (p13 > p12);//循環條件,p12 p13都有值,且p13 > p12,p9不為nil
/***4、遍歷全部的buckets,沒有找到方法的IMP,跳轉到 __objc_msgSend_uncached***/
__objc_msgSend_uncached;
}
找到IMP后的實現流程
方法列表、查詢父類、動態查詢相關代碼
在當前類的方法緩存中cache
沒有找到方法的對應IMP。那么接下來就是要到類的方法列表中,遍歷方法列表。
上面的圖能顯示出來,緩存中找不到方法的情況下,會調用__objc_msgSend_uncached
方法,而這個方法最終會調用到IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
。這個方法中,使用for循環去查詢類的緩存或者類的方法列表,并且在上述查不到的情況下,遞歸調用父類,查詢父類的緩存和方法列表。直到找到IMP,或者特殊情況的break,才能跳出for 循環。
下面是IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
方法的源碼,查找類的方法列表、查詢父類以及方法的動態解析都在這個方法中。
NEVER_INLINE
//在MethodTableLookup方法中,傳進來的behavior = 3
//LOOKUP_INITIALIZE | LOOKUP_RESOLVER // LOOKUP_INITIALIZE = 1;LOOKUP_RESOLVER = 2
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
//forward_imp賦值
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
//要返回的imp
IMP imp = nil;
//當前查找的cls
Class curClass;
runtimeLock.assertUnlocked();
//如果類沒有初始化,behavior會增加 LOOKUP_NOCACHE。
if (slowpath(!cls->isInitialized())) {
//behavior = 3// 011
//LOOKUP_NOCACHE = 8// 1000
//behavior |= LOOKUP_NOCACHE = 0011 | 1000 = 1011
behavior |= LOOKUP_NOCACHE;
}
//加鎖
runtimeLock.lock();
//在緩存內、loaded image的數據段內或已使用obj_allocateClassPair分配,則返回true
checkIsKnownClass(cls);
//如果類沒有初始化,則對類進行初始化。
//在類的初始化過程中,完成對rw、ro的準備工作,并且對父類及其原類進行數據準備
//這樣也為后面的查詢,提供了數據信息
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
// 解鎖
runtimeLock.assertLocked();
//賦值要查找的類
curClass = cls;
/*--循環開始--*/
//在上限范圍內循環,除非return/break
//Provides an upper bound for any iteration of classes, to prevent spins when runtime metadata is corrupted.
for (unsigned attempts = unreasonableClassCount();;) {
//先去緩存查找,防止這個時候共享緩存中已經寫入了該方法。
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES //defined(__arm64__) && TARGET_OS_IOS && !TARGET_OS_SIMULATOR && !TARGET_OS_MACCATALYST
//iOS
//這里也是調用到了`_cache_getImp`匯編代碼,最終調用了`CacheLookup`查找緩存
imp = cache_getImp(curClass, sel);
//找到后直接跳轉done_unlock
if (imp) goto done_unlock;
//這個操作是要做什么?
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
// curClass method list.
// curClass method list.查找類的方法列表,cls->data()->methods()
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
//找到了IMP,跳到done,插入緩存
imp = meth->imp(false);
goto done;
}
//這里curClass 會被賦值為上一級父類,
//也就是自己類沒有查找到,就去查找父類的方法列表
//如果到了NSObject的父類,也就是nil,就賦值imp為foreard_imp。break出循環
if (slowpath((curClass = curClass->getSuperclass()) == 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.");
}
// 遞歸調用父類的查詢機制,顯示緩存再是方法裂列表
//_cache_getImp --> CacheLookup --> __objc_msgSend_uncached -->MethodTableLookup --> _lookUpImpOrForward
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.
break;
}
if (fastpath(imp)) {
// Found the method in a superclass. Cache it in this class.
goto done;
}
}
/*--循環結束--*/
//在方法列表中沒有找到實現,開始動態解析
// No implementation found. Try method resolver once.
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
//該方法在沒有找到動態解析的方法時,也是會返回nil。
//這樣就得進入到消息轉發流程了
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
//完成
if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
cls = cls->cache.preoptFallbackClass();
}
#endif
//把方法存儲到緩存中。方法的IMP,是在本次for結束后,下次for開始時查詢類的緩存中進行返回的
//也就是在類的方法列表,或者在父類中找到的方法IMP,都要在done_unlock中返回
log_and_fill_cache(cls, imp, sel, inst, curClass);
}
done_unlock:
runtimeLock.unlock();
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
//返回方法的IMP
return imp;
}
該方法的查找流程如下圖所示:b、查找類的方法列表cls->data()->methods()
在類的方法列表中查詢IMP的代碼如下:
// curClass method list.
// curClass method list.查找類的方法列表,cls->data()->methods()
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
goto done;
}
調用static method_t *getMethodNoSuper_nolock(Class cls, SEL sel)
方法進行的查詢
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
// fixme nil cls?
// fixme nil sel?
auto const methods = cls->data()->methods();
for (auto mlists = methods.beginLists(),
end = methods.endLists();
mlists != end;
++mlists)
{
// <rdar://problem/46904873> getMethodNoSuper_nolock is the hottest
// caller of search_method_list, inlining it turns
// getMethodNoSuper_nolock into a frame-less function and eliminates
// any store from this codepath.
method_t *m = search_method_list_inline(*mlists, sel);
if (m) return m;
}
return nil;
}
最終會調用到static method_t * findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
方法,該方法使用二分法對method_list_t
進行遍歷。
template<class getNameFunc>
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
ASSERT(list);
auto first = list->begin();
auto base = first;
decltype(first) probe;
//目標sel所在的位置
uintptr_t keyValue = (uintptr_t)key;
//方法列表中的數量
uint32_t count;
//使用二分法查詢
//count >>= 1 count右移以一位,相當于count / 2.
for (count = list->count; count != 0; count >>= 1) {
//計算probe查詢位置
probe = base + (count >> 1);
//獲取到查詢位置的sel,與keyValue對比
uintptr_t probeValue = (uintptr_t)getName(probe);
if (keyValue == probeValue) {
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
probe--;
}
return &*probe;
}
//probe位置的sel不是要查詢的sel,并且目標sel的位置比probe要大
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
//probe位置的sel不是要查詢的sel,如果目標sel的位置小于等于probe的話,進入到下一次for循環,
//即count>>1后,再次計算probe
}
return nil;
}
下面,我們帶入數字走一遍查詢流程:
假設現在的method_list_t
中右10條方法,則list->count = 10
,并且假設目標sel的位置在第3位,那么首次遍歷的初始條件為:
第1次:
base = 0; count = 10; keyValue = 3;
>>> probe = base + count>>1 = 0 + 10 / 2 = 5;
>>>> 不是要找的sel,并且keyvalue<probe,不做操作。
>>>>>在for結束時執行count>>1
,即:count = 10>>1 = 5;
>>>>>>綜上,得出下一次的相關數據:base = 0; count = 5; keyValue = 3;
第2次:
base = 0; count = 5; keyValue = 3;
>>> probe = base + count>>1 = 0 + 5 / 2 = 2;
>>>> 不是要找的sel,并且keyvalue > probe,執行 base = probe+1; count--;
操作。即:base = 2+1 = 3;count = 5-1=4
>>>>> 在for結束時執行count>>1
,即:count = 4>>1 = 2;
>>>>>>綜上,得出下一次的相關數據:base = 3; count = 2; keyValue = 3;
第3次:
base = 3; count = 2; keyValue = 3;
>>> probe = base + count>>1 = 3 + 2 / 2 = 4;
>>>> 不是要找的sel,且keyvalue < probe,不做操作。
>>>>> 在for結束時執行count>>1
,即:count = 2>>1 = 1;
>>>>>>綜上,得出下一次的相關數據:base = 3; count = 1; keyValue = 3;
第4次:
base = 3; count = 1; keyValue = 3;
>>> probe = base + count>>1 = 3 + 1 / 2 = 3;
>>>> 該位置就是要查詢的sel,查找結束。
在查詢流程結束后,會進入到一個while
循環,來查看當前位置之前,是不是有相同SEL的方法。如果有的話,調用前面的IMP。這種情況發生在有分類并且分類中也實現了相同的方法。分類的方法是在類的前面的,優先調用分類中的方法。
c、遞歸調用上一級父類,按照a和b的方式,在父類的緩存和方法列表中查找
- 1、在方法的列表中沒有找到方法IMP,把curClass->superClass賦值給curClass
- 2、調用“cache_getImp”方法
- 3、cache_getImp是要調用“CacheLookup”,這樣又走了一遍匯編查詢cache、查詢類的方法列表、查詢父類方法列表的流程。
- 4、因此在這里進行了遞歸調用
- 5、一旦在父類中查詢到了方法的實現,需要在方法接受類的緩存中添加該方法。
- 6、在下一次的for循環中,從cache中讀取并調用IMP。
d、在類及其父類中,沒有找到IMP,嘗試方法的動態解析過程
目標sel的方法實現imp沒找到,系統會再給一次補救機會:動態方法解析。
下面兩個方法,是實現動態解析的實現,在方法的實現中,可以動態的為sel添加IMP
實例方法:+ (BOOL)resolveInstanceMethod:(SEL)sel;
類方法:+ (BOOL)resolveClassMethod:(SEL)sel;
動態解析使用舉例:
/*======================================*/
/*********--MethodSend的類聲明--**********/
/*=====================================*/
@interface MethodSend : NSObject
//聲明實例方法,但不實現該方法
-(void)instanceMethodResolveTest;
//聲明類方法,但不實現該方法
+(void)classMethodResolveTest;
//聲明一個實例方法,但不實現該方法,并且讓另外一個類實現動態解析
-(void)otherCls_InstanceMethodResolveTest;
@end
/*======================================*/
/*********--MethodSend的類實現--**********/
/*=====================================*/
#import "Dog.h"
#import "MethodSend.h"
#include <objc/runtime.h>
@implementation MethodSend
/*--類方法的動態解析--*/
+ (BOOL)resolveClassMethod:(SEL)sel{
if (sel == @selector(classMethodResolveTest)) {
class_addMethod(object_getClass(self),
sel,
class_getMethodImplementation(object_getClass(self), @selector(dynamicClassIMP)),
"v@:");
return true;
}
return [class_getSuperclass(self) resolveClassMethod:sel];
}
+(void)dynamicClassIMP{
NSLog(@"類方法--classMethodResolveTest--的動態解析:IMP");
}
/*--對象方法的動態解析--*/
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(instanceMethodResolveTest)) {
class_addMethod([self class],
sel,
class_getMethodImplementation([self class],
@selector(dynamicInstanceIMP)),
"v@:");
return true;
}else if (sel == @selector(otherCls_InstanceMethodResolveTest)) {
class_addMethod([self class],
sel,
class_getMethodImplementation([Dog class], @selector(dog_dynamicInstanceResolve)),
"v@:");
return true;
}
return [super resolveInstanceMethod:sel];
}
-(void)dynamicInstanceIMP{
NSLog(@"實例方法--instanceMethodResolveTest--的動態解析:IMP");
}
@end
/*====================================================================*/
/*********--在Dog類中實現“otherCls_InstanceMethodResolveTest”--**********/
/*====================================================================*/
#import "Dog.h"
@implementation Dog
- (void)dog_dynamicInstanceResolve{
NSLog(@"~~~~我是“Dog”類的動態解析方法~~~");
}
@end
打印結果如下:
2022-07-31 11:58:42.412965+0800 schemeUse[3546:138093] 實例方法--instanceMethodResolveTest--的動態解析:IMP
2022-07-31 11:58:42.413031+0800 schemeUse[3546:138093] 類方法--classMethodResolveTest--的動態解析:IMP
2022-07-31 11:58:42.413075+0800 schemeUse[3546:138093] ~~~~我是“Dog”類的動態解析方法~~~
3、消息轉發
動態方法解析失敗后流程會被標記,隨即再次觸發消息查詢,此時會跳過動態方法解析流程直接進行消息轉發。
所謂消息轉發,是將當前消息轉發到其它對象進行處理。(這里可以做出類的虛假的多繼承,即一個類繼承了多個類,使用每個類中的方法)
(1)、快速轉發
轉發實例方法: - (id)forwardingTargetForSelector:(SEL)aSelector
轉發類方法,id需要返回類對象: + (id)forwardingTargetForSelector:(SEL)sel
定義在NSObject
類中,默認返回nil
- (id)forwardingTargetForSelector:(SEL)sel {
return nil;
}
+ (id)forwardingTargetForSelector:(SEL)sel {
return nil;
}
舉例如下:
/*======================================*/
/*********--MethodSend的類聲明--**********/
/*=====================================*/
@interface MethodSend : NSObject
//聲明實例方法,但不實現該方法
-(void)instanceMethodResolveTest;
//聲明類方法,但不實現該方法
+(void)classMethodResolveTest;
@end
/*======================================*/
/*********--MethodSend的類實現--**********/
/*=====================================*/
@implementation MethodSend
//快速forward,實例對象的消息轉發
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(instanceMethodResolveTest)) {
//轉發到Dog類,讓Dog類的instanceMethodResolveTest方法,進行實現
return [[Dog alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
//快速forward,類方法的消息轉發
+ (id)forwardingTargetForSelector:(SEL)sel {
if (sel == @selector(classMethodResolveTest)) {
//轉發類方法,id需要返回類對象
return NSClassFromString(@"Dog");
}
return [super forwardingTargetForSelector:sel];
}
@end
/*======================================*/
/*********--Dog的類聲明--**********/
/*=====================================*/
@interface Dog : NSObject
//對象方法
-(void)instanceMethodResolveTest;
//類方法
+(void)classMethodResolveTest;
@end
/*====================================================================*/
/*********--在Dog類中實現對象方法“instanceMethodResolveTest”--**********/
/************--在Dog類中實現類方啊“classMethodResolveTest”--************/
/*====================================================================*/
@implementation Dog
-(void)instanceMethodResolveTest{
NSLog(@"instanceMethodResolveTest 在 Dog 中實現了");
}
+(void)classMethodResolveTest{
NSLog(@"classMethodResolveTest 在 Dog 中實現了");
}
@end
打印結果如下
MethodSend *ms = [[MethodSend alloc] init];
//實例方法消息轉發
[ms instanceMethodResolveTest];
//類方法的消息轉發
[MethodSend classMethodResolveTest];
2022-08-01 17:30:24.147594+0800 schemeUse[15671:253255] instanceMethodResolveTest 在 Dog 中實現了
2022-08-01 17:30:24.147658+0800 schemeUse[15671:253255] classMethodResolveTest 在 Dog 中實現了
(2)、慢速轉發
如果forwardingTargetForSelector沒有實現,或返回了nil或self,則會進入另一個轉發流程。
它會依次調用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
,然后runtime會根據該方法返回的值,組成一個NSInvocation對象。如果返回的方法簽名為nil,則直接崩潰報錯。如果返回的方法簽名不為nil,走到forwardInvocation方法中,對invocation事務進行處理,如果不處理也不會報錯
再去調用- (void)forwardInvocation:(NSInvocation *)anInvocation
。注意,當調用到forwardInvocation時,無論我們是否實現了該方法,系統都默認消息已經得到解析,不會引起crash。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if (aSelector == @selector(instanceMethodResolveTest)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s",__func__);
}
(3)、doesNotRecognizeSelector
如果沒有方法的動態解析過程呢,也沒有消息轉發過程的話,則直接調用 doesNotRecognizeSelector拋出異常信息。
(4)、圖示轉發流程
4、調用super
的方法
前面圖中說過,調用super
方法的時候,在編譯階段,會編譯成 void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
這種形式。并以這種形式完成消息的傳遞。
調用父類方法的格式為[super xxxx: xx]
, 其中super
會被編譯成objc_super *super
結構體。也就是objc_msgSendSuper
的第一個參數。
objc_super *super
結構如下:
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
//類的實例變量,也是實際消息的接受者。
//即使使用super的方法,該父類方法的消息最終還是要發送給調用super方法所在的類的實例對象
__unsafe_unretained _Nonnull id receiver;
//下面是對象>類>superClass ,以super
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
/* super_class is the first class to search */
};
當調用super method時,runtime會到super class中找到IMP,然后發送到當前class的實例上。
5、幾個特殊方法
1、class
與object_getClass
實例對象的class方法
:
- (Class)class
方法內部直接返回的是object_getClass(self)
,所以實例對象的class
和object_getClass
是一樣的。
- (Class)class {
return object_getClass(self);
}
示例代碼:從打印結果和地址的輸出,可以看出兩者是同樣的地址
- (void)instanceMethod{
Class cls_Clazz = [self class];
Class cls_Objc = object_getClass(self);
NSLog(@"cls_Clazz is %@ -- cls_Objc is %@",cls_Clazz,cls_Objc);
}
//打印結果為
2022-08-02 17:28:18.933562+0800 schemeUse[6677:286665] cls_Clazz is MethodSend -- cls_Objc is MethodSend
//對cls_Clazz和cls_Objc 輸出:
(lldb) p/x cls_Clazz
(Class) $0 = 0x0000000104fdd380 MethodSend
(lldb) p/x cls_Objc
(Class) $1 = 0x0000000104fdd380 MethodSend
類對象的class
方法:
在+ (Class)class
方法內部,返回了self。而如果在類方法中調用object_getClass
,其實返回的是元類。所以在類方法中,使用這兩個函數,是完全不同的。
+ (Class)class {
return self;
}
示例代碼:從打印結果和地址的輸出,打印出來的類是一樣但是兩者地址是不同的。
+ (void)classMethod{
Class cls_Clazz = [self class];
Class cls_Objc = object_getClass(self);
NSLog(@"cls_Clazz is %@ -- cls_Objc is %@",cls_Clazz,cls_Objc);
}
//打印結果為
2022-08-02 17:36:09.385057+0800 schemeUse[6795:292600] cls_Clazz is MethodSend -- cls_Objc is MethodSend
//對cls_Clazz和cls_Objc 輸出:
(lldb) p/x cls_Clazz
(Class) $0 = 0x0000000102ed5380 MethodSend
(lldb) p/x cls_Objc
(Class) $1 = 0x0000000102ed5358
2、isMemberOfClass
實例方法:
用于判斷當前對象所在的類是不是和目標類cls相同
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
類方法:
用于判斷當前類對應的元類是不是和目標類cls相同
+ (BOOL)isMemberOfClass:(Class)cls {
return self->ISA() == cls;
}
3、isKindOfClass
實例方法:
用于判斷對象的類或者父類 ,有能與cls匹配即返回true。
- (BOOL)isKindOfClass:(Class)cls {
//取出對象的類,并遍歷自己的父類,只要能和cls匹配,就返回true
for (Class tcls = [self class]; tcls; tcls = tcls->getSuperclass()) {
if (tcls == cls) return YES;
}
return NO;
}
類方法:
用于判斷當前類對應的的元類及元類的父類,只要能和cls匹配,就返回true
+ (BOOL)isKindOfClass:(Class)cls {
//當前self為類,類的isa指向為元類
//取出當前類的元類,并遍歷元類的父類,只要能和cls匹配,就返回true
for (Class tcls = self->ISA(); tcls; tcls = tcls->getSuperclass()) {
if (tcls == cls) return YES;
}
return NO;
}
4、isKindOfClass
和isMemberOfClass
的面試題
isKindOfClass
和isMemberOfClass
的面試題,結合 isa
指針指向:
BOOL result1 = [[NSObject class] isKindOfClass:[NSObject class]];
BOOL result2 = [[NSObject class] isMemberOfClass:[NSObject class]];
BOOL result3 = [[Person class] isKindOfClass:[Person class]];
BOOL result4 = [[Person class] isMemberOfClass:[Person class]];
BOOL result5 = [[Person class] isKindOfClass:[NSObject class]];
得出的結果為:
2022-08-03 10:19:23.659741+0800 schemeUse[2917:111755] result1 = 1
2022-08-03 10:19:23.659798+0800 schemeUse[2917:111755] result2 = 0
2022-08-03 10:19:23.659836+0800 schemeUse[2917:111755] result3 = 0
2022-08-03 10:19:23.659873+0800 schemeUse[2917:111755] result4 = 0
2022-08-03 10:19:23.659916+0800 schemeUse[2917:111755] result5 = 1