一尘吗、感受運行時
什么是runtime?
為OC提供運行機制浇坐,用C/C++寫成的摇予,通過底層的API、OC 源碼吗跋、調(diào)用方法、基礎語法、framework跌宛、service接口等為OC層面提供的運行制機制酗宋。
它也是為面向?qū)ο螅∣OP)提供運行時機制;在運行過程中疆拘,讓對象找到真正的執(zhí)行邏輯蜕猫,包括內(nèi)存布局(isa的走位指向)。
再來理解下Apple的
從編譯時間和鏈接時間到運行時哎迄,Objective-C語言會盡可能多地推遲決策回右。 只要有可能,它都會動態(tài)執(zhí)行操作漱挚,例如創(chuàng)建對象和確定要調(diào)用的方法翔烁。 因此,該語言不僅需要編譯器旨涝,還需要運行時系統(tǒng)來執(zhí)行編譯后的代碼蹬屹。 運行時系統(tǒng)充當Objective-C語言的一種操作系統(tǒng)。 這就是使語言有效的原因白华。 但是慨默,一般情況下,您不需要直接與運行時進行交互弧腥。--Apple
- 與Runtime交互的三種方式
1.通過Objective-C源代碼厦取;--[book write]
2.通過Framework & Service的類中定義的方法;--[[Book class] isKindOfClass:[NSObject class]]
3.通過直接調(diào)用運行時函數(shù)管搪。--objc_msgSendSuper虾攻、sel_registerName
Compiler是編譯器,即LLVM抛蚤。比如OC層面的
alloc
在LLVM的實現(xiàn)就是objc_alloc
.
方法的本質(zhì)
利用clang
指令編譯main.m
文件得到main.cpp
台谢,在之前的OC底層原理03— isa探究中稍稍介紹過獲取C++
的Clang指令:
// 模擬器sdk路徑替換自己的即可
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-12.0.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.0.sdk ViewController.m
定義一個類Book,編寫兩個方法read岁经,write 朋沮;其中read實現(xiàn),write不實現(xiàn)
Book * book = [Book alloc];
[book read];
執(zhí)行之后缀壤,在main.cpp
中找尋read,write
方法樊拓。
// main.cpp
Book * book = ((Book *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Book"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)book, sel_registerName("read"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)book, sel_registerName("write"));
通過以上,可以知道底層會將方法編譯供 objc_msgSend調(diào)用塘慕,這就是方法的本質(zhì):消息的發(fā)送(objc_msgSend)筋夏。
既然這樣我們就可以通過直接調(diào)用底層的objc_msgSend
來調(diào)用方法;我們先導入頭文件#import <objc/message.h>
;為了不報錯图呢,我們在target —— Build Setting —— 搜索msgSend, 把 enable strict checking of obc_msgSend calls由YES 改為NO条篷,將嚴厲的檢查機制關掉骗随。
調(diào)用方法:
NSSeletorFromString() (OC層) = @seletor()(OC層)= sel_registerName(runtime)
在調(diào)用Book類方法后調(diào)用objc_msgSend(book,sel_registerName("read"));
得到以下的結果
再一次驗證方法的本質(zhì)是通過消息的發(fā)送。
對象方法是否能執(zhí)行父類的實現(xiàn)
定義兩個類:Book 和 English 赴叹,Book中實現(xiàn)read方法鸿染,English中實現(xiàn)write方法
@interface Book : NSObject
-(void)read;
@end
@implementation Book
- (void)read{
NSLog(@"read some book");
}
@end
@interface English : Book
-(void)write;
@end
@implementation English
-(void)write{
NSLog(@"write A B C");
}
@end
通過調(diào)用以下:
English * english = [English alloc];
[english read];
struct objc_super mysuper;
mysuper.receiver = English;
mysuper.super_class = [Book class];
objc_msgSendSuper(&mysuper, sel_registerName("read"));
執(zhí)行結果:
子類調(diào)用父類方法,這很好理解乞巧。消息的發(fā)送流程中涨椒,消息的接收是english,但是具體的實現(xiàn)绽媒,可以執(zhí)行父類Book中的
read
實現(xiàn)蚕冬。objc_msgSendSuper方法中有兩個參數(shù)(struct objc_super *,SEL)是辕,其結構體類型是objc_super定義的結構體對象囤热,且需要指定receiver 和 super_class兩個屬性,源碼實現(xiàn) & 定義如下
/**
* 將具有簡單返回值的消息發(fā)送到類實例的父類免糕。
* @param super A pointer to an \c objc_super data structure. Pass values identifying the
* context the message was sent to, including the instance of the class that is to receive the
* message and the superclass at which to start searching for the method implementation.
* @param op A pointer of type SEL. Pass the selector of the method that will handle the message.
* @param ...
* A variable argument list containing the arguments to the method.
*
* @return The return value of the method identified by \e op.
*
* @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);
#endif
而objc_super
定義是:
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
/// 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 */
};
#endif
由上面印證了赢乓,對象方法能執(zhí)行父類的實現(xiàn) 。
但是其底層是怎么找的呢石窑?
基本邏輯是:OC 方法是通過消息中的sel(方法編號)牌芋,找到函數(shù)指針imp,再找到底層匯編執(zhí)行其內(nèi)容松逊。
消息接收流程:對象 ->isa -> 方法or類 -> cache_t -> methodlist
objc_msgSend 的匯編流程
objc_msgSend 底層是使用匯編構造的躺屁。
因為匯編特性:1)編譯速度快 ;2)參數(shù)的動態(tài)性经宏,易調(diào)整犀暑。
匯編小補
LSR:邏輯右移
add 加法
b.le :判斷上面cmp的值是小于等于執(zhí)行標號,否則直接往下走
b.eq 等于 執(zhí)行地址 否則往下
cmp a,b 比較a與b
mov a,b 把b的值送給a
ret 返回主程序
nop無作用,英文“no operation”的簡寫烁兰,意思是“do nothing”(機器碼90)
call 調(diào)用子程序
je 或jz 若相等則跳(機器碼74 或0F84)
jne或jnz 若不相等則跳(機器碼75或0F85)
jmp 無條件跳(機器碼EB)
jb 若小于則跳
ja 若大于則跳
jg 若大于則跳
jge 若大于等于則跳
jl 若小于則跳
jle 若小于等于則跳
ldr w10 ,[sp] w10 = sp棧內(nèi)存中的值
pop 出棧
push 壓棧
快速查找流程
781源碼中搜索objc_msgSend
,匯編代碼就要找.s
文件耐亏,找到objc-msg-arm64.s
,以下是主要的匯編代碼:
// 消息發(fā)送:objc_msgSend的匯編入口,獲取receiver的isa
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
// p0與空做對比沪斟,判斷receiver是否存在广辰,p0為objc_msgSend的第一個參數(shù)(消息接收者receiver,第二個參數(shù)是_cmd)
cmp p0, #0 // 判空檢查和標記指針檢查
// 是否支持taggedpointers對象
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged
#else
// p0 等于 0 時主之,直接返回 空
b.eq LReturnZero
#endif
// p0即receiver 肯定存在的流程
// 從x0寄存器指向的地址中取出 isa择吊,存入 p13寄存器
ldr p13, [x0]
// 在64位架構下通過 p16 = isa(p13) & ISA_MASK,拿出shiftcls信息槽奕,得到class信息
GetClassFromIsa_p16 p13
LGetIsaDone:
// 調(diào)用imp或objc_msgSend_uncached
//如果獲取到isa几睛,跳轉(zhuǎn)CacheLookup 執(zhí)行緩存查找流程(sel-imp的快速查找)
CacheLookup NORMAL, _objc_msgSend
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
//如果為nil,則返回空
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: // 不支持taggedpointer對象粤攒,或者為空所森,就返回空:returnZero
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
END_ENTRY _objc_msgSend
-
CacheLookup
看定義.macro CacheLookup
囱持,跳轉(zhuǎn)至LLookupStart$1
.macro CacheLookup
//重新啟動協(xié)議:
//一旦我們經(jīng)過LLookupStart $ 1標簽,我們可能已經(jīng)加載了
//無效的緩存指針或掩碼必峰。
//
//調(diào)用task_restartable_ranges_synchronize()時洪唐,
//(或當有信號到達我們時),直到我們經(jīng)過LLookupEnd$1吼蚁,
//然后我們的電腦將重置為LLookupRecover $ 1
//跳轉(zhuǎn)到具有以下內(nèi)容的cache-miss代碼路徑
// 要求:
//
// GETIMP:
//緩存未命中只是返回NULL(將x0設置為0)
//
// NORMAL和LOOKUP:
//-x0包含接收者
//-x1包含選擇器
//-x16包含isa
//-根據(jù)調(diào)用約定設置其他寄存器
LLookupStart$1:
// #define CACHE (2 * __SIZEOF_POINTER__),其中 __SIZEOF_POINTER__表示pointer的大小 问欠,即 2*8 = 16
// p1 = SEL, p16 = isa
// x16(即isa)中平移16字節(jié)得到cache肝匆,取出cache 存入p11寄存器; isa(8字節(jié)),superClass(8字節(jié))顺献,cache(mask高16位 + buckets低48位)
// p11 = mask|buckets
ldr p11, [x16, #CACHE]
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
// arm64--對應cache_t的HIGH_16位宏
and p10, p11, #0x0000ffffffffffff // p10 = buckets = cacahe & 0x0000ffffffffffff 把mask高16位抹零旗国,得到buckets 存入p10寄存器,去掉mask注整,留下buckets
// 把p11邏輯右移48位得到mask能曾,mask & p1,得到sel-imp的下標index(即搜索下標)
// 存入p12(cache insert寫入時的哈希下標計算是 通過 sel & mask,讀取時也需要通過這種方式)
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
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// PTRSHIFT: arm64-asm.h 中定義 1 << 3 ,二進制 1 左移3位肿轨,結果為8.
// buckets 是一個數(shù)組寿冕,要想獲得其中某個元素值,利用內(nèi)存偏移 椒袍,((_cmd & mask ) << (1 + PTRSHIFT)) = 2 << 4 = 2 ^4 = 16 驼唱。(_cmd & mask ) 為2,內(nèi)存偏移相當于找buckets的第三個元素驹暑。
//(_cmd & mask ) : 由于mask= ocuupi - 1= 4-1 = 3玫恳,在0,1,2,3 中去取,& 的結果就相當于在取余數(shù)优俘,0,1,2,3
add p12, p10, p12, LSL #(1+PTRSHIFT)
// 通過取出p12的bucket結構體得到 * bucket = {imp,sel},p9 存sel京办, p17 存 imp
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
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
// p12 = buckets + (mask << 1+PTRSHIFT)
// p11:cache 右移44位,將結果存入p12帆焕,p12 = 高16位mask + 一個buckets (2中第一個bucket == 最后一個bucket情況惭婿,在下面的情況中依然沒有則該循環(huán)結束)
#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
//當緩存損壞時,克隆掃描循環(huán)會丟失而不是掛起视搏。
//緩慢的路徑可能會檢測到任何損壞并在以后暫停审孽。
// 再查找一遍緩存()
// 拿到x12(即p12)bucket中的 imp-sel 分別存入 p17-p9
ldp p17, p9, [x12] // {imp, sel} = *bucket
// 比較 sel 與 p1(傳入的參數(shù)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數(shù)組第一個元素)-- 表示前面已經(jīng)沒有了,但是還是沒有找到
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f //如果等于筋遭,跳轉(zhuǎn)至第3步
// 從x12(即p12 buckets首地址)- 實際需要平移的內(nèi)存大小BUCKET_SIZE打颤,得到得到第二個bucket元素暴拄,imp-sel分別存入p17-p9,即向前查找
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
// 跳轉(zhuǎn)至第1步编饺,繼續(xù)對比 sel 與 cmd
b 1b // loop
LLookupEnd$1:
LLookupRecover$1:
3: // double wrap
// 跳轉(zhuǎn)至JumpMiss 因為是normal 乖篷,跳轉(zhuǎn)至__objc_msgSend_uncached
JumpMiss $0
.endmacro
//以下是最后跳轉(zhuǎn)的匯編函數(shù)
.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
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
//如果為GETIMP ,則跳轉(zhuǎn)至 LGetImpMiss
cbz p9, LGetImpMiss
.elseif $0 == NORMAL
// 如果為NORMAL 透且,則跳轉(zhuǎn)至 __objc_msgSend_uncached
cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
//如果為LOOKUP 撕蔼,則跳轉(zhuǎn)至 __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
下面是objc_msgSend的匯編流程圖
總結:Objective-C 方法的實現(xiàn)本質(zhì)上是一個 C 函數(shù),而方法的調(diào)用過程則涉及到消息的傳遞和轉(zhuǎn)發(fā)秽誊。
在 Objective-C 中鲸沮,每個方法都有一個方法選擇器(selector),它是一個指向方法名稱的指針锅论。調(diào)用方法時讼溺,實際上是向?qū)ο蟀l(fā)送一條消息,消息中包含方法選擇器和參數(shù)列表等信息最易。Objective-C 運行時系統(tǒng)會根據(jù)方法選擇器查找對應的方法實現(xiàn)怒坯,然后執(zhí)行該方法。方法實現(xiàn)是一個 C 函數(shù)藻懒,它接收兩個參數(shù)剔猿,分別是方法的接收者和方法選擇器。因此束析,可以說 Objective-C 方法的實現(xiàn)本質(zhì)上是一個 C 函數(shù)艳馒。
但是,方法調(diào)用過程并不僅僅涉及到方法實現(xiàn)的調(diào)用员寇,還包括消息的傳遞和轉(zhuǎn)發(fā)過程弄慰。如果對象無法響應某個消息,Objective-C 運行時系統(tǒng)會嘗試將消息轉(zhuǎn)發(fā)給其他對象蝶锋。這種消息傳遞和轉(zhuǎn)發(fā)的機制是 Objective-C 方法的重要特性之一陆爽,它允許在運行時動態(tài)地修改方法的實現(xiàn),以及在多態(tài)和繼承等方面提供靈活性和可擴展性扳缕。因此慌闭,可以說 Objective-C 方法的本質(zhì)不僅是一個 C 語言函數(shù),還涉及到消息傳遞和轉(zhuǎn)發(fā)的機制躯舔。