objc_msgsend
runtime運行時
編譯時:顧名思義就是正在編譯的時候,把源代碼編譯成機器能識別的語言,主要是對語言進行最基本的檢查報錯,即詞法分析、語法分析等,是一個靜態的階段。
command + b
。-
運行時:就是代碼跑起來被裝載到內存中去了,如果此時出錯,程序會崩潰,是一個動態的階段。
command + R
runtime
:一套由C/C++``匯編
寫成的為我們OC
提供運行時
功能的API
。現行對應的編程接口:Objective2.0
。runtime的使用有以下三種方式,其中三種實現方法與編譯層和底層的關系圖,如下圖所示:
image.png 通過
OC
代碼,如直接調用方法[LGPerson sayHello]
等。通過
NSObject
方法,如isKindOfClass
,isMemberOfClass
等。通過
runtim API
,如class_getInstanceSize
等底層方法。
其中Compiler
就是我們了解的編譯器,即LLVM
,runtime System Library
--底層庫。
探索OC方法本質
- 1、新建一個Person類,并聲明一個實例方法。
@interface Person : NSObject
-(void)eat;
@end
@implementation Person
-(void)eat {
NSLog(@"%s",__func__);
}
@end
- 2、在main.h文件中,初始化Person,并調用eat方法。
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
Person * p = [Person alloc];
[p eat];
NSLog(@"Hello, World!");
}
return 0;
}
- 3、執行clang,將main.m文件編譯成main.cpp文件,在main.cpp文件中我們找到main函數。
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_jf_j8m194qs4517fp_kg01ywhhh0000gn_T_main_b774b2_mi_0);
Person * p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("eat"));
}
return 0;
}
分析main.cpp
文件的main
函數我們發現,在調用alloc
,eat
方法時都使用了objc_msgSend
這個方法,消息發送。然后通過runtime
直接在main
函數使用這個objc_msgSend
方式調用一下eat
方法,看能否打印出結果。
1、直接調用objc_msgSend
,需要導入頭文件#import <objc/message.h>
。
2、需要將target --> Build Setting -->搜索msg -- 將enable strict checking of obc_msgSend calls由YES 改為NO
,將嚴厲的檢查機制關掉,否則objc_msgSend
的參數會報錯。
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
Person * p = [Person alloc];
[p eat];
objc_msgSend((id)p, sel_registerName("eat"));
NSLog(@"Hello, World!");
}
return 0;
}
運行打印結果如下:
調用父類方法
新建Teacher類繼承于Person類。
#import "Person.h"
@interface Teacher : Person
@end
@implementation Teacher
@end
在main.m文件中,使用聲明并teacher,并調用eat方法;使用objc_msgSendSuper調用eat方法。
struct objc_super teacher;
Teacher * t = [Teacher alloc];
[t eat];
teacher.receiver = t;
teacher.super_class = [t class];
objc_msgSendSuper(&teacher, sel_registerName("eat"));
打印結果如下:
發現:兩種方式都調用了父類Person
的eat
的方法。
方法的本質就是發送消息,OC調用方法等價于runtime中的objc_msgSend和objc_msgSendSuper消息發送
。方法的調用,首先是從類中查找,如果類中沒有找到,就會去類的父類中查找。
objc_msgSend快速查找流程分析
在objc4-781
源碼中,搜索objc_msgSend
,由于我們日常開發的都是架構是arm64
,所以需要在arm64.s
后綴的文件中查找objc_msgSend
源碼實現,發現是匯編實現。
匯編的主要特性:速度快,更容易被機器識別
。方法參數的動態性,匯編調用函數時傳遞的參數是不確定的
,那么發送消息時,直接調用一個函數就可以發送所有的消息。
快速查找:cache
中查找
慢速查找:methodList
中查找,消息轉發
流程如下圖:
源碼分析:
//消息發送 --匯編入口--obj_msgSend主要是拿到接收者的isa信息
ENTRY _objc_msgSend
//--無窗口
UNWIND _objc_msgSend, NoFrame
//--比較p0和空做對比,判斷p0(recevier)消息接收者對象是否為空,其中p0是objec_msgSend的第一個參數-消息接收者receiver
cmp p0, #0 // nil check and tagged pointer check
//--le小于--支持taggedpointer(小對象類型)的流程
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)如果支持了tagerpointer類型
#else
//--p0 等于0時 ,直接返回空
b.eq LReturnZero //返回0
#endif
//--p0即receiver 肯定存在流程
//--根據對象拿出isa,即x0寄存器指向的地址 取出isa,存入 p13寄存器
ldr p13, [x0] // p13 = isa 消息存在,拿出isa-拿到類
//--在64位架構下通過p16 = isa (p13)& isa_mask ,拿出shiftcls信息,得到class信息
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
//--如果有isa,走到cachelookup,即緩存查找流程,也就是所謂的sel-imp快速查找流程
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
1、首先判斷p0
是否為空,objc_msgSend
方法的第一個參數receiver
是否為空
* 如果為空:直接返回0 LReturnZero
* 如果支持小對象tagged pointer
,跳轉至LNilOrTagged
* 如果存在receiver(p0存在)
,不是小對象,從receiver
中取出isa
存入p13
寄存器,
通過GetClassFromIsa_p16
中,arm64
架構下通過 isa & ISA_MASK
獲取shiftcls
位域的類信息,即class
,GetClassFromIsa_p16
的匯編實現如下:
.macro GetClassFromIsa_p16 /* src */
#if SUPPORT_INDEXED_ISA
// Indexed isa
//---- 將isa的值存入p16寄存器
mov p16, $0 // optimistically set dst = src
tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer isa--判斷是否是nonapointer isa
// isa in p16 is indexed
//---- 將_objc_indexed_classes所在的頁的基址 讀入x10寄存器
adrp x10, _objc_indexed_classes@PAGE
//---- x10 = x10 + _objc_indexed_classes(page中的偏移量) --x10基址 根據 偏移量 進行 內存偏移
add x10, x10, _objc_indexed_classes@PAGEOFF
//---- 從p16的第ISA_INDEX_SHIFT位開始,提取 ISA_INDEX_BITS 位 到 p16寄存器,剩余的高位用0補充
ubfx p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS // extract index
ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:
//--用于64位系統
#elif __LP64__
// 64-bit packed isa
//---- p16 = class = isa & ISA_MASK(位運算 & 即獲取isa中的shiftcls信息)
and p16, $0, #ISA_MASK
#else
// 32-bit raw isa
// 32-bit raw isa ---- 用于32位系統
mov p16, $0
#endif
.endmacro
2、開啟緩存查找流程:CacheLookup
其匯編實現如下:
.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:
// p1 = SEL, p16 = isa #CACHE = 2 * pointer->平移16個字節得到cache_t
//_maskAndBuckets 高16位為mask bucket為低48位
//---- p1 = SEL, p16 = isa --- #define CACHE (2 * __SIZEOF_POINTER__),其中 __SIZEOF_POINTER__表示pointer的大小 ,即 2*8 = 16
//---- p11 = mask|buckets -- 從x16(即isa)中平移16字節,取出cache 存入p11寄存器 -- isa距離cache 正好16字節:isa(8字節)-superClass(8字節)-cache(mask高16位 + buckets低48位)
ldr p11, [x16, #CACHE] // p11 = mask|buckets
//-- 64位真機
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//--p11(cache) & 0x0000ffffffffffff,mask高16位抹零,得到buckets 存入p10寄存器--即去掉mask,留下buckets
and p10, p11, #0x0000ffffffffffff // p10 = buckets 獲取buckets
//--p11(cache)右移48位,得到mask(即p11 存儲 mask),mask & p1(msgSend的第二個參數cmd-sel),得到sel-imp的下標index(即搜索下標)存入p12(cache insert寫入時的哈希下標計算是通過 sel & mask,讀取時也需要通過這種方式)
and p12, p1, p11, LSR #48 // x12 = _cmd & mask 邏輯右移 獲取mask
//--非64位真機
#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
//--p12是下標,p10是buckets數組首地址,下標 * 1 << 4(即16)得到實際內存的偏移量,通過buckets的首地址偏移,獲取bucket存入p12寄存器
//--LSL#(1+PTRSHIFT)--實際含義就是得到一個bucket占用的內存的大小--相當于mask=occupied-1 --- _cmd & mask --取余數
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
//--從x12(即p12)中取出bucket 分別將imp和sel存入 p17(存儲imp)和 p9(存儲sel)、
ldp p17, p9, [x12] // {imp, sel} = *bucket
//--比較sel和p1(傳入的參數_cmd)
1: cmp p9, p1 // if (bucket->sel != _cmd)
//--如果不相等,即沒有找到,請跳轉至2f
b.ne 2f // scan more
//如果相等 即CacheHit 緩存命中 直接返回imp
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
//--如果一直都找不到 因為是normal,跳轉值__objc_msgSend_uncached
CheckMiss $0 // miss if bucket->sel == 0
//--判斷p12(下標對應的bucket) 是否等于 p0 (bucket是數組的第一個元素),如果等于則跳轉至第3步
cmp p12, p10 // wrap if bucket == buckets
//--- 定位到最后一個元素(即第一個bucket)
b.eq 3f
//--- 從x12(即p12 buckets首地址)- 實際需要平移的內存大小BUCKET_SIZE,得到得到第二個bucket元素,imp-sel分別存入p17-p9,即向前查找
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
//--- 跳轉至第1步,繼續對比 sel 與 cmd
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//--- 人為設置到最后一個元素
//--- p11(mask)右移44位 相當于mask左移4位,直接定位到buckets的最后一個元素,緩存查找順序是向前查找
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.
//--- 再查找一遍緩存()
//--- 拿到x12(即p12)bucket中的 imp-sel 分別存入 p17-p9
ldp p17, p9, [x12] // {imp, sel} = *bucket
//--- 比較 sel 與 p1(傳入的參數cmd)
1: cmp p9, p1 // if (bucket->sel != _cmd)
//--- 如果不相等,即走到第二步
b.ne 2f // scan more
//--- 如果相等 即命中,直接返回imp
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
//--- 如果一直找不到,則CheckMiss
CheckMiss $0 // miss if bucket->sel == 0
//--- 判斷p12(下標對應的bucket) 是否 等于 p10(buckets數組第一個元素)-- 表示前面已經沒有了,但是還是沒有找到
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f //如果等于,跳轉至第3步
//--- 從x12(即p12 buckets首地址)- 實際需要平移的內存大小BUCKET_SIZE,得到得到第二個bucket元素,imp-sel分別存入p17-p9,即向前查找
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
//--- 跳轉至第1步,繼續對比 sel 與 cmd
b 1b // loop
LLookupEnd$1:
LLookupRecover$1:
3: // double wrap
//--- 跳轉至JumpMiss 因為是normal ,跳轉至__objc_msgSend_uncached
JumpMiss $0
.endmacro
匯編流程分析:
LLookupStart$1
:-->很好解釋,就是開始查找
ldr p11, [x16, #CACHE]
-->從x16(即isa)
中平移16
字節,取出cache
存入p11
寄存器 -- isa
距離cache
正好16
字節:isa(8字節)
-superClass(8字節)
-cache(mask高16位 + buckets低48位)
and p10, p11, #0x0000ffffffffffff
-->p11(cache) & 0x0000ffffffffffff
,mask
高16
位抹零
,得到buckets
存入p10
寄存器--即去掉mask
,留下buckets
。
and p12, p1, p11, LSR #48
-->p11(cache)右移48位
,得到mask
(即p11
存儲 mask
),mask & p1
(msgSend
的第二個參數cmd-sel
),得到sel-imp
的下標index
(即搜索下標)存入p12
(cache insert
寫入時的哈希下標計算是通過sel & mask
,讀取時也需要通過這種方式)
add p12, p10, p12, LSL #(1+PTRSHIFT)
-->p10
上面說了是bucket
的首位,將其左移p12*4
位(p12
是傳進來方法,通過哈希算法得到的下標,每個bucket包含sel以及imp,所以每個bucket是16字節
,左移4位,就是左移16字節,下標乘于16
,就拿到cache_t
這個下標的bucket
。)
1:方法
ldp p17, p9, [x12]
-->通過bucket
的結構體得到{imp, sel} = *bucket
cmp p9, p1
-->是取的sel
跟p1
傳進來的_cmd
(就是傳進來的sel
)不相等
b.ne 2f
-->如果不相等,進入2
CacheHit $0
-->如果相等,返回imp
2:方法:上面查找不相等,下面的方法是遞歸查找
CheckMiss $0
-->如果從最后一個元素遍歷過來都找到不到,就返回CheckMiss
。
cmp p12, p10
-->我們知道p10
是第一個bucket
,p12
是算的下標,這意思(判斷下標發現不是第一個)
b.eq 3f
-->如果下標是第一個,走3
ldp p17, p9, [x12, #-BUCKET_SIZE]!
-->如果不是第一個,就向前取bucket
,循環一次對內存偏移-1
,把取的bucket
給p17
b 1b
-->執行1
3:方法:上面發現是第一個bucket
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
-->p11
右移48-(1+3)=44
位,再跟第一次通過哈希算法
的得到的下標p12
,再次進行哈希
算法。這次得到的這個下標是cache_t
的最后
一位。
再查找一遍緩存后面再執行1,2方法。
如果當前的bucket
還是等于 buckets
的第一個元素,則直接跳轉至JumpMiss
,此時的$0
是normal
,也是直接跳轉至__objc_msgSend_uncached
,即進入慢速查找流程
CheckMiss和JumpMiss的匯編流程
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
//--- 如果為GETIMP ,則跳轉至 LGetImpMiss
cbz p9, LGetImpMiss
.elseif $0 == NORMAL
//--- 如果為NORMAL ,則跳轉至 __objc_msgSend_uncached
cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
//--- 如果為LOOKUP ,則跳轉至 __objc_msgLookup_uncached
cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
.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