iOS源碼解析:runtime<一> isa,class底層結(jié)構(gòu)窺探
iOS方法調(diào)用的過程我們都很清楚,比如下面這個方法調(diào)用:
[person test];
這個方法調(diào)用過程是首先通過person對象的isa指針找到Person類的類對象,由于實(shí)例方法存儲在類對象中,所以我們就去Person類對象中查找這個test方法如果找到了那就拿來調(diào)用,如果沒有找到,那就通過Person類對象的superclass指針找到Person類的父類的類對象,去這里查找這個test,如果還沒找到則繼續(xù)沿著繼承鏈往上找,如果最終還是沒有找到就會報(bào)unrecognized selector sent to instance 0x60000001b830
這個經(jīng)典的錯誤。
這樣回答方法的調(diào)用過程也沒有問題,但是顯得淺顯了一些,還不足以應(yīng)付面試。下面我們就一起探討一下iOS中方法的調(diào)用過程。
首先把[person test]
轉(zhuǎn)化為c++的源碼:
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("test"));
化簡一下:
objc_msgSend(person, sel_registerName("test"));
sel_registerName()
我在上一篇文章已經(jīng)說過,它就是傳入方法名,返回SEL,sel_registerName("test")
就等價于@selector(test)
。這句代碼就是給消息接收者發(fā)送SEL消息,所以接下來的問題就變成了去探究objc_msgSend()這個函數(shù)的調(diào)用過程。
objc_msgSend()的執(zhí)行流程可以分為三個階段
- 消息發(fā)送
- 動態(tài)方法解析
- 消息轉(zhuǎn)發(fā)
下面通過源碼逐一分析。
首先我們在runtime的源碼中搜索objc_msgSend
,我們發(fā)現(xiàn)搜索的結(jié)非常多,那我們要找的是它的實(shí)現(xiàn),最終我們在objc-msg-arm64.s
這樣一個匯編文件中找到objc_msgSend()
的實(shí)現(xiàn)。runtime的源碼基本都是由c,c++,匯編語言組成,并且很多經(jīng)常使用的都是由匯編語言給出的。
一 消息發(fā)送
在objc-msg-arm64.s
中。第304-346行是objc_msgSend()
的實(shí)現(xiàn)。
//從這里開始
304 ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
MESSENGER_START
//x0寄存器,消息接收者
308 cmp x0, #0 // nil check and tagged pointer check
309 b.le LNilOrTagged // b是跳轉(zhuǎn),le是小于等于,也就是x0小于等于0時,跳轉(zhuǎn)到LNilOrTagged,x0是objc_msgSend()傳入的第一個參數(shù),也就是消息接收者
ldr x13, [x0] // x13 = isa
and x16, x13, #ISA_MASK // x16 = class
LGetIsaDone:
313 CacheLookup NORMAL // 緩存查找
315 LNilOrTagged:
316 b.eq LReturnZero // 如果消息接收者為空,直接退出這個函數(shù)
// tagged
mov x10, #0xf000000000000000
cmp x0, x10
b.hs LExtTag
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]
b LGetIsaDone
LExtTag:
// 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
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
MESSENGER_END_NIL
ret
346 END_ENTRY _objc_msgSend
//結(jié)束
1.首先從308行開始,
cmp x0, #0
這里x0是寄存器,里面是消息接收者。b.le LNilOrTagged
,b是跳轉(zhuǎn)的意思,le是如果x0小于等于0,總體意思是若x0小于等于0,則跳轉(zhuǎn)到LNilOrTagged
。這里意思就是如果消息接收者是nil,則跳轉(zhuǎn)到LNilOrTagged
,我們看315行的LNilOrTagged
,執(zhí)行b.eq LReturnZero
就是直接退出程序。-
2.判斷完了消息接收者是否為nil之后,匯編代碼繼續(xù)執(zhí)行,到313行
CacheLookup NORMAL
,通過字面意思可以知道這是從緩存中查找方法的實(shí)現(xiàn),我們復(fù)制一下CacheLookup
然后去本文件中搜索一下:66394697-9FCF-4D47-AE77-96BCB4A9D558.png -
3.在緩存中找到了方法那就直接調(diào)用,這沒什么好說的,下面看一下從緩存中沒有找到方法怎么辦。沒有找到方法則會執(zhí)行
CheckMiss
,我們搜索一下它的實(shí)現(xiàn)。9FAC4F96-8798-4867-BC3E-EFC2ABB94AB1.png
再搜索一下__objc_msgSend_uncached
:
通過MethodTableLookup
這個字面名稱我們就大概知道這是從方法列表中去查找方法。我們再查看一下它的結(jié)構(gòu):
然后我們在本文件中搜索__class_lookupMethodAndLoadCache3
發(fā)現(xiàn)沒有它的定義,然后我們再在整個文件中搜索,發(fā)現(xiàn)還是沒有,這個時候我們?nèi)サ糸_頭的一個下劃線再搜索,發(fā)現(xiàn)有了結(jié)果,這是因?yàn)閰R編的函數(shù)比c++的多一個下劃線。
- 4.我們在
objc-runtime-new.mm
這個文件中找到了_class_lookupMethodAndLoadCache3
的實(shí)現(xiàn):A2A6063B-72D4-46CD-B491-CD08D3B44D02.png
主要就是實(shí)現(xiàn)了lookUpImpOrForward()
這個方法,然后我們再查找一下這個方法:
- 5.我們具體看一下是怎么從類對象中查找方法的,這個主要是在
getMethodNoSuper_nolock()
這個方法。9E58326A-4974-4949-8BE7-99444E5004B5.png
總結(jié)一下消息發(fā)送的過程就是下圖:
5EE45D9F-8DA7-400D-A3C7-FAE7E9F212F2.png
二 動態(tài)方法解析
在自己的類對象的緩存和方法列表中都沒有找到方法,并且在父類的類對象的緩存和方法列表中都沒有找到方法時,這時候就會啟動動態(tài)方法解析。
我們再找到lookUpImpOrForward
這個方法。在這個方法中前半部分是在自己的類對象以及父類對象中查找方法,后半部分就是處理在自己的類對象和父類對象中都找到不這個方法:
然后我們查看一下
_class_resolveMethod()
的實(shí)現(xiàn):其實(shí)實(shí)現(xiàn)很簡單,就是判斷是類對象還是元類對象,如果是類對象則說明調(diào)用的實(shí)例方法,則調(diào)用類的
resolveInstanceMethod:
方法,如果是元類對象,則說明是調(diào)用的類方法,則調(diào)用類的resolveClassMethod:
方法。
那下面就用實(shí)例來演示一下動態(tài)方法解析的過程。
首先在main.m文件中創(chuàng)建person對象并調(diào)用test方法:
Person *person = [[Person alloc] init];
[person test];
雖然在Person.h文件中聲明了test方法,但是在Person.m文件中并沒有實(shí)現(xiàn)test.m文件。所以運(yùn)行代碼的話應(yīng)該會崩潰,我們運(yùn)行代碼:
果然崩潰了,并且打印了經(jīng)典錯誤:unrecognized selector sent to instance 0x60400000e3e0
。
程序崩潰很容易理解,因?yàn)樵诘谝徊讲檎曳椒ㄖ校谧约旱念悓ο笠约案割惖念悓ο笾卸紱]有找到這個方法,所以轉(zhuǎn)向動態(tài)方法解析,動態(tài)方法解析我們什么也沒做,所以會轉(zhuǎn)向消息轉(zhuǎn)發(fā),消息轉(zhuǎn)發(fā)我們也什么都沒做,所以最后產(chǎn)生崩潰。接下來我們實(shí)現(xiàn)一下動態(tài)方法解析。
動態(tài)方法解析是當(dāng)?shù)谝徊街蟹椒ú檎沂r會進(jìn)行的,當(dāng)調(diào)用的是對象方法時,動態(tài)方法解析是在
resolveInstanceMethod:
方法中實(shí)現(xiàn)的,當(dāng)調(diào)用的是類方法時,動態(tài)方法解析是在resolveClassMethod:
方法中實(shí)現(xiàn)的。利用動態(tài)方法解析和runtime,我們可以給一個沒有實(shí)現(xiàn)的方法添加方法實(shí)現(xiàn)。
與動態(tài)添加方法實(shí)現(xiàn)相關(guān)的runtime的API是
/**
* Adds a new method to a class with a given name and implementation.
*/
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types)
我們看注釋就是可以知道,這個方法是給一個給定的方法名也就是SEL添加方法的實(shí)現(xiàn)。
@cls : 給哪個類對象添加方法
@name : SEL類型的,給哪個方法名添加方法實(shí)現(xiàn)
@imp : IMP類型的,要把哪個方法實(shí)現(xiàn)添加給給定的方法名
@types :在講method_t的結(jié)構(gòu)時講過這個,就是表示返回值和參數(shù)類型的字符串,比如"v16@0:8"
我現(xiàn)在在Person.m文件中實(shí)現(xiàn)了test2方法:
- (void)test2{
NSLog(@"測試動態(tài)方法解析");
}
那我想要把這個方法的方法實(shí)現(xiàn)添加到Person類中,我就需要調(diào)用runtime的class_addMethod
這個API,這些參數(shù)中,cls可以傳self,name可以傳@selector(test),types可以傳"v16@0:8"
,最難的就是imp應(yīng)該傳什么。我們需要獲取test2函數(shù)的imp,這個應(yīng)該怎么獲取呢?
runtime中也有相對應(yīng)的API:
Method _Nullable
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
這個API返回的就是一個代表方法的Method。我們可以通過
IMP _Nonnull
method_getImplementation(Method _Nonnull m)
這個runtime的API通過Method結(jié)構(gòu)獲取方法的IMP,所以最終的代碼就是這樣:
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(test)) {
Method method = class_getInstanceMethod(self, @selector(test2));
class_addMethod(self, sel, method_getImplementation(method), "v16@0:8");
return YES;
}
return [super resolveInstanceMethod:sel];
}
- (void)test2{
NSLog(@"測試動態(tài)方法解析");
}
這樣當(dāng)?shù)谝徊椒椒ú檎艺也坏椒椒〞r,就會進(jìn)行第二步動態(tài)方法解析,由于調(diào)用的是對象方法,所以會執(zhí)行resolveInstanceMethod:
方法中的代碼,在這個方法中,使用runtime的API,給類對象中動態(tài)添加了test方法的實(shí)現(xiàn),這個實(shí)現(xiàn)是test2方法的實(shí)現(xiàn)。當(dāng)動態(tài)方法解析結(jié)束后還會返回去進(jìn)行方法查找,這次能夠查找到test方法及其實(shí)現(xiàn)了,也就能夠成功調(diào)用test方法了。
用一個圖總結(jié)動態(tài)方法解析的整個過程:
三 消息轉(zhuǎn)發(fā)
我們再看一下動態(tài)方法解析的過程:
進(jìn)行動態(tài)方法解析結(jié)束之后,會從頭開始再進(jìn)行消息發(fā)送這一步,如果在動態(tài)方法解析的時候有動態(tài)添加方法實(shí)現(xiàn),那么就能找到方法實(shí)現(xiàn)并返回方法實(shí)現(xiàn),不再執(zhí)行下面的代碼;如果在動態(tài)方法解析的時候沒有做什么事,那么就不能找到方法實(shí)現(xiàn),這時候由于
triedResolver
標(biāo)志位已經(jīng)置為YES,也就不會再進(jìn)入動態(tài)消息解析,而是會進(jìn)入消息轉(zhuǎn)發(fā)。
消息轉(zhuǎn)發(fā)通俗地講就是本類沒有能力去處理這個消息,那么就轉(zhuǎn)發(fā)給其他的類,讓其他類去處理。
接下來我們看一下進(jìn)行消息轉(zhuǎn)發(fā)的函數(shù)_objc_msgForward_impcache
的具體實(shí)現(xiàn),去文件中搜索,在匯編中找到了它的實(shí)現(xiàn):
然后我們?nèi)ゲ檎?code>__objc_forward_handler的實(shí)現(xiàn),但是找到了半天好像并不能找到其實(shí)現(xiàn),這個函數(shù)有可能并不是開源的,那我們這條路就行不通了。
網(wǎng)上有人寫了__forwarding__
這個函數(shù)的實(shí)現(xiàn)的偽代碼,我們可以拿來學(xué)習(xí)一下。為什么要學(xué)習(xí)這個函數(shù)呢?因?yàn)楫?dāng)[person test]
崩潰時調(diào)用棧是這樣的:
我們來看一下
__forwarding__
函數(shù)的第一步:下面用例子說明一下:
在Person.h中聲明了test方法,但是Person.m中并沒有去實(shí)現(xiàn)。那這個時候用Person對象去調(diào)用test方法就會產(chǎn)生崩潰。這個時候在Student.m文件中實(shí)現(xiàn)一個test方法,并且在Person.m中通過
forwardingTargetForSelector:
方法把消息轉(zhuǎn)發(fā)對象設(shè)置為Student對象:
// Student.m
- (void)test{
NSLog(@"轉(zhuǎn)發(fā)給student處理");
}
// Person.m
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(test)) {
return [[Student alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
這樣的話person對象就成功把@selector(test)
這個消息轉(zhuǎn)發(fā)給student對象讓它去處理,自己不管了。相當(dāng)于是調(diào)用了objc_msgSend(student, @selector(test))。我們可以從另外一個角度去驗(yàn)證這個問題,使一個沒有實(shí)現(xiàn)test方法的類的對象成為消息轉(zhuǎn)發(fā)對象:
// Person.m
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(test)) {
return [[NSObject alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
這個NSObject類是沒有實(shí)現(xiàn)test方法的,我們看一下運(yùn)行結(jié)果:
-[NSObject test]: unrecognized selector sent to instance 0x600000013810
我們看到,現(xiàn)在直接是在NSObject這個類中沒有找到test方法了。
現(xiàn)在有一個問題了,如果- (id)forwardingTargetForSelector:(SEL)aSelector
返回為空或者壓根就沒有實(shí)現(xiàn),程序又會如何繼續(xù)呢?我們還是從偽碼中查找答案:
下面用代碼實(shí)例來講解:
Person.h中有- (void)testAge:(int)age;
但是在Person.m中并沒有實(shí)現(xiàn)。
現(xiàn)在在main.m中去調(diào)用這個方法:
[person testAge:10];
這個時候會產(chǎn)生崩潰,因?yàn)樵谙l(fā)送階段沒有找到該方法的實(shí)現(xiàn),而動態(tài)方法解析和消息轉(zhuǎn)發(fā)階段則什么都沒有做,所以就崩潰了。
第一階段消息發(fā)送結(jié)束后會進(jìn)行第二階段動態(tài)消息解析,動態(tài)消息解析依賴于+ (BOOL)resolveInstanceMethod:(SEL)sel這個函數(shù),當(dāng)這個函數(shù)也沒有動態(tài)添加方法實(shí)現(xiàn)時,就會進(jìn)入第三階段-消息轉(zhuǎn)發(fā)。
消息轉(zhuǎn)發(fā)首先依賴于- (id)forwardingTargetForSelector:(SEL)aSelector
這個方法,若是這個方法直接返回了一個消息轉(zhuǎn)發(fā)對象,則會通過objc_msgSend()把這個消息轉(zhuǎn)發(fā)給消息轉(zhuǎn)發(fā)對象了。若是這個方法沒有實(shí)現(xiàn)或者實(shí)現(xiàn)了但是返回值為空,則會跑去執(zhí)行后面的- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
這個函數(shù)以及- (void)forwardInvocation:(NSInvocation *)anInvocation
這個函數(shù)。
現(xiàn)在我們在第二階段動態(tài)方法解析階段沒有做任何處理,在- (id)forwardingTargetForSelector:(SEL)aSelector
這個函數(shù)中也不做處理。那么代碼就會執(zhí)行到- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
這個函數(shù),在這個函數(shù)中我們要返回一個方法簽名:
Person.m
//方法簽名:返回值類型,參數(shù)類型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if(aSelector == @selector(testAge:)){
return [NSMethodSignature signatureWithObjCTypes:"v20@0:8i16"];
}
return [super methodSignatureForSelector:aSelector];
}
我們想一下,要完整的表征person對象調(diào)用- (void)testAge:(int)age
這個過程,我們就需要知道方法調(diào)用者,方法名,方法參數(shù)。而在Person.m中我們肯定知道方法調(diào)用者是person對象,方法名也知道是"testAge:",那么現(xiàn)在不知道的就是方法參數(shù)了,那么這個方法簽名就是表征這個方法參數(shù)的,包括返回值和參數(shù),這樣方法調(diào)用者,方法名和方法參數(shù)就都知道了。
然后看- (void)forwardInvocation:(NSInvocation *)anInvocation
的實(shí)現(xiàn):
//NSInvocation封裝了一個方法調(diào)用,包括:方法調(diào)用者,方法名,方法參數(shù)
@ anInvocation.target 方法調(diào)用者
@ anInvocation.selector 方法名
@ [anInvocation getArgument:NULL atIndex:0];
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%@ %@", anInvocation.target, NSStringFromSelector(anInvocation.selector));
int age;
[anInvocation getArgument:&age atIndex:2];
NSLog(@"%d", age);
//這行代碼是把方法的調(diào)用者改變?yōu)閟tudent對象
[anInvocation invokeWithTarget:[[Student alloc] init]];
}
在這個方法中有一個NSInvocation類型的anInvocation參數(shù),這個參數(shù)就是表征一個方法調(diào)用的,我們可以通過這個參數(shù)獲取person對象調(diào)用- (void)testAge:(int)age
方法這個過程中的方法調(diào)用者,方法名,方法參數(shù)。然后我們可以通過修改方法調(diào)用者來達(dá)到消息轉(zhuǎn)發(fā)的效果,這里是把方法調(diào)用者修改為了student對象。這樣就完成了成功轉(zhuǎn)發(fā)消息給student對象。
那么我們思考一個問題,在第三階段消息轉(zhuǎn)發(fā)階段為什么會有三個函數(shù)這個復(fù)雜?如果我們想要轉(zhuǎn)發(fā)消息,那么直接在- (id)forwardingTargetForSelector:(SEL)aSelector
去返回一個消息轉(zhuǎn)發(fā)對象就可以了呀。設(shè)計(jì)三個函數(shù)的好處就是,當(dāng)來到- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
這個方法時,如果這個方法返回為空,那么走到這里直接結(jié)束方法調(diào)用,產(chǎn)生崩潰,而如果返回不為空,那么就會繼續(xù)去調(diào)用- (void)forwardInvocation:(NSInvocation *)anInvocation
這個方法,那么來到這個里面,我們就可以為所欲為,即使我們什么也不做,運(yùn)行程序也不會崩潰了,我們可以在這個方法里面為方法指定新的調(diào)用者,也即是進(jìn)行消息轉(zhuǎn)發(fā),也可以做一些其他的操作,都可以,這就是這樣設(shè)計(jì)的一個好處,我們可以在這個方法里面做一切我們想做的。
總結(jié)一下消息準(zhǔn)發(fā)的過程就是: