該文章屬于劉小壯原創(chuàng),轉(zhuǎn)載請注明:劉小壯
方法調(diào)用
在OC
中方法調(diào)用是通過Runtime
實現(xiàn)的彼水,Runtime
進(jìn)行方法調(diào)用本質(zhì)上是發(fā)送消息条霜,通過objc_msgSend()
函數(shù)進(jìn)行消息發(fā)送间雀。
例如下面的OC
代碼會被轉(zhuǎn)換為Runtime
代碼悔详。
原方法:[object testMethod]
轉(zhuǎn)換后的調(diào)用:objc_msgSend(object, @selector(testMethod));
發(fā)送消息的第二個參數(shù)是一個SEL
類型的參數(shù),在項目里經(jīng)常會出現(xiàn)惹挟,不同的類定義了相同的方法茄螃,這樣就會有相同的SEL
。那么問題就來了连锯,也是很多人博客里都問過的一個問題归苍,不同類的SEL
是同一個嗎?
然而运怖,事實是通過我們的驗證拼弃,創(chuàng)建兩個不同的類,并定義兩個相同的方法摇展,通過@selector()
獲取SEL
并打印吻氧。我們發(fā)現(xiàn)SEL
都是同一個對象,地址都是相同的咏连。由此證明盯孙,不同類的相同SEL
是同一個對象。
@interface TestObject : NSObject
- (void)testMethod;
@end
@interface TestObject2 : NSObject
- (void)testMethod;
@end
// TestObject2實現(xiàn)文件也一樣
@implementation TestObject
- (void)testMethod {
NSLog(@"TestObject testMethod %p", @selector(testMethod));
}
@end
// 結(jié)果:
TestObject testMethod 0x100000f81
TestObject2 testMethod 0x100000f81
在Runtime
中維護(hù)了一個SEL
的表祟滴,這個表存儲SEL
不按照類來存儲振惰,只要相同的SEL
就會被看做一個,并存儲到表中垄懂。在項目加載時骑晶,會將所有方法都加載到這個表中,而動態(tài)生成的方法也會被加載到表中草慧。
隱藏參數(shù)
我們在方法內(nèi)部可以通過self
獲取到當(dāng)前對象桶蛔,但是self
又是從哪來的呢?
方法實現(xiàn)的本質(zhì)也是C
函數(shù)冠蒋,C
函數(shù)除了方法傳入的參數(shù)外羽圃,還會有兩個默認(rèn)參數(shù),這兩個參數(shù)在通過objc_msgSend()
調(diào)用時也會傳入抖剿。這兩個參數(shù)在Runtime
中并沒有聲明朽寞,而是在編譯時自動生成的。
從objc_msgSend
的聲明中可以看出這兩個隱藏參數(shù)的存在斩郎。
objc_msgSend(void /* id self, SEL op, ... */ )
-
self
脑融,調(diào)用當(dāng)前方法的對象。 -
_cmd
缩宜,當(dāng)前被調(diào)用方法的SEL
肘迎。
雖然這兩個參數(shù)在調(diào)用和實現(xiàn)方法中都沒有明確聲明甥温,但是我們?nèi)匀豢梢允褂盟m憫?yīng)對象就是self
妓布,被調(diào)用方法的selector
是_cmd
姻蚓。
- (void)method {
id target = getTheReceiver();
SEL method = getTheMethod();
if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}
函數(shù)調(diào)用
一個對象被創(chuàng)建后,自身的類及其父類一直到NSObject
類的部分匣沼,都會包含在對象的內(nèi)存中狰挡,例如其父類的實例變量。當(dāng)通過[super class]
的方式調(diào)用其父類的方法時释涛,會創(chuàng)建一個結(jié)構(gòu)體加叁。
struct objc_super { id receiver; Class class; };
對super
的調(diào)用會被轉(zhuǎn)化為objc_msgSendSuper()
的調(diào)用,并在其內(nèi)部調(diào)用objc_msgSend()
函數(shù)唇撬。有一點需要注意它匕,盡管是通過[super class]
的方式調(diào)用的,但傳入的receiver
對象仍然是self
窖认,返回結(jié)果也是self
的class
豫柬。由此可知,當(dāng)前對象無論調(diào)用任何方法耀态,receiver
都是當(dāng)前對象轮傍。
objc_msgSend(objc_super->receiver, @selector(class))
在objc_msg.s
中,存在多個版本的objc_msgSend
函數(shù)首装。內(nèi)部實現(xiàn)邏輯大體一致,都是通過匯編實現(xiàn)的杭跪,只是根據(jù)不同的情況有不同的調(diào)用仙逻。
objc_msgSend
objc_msgSend_fpret
objc_msgSend_fp2ret
objc_msgSend_stret
objc_msgSendSuper
objc_msgSendSuper_stret
objc_msgSendSuper2
objc_msgSendSuper2_stret
在上面源碼中,帶有super
的會在外界傳入一個objc_super
的結(jié)構(gòu)體對象涧尿。stret
表示返回的是struct
類型系奉,super2
是objc_msgSendSuper()
的一種實現(xiàn)方式,不對外暴露姑廉。
struct objc_super {
id receiver;
Class class;
};
fp
則表示返回一個long double
的浮點型缺亮,而fp2
則返回一個complex long double
的復(fù)雜浮點型,其他float
桥言、double
的普通浮點型都用objc_msgSend
萌踱。除了上面這些情況外,其他都通過objc_msgSend()
調(diào)用号阿。
消息發(fā)送流程
當(dāng)一個對象被創(chuàng)建時并鸵,系統(tǒng)會為其分配內(nèi)存,并完成默認(rèn)的初始化工作扔涧,例如對實例變量進(jìn)行初始化园担。對象第一個變量是指向其類對象的指針-isa
届谈,isa
指針可以訪問其類對象,并且通過其類對象擁有訪問其所有繼承者鏈中的類弯汰。
isa
指針不是語言的一部分艰山,主要為Runtime
機(jī)制提供服務(wù)。
當(dāng)對象接收到一條消息時咏闪,消息函數(shù)隨著對象isa
指針到類的結(jié)構(gòu)體中曙搬,在method list
中查找方法selector
。如果在本類中找不到對應(yīng)的selector
汤踏,則objc_msgSend
會向其父類的method list
中查找selector
织鲸,如果還不能找到則沿著繼承關(guān)系一直向上查找,直到找到NSObject
類溪胶。
Runtime
在selector
查找的過程做了優(yōu)化搂擦,為類的結(jié)構(gòu)體中增加了cache
字段,每個類都有獨立的cache
哗脖,在一個selector
被調(diào)用后就會加入到cache
中瀑踢。在每次搜索方法列表之前,都會先檢查cache
中有沒有才避,如果沒有才調(diào)用方法列表橱夭,這樣會提高方法的查找效率。
如果通過OC
代碼的調(diào)用都會走消息發(fā)送的階段桑逝,如果不想要消息發(fā)送的過程棘劣,可以獲取到方法的函數(shù)指針直接調(diào)用。通過NSObject
的methodForSelector:
方法可以獲取到函數(shù)指針楞遏,獲取到指針后需要對指針進(jìn)行類型轉(zhuǎn)換茬暇,轉(zhuǎn)換為和調(diào)用函數(shù)相符的函數(shù)指針,然后發(fā)起調(diào)用即可寡喝。
void (*setter)(id, SEL, BOOL);
int i;
setter = (void (*)(id, SEL, BOOL))[target
methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
setter(targetList[i], @selector(setFilled:), YES);
實現(xiàn)原理
在Runtime
中糙俗,objc_msgSend
函數(shù)也是開源的,但其是通過匯編代碼實現(xiàn)的预鬓,arm64
架構(gòu)代碼可以在objc-msg-arm64.s
中找到巧骚。在Runtime
中,很多執(zhí)行頻率比較高的函數(shù)格二,都是用匯編寫的劈彪。
objc_msgSend
并不是完全開源的,在_class_lookupMethodAndLoadCache3
函數(shù)中已經(jīng)獲取到Class
參數(shù)了蟋定。所以在下面中有一個肯定是對象中獲取isa_t
的過程粉臊,從方法命名和注釋來看,應(yīng)該是GetIsaFast
匯編命令驶兜。如果這樣的話扼仲,就可以從消息發(fā)送到調(diào)用流程銜接起來了远寸。
ENTRY _objc_msgSend
MESSENGER_START
NilTest NORMAL
GetIsaFast NORMAL // r11 = self->isa
CacheLookup NORMAL // calls IMP on success
NilTestSupport NORMAL
GetIsaSupport NORMAL
// cache miss: go search the method lists
LCacheMiss:
// isa still in r11
MethodTableLookup %a1, %a2 // r11 = IMP
cmp %r11, %r11 // set eq (nonstret) for forwarding
jmp *%r11 // goto *imp
END_ENTRY _objc_msgSend
-
MESSENGER_START
:消息開始執(zhí)行。 -
NilTest
:判斷接收消息的對象是否為nil
屠凶,如果為nil
則直接返回驰后,這就是對nil
發(fā)送消息無效的原因。 -
GetIsaFast
:快速獲取到isa
指向的對象矗愧,是一個類對象或元類對象灶芝。 -
CacheLookup
:從ache list
中獲取緩存selector
,如果查到則調(diào)用其對應(yīng)的IMP
唉韭。 -
LCacheMiss
:緩存沒有命中夜涕,則執(zhí)行此條匯編下面的方法。 -
MethodTableLookup
:如果緩存中沒有找到属愤,則從method list
中查找女器。
cache_t
如果每次進(jìn)行方法調(diào)用時,都按照對象模型來進(jìn)行方法列表的查找住诸,這樣是很消耗時間的驾胆。Runtime
為了優(yōu)化調(diào)用時間,在objc_class
中添加了一個cache_t
類型的cache
字段贱呐,通過緩存來優(yōu)化調(diào)用時間丧诺。
在執(zhí)行objc_msgSend
函數(shù)的消息發(fā)送過程中,同一個方法第一次調(diào)用是沒有緩存的奄薇,但調(diào)用之后就會存在緩存驳阎,之后的調(diào)用就直接調(diào)用緩存。所以方法的調(diào)用馁蒂,可以分為有緩存和無緩存兩種搞隐,這兩種情況下的調(diào)用堆棧是不同的。
首先是從緩存中查找IMP
远搪,但是由于cache3
調(diào)用lookUpImpOrForward
函數(shù)時,已經(jīng)查找過cache
了逢捺,所以傳入的是NO
谁鳍,不進(jìn)入查找cahce
的代碼塊中。
struct cache_t {
// 存儲被緩存方法的哈希表
struct bucket_t *_buckets;
// 占用的總大小
mask_t _mask;
// 已使用大小
mask_t _occupied;
}
struct bucket_t {
cache_key_t _key;
IMP _imp;
};
當(dāng)給一個對象發(fā)送消息時劫瞳,Runtime
會沿著isa
找到對應(yīng)的類對象倘潜,但并不會立刻查找method_list
,而是先查找cache_list
志于,如果有緩存的話優(yōu)先查找緩存涮因,沒有再查找方法列表。
這是Runtime
對查找method
的優(yōu)化伺绽,理論上來說在cache
中的method
被訪問的頻率會更高养泡。cache_list
由cache_t
定義嗜湃,內(nèi)部有一個bucket_t
的數(shù)組,數(shù)組中保存IMP
和key
澜掩,通過key
找到對應(yīng)的IMP
并調(diào)用购披。具體源碼可以查看objc-cache.mm
。
如果類對象沒有被初始化肩榕,并且lookUpImpOrForward
函數(shù)的initialize
參數(shù)為YES
刚陡,則表示需要對該類進(jìn)行創(chuàng)建。函數(shù)內(nèi)部主要是一些基礎(chǔ)的初始化操作株汉,而且會遞歸檢查父類筐乳,如果父類未初始化,則先初始化其父類對象乔妈。
STATIC_ENTRY _cache_getImp
mov r9, r0
CacheLookup NORMAL
// cache hit, IMP in r12
mov r0, r12
bx lr // return imp
CacheLookup2 GETIMP
// cache miss, return nil
mov r0, #0
bx lr
END_ENTRY _cache_getImp
下面會進(jìn)入cache_getImp
的代碼中蝙云,然而這個函數(shù)不是開源的,但是有一部分源碼可以看到褒翰,是通過匯編寫的贮懈。其內(nèi)部調(diào)用了CacheLookup
和CacheLookup2
兩個函數(shù),這兩個函數(shù)也都是匯編寫的优训。
經(jīng)過第一次調(diào)用后朵你,就會存在緩存。進(jìn)入objc_msgSend
后會調(diào)用CacheLookup
命令揣非,如果找到緩存則直接調(diào)用抡医。但是Runtime
并不是完全開源的,內(nèi)部很多實現(xiàn)我們依然看不到早敬,CacheLookup
命令內(nèi)部也一樣忌傻,只能看到調(diào)用完命令后就開始執(zhí)行我們的方法了。
CacheLookup NORMAL, CALL
源碼分析
在上面objc_msgSend
匯編實現(xiàn)中搞监,存在一個MethodTableLookup
的匯編調(diào)用水孩。在這條匯編調(diào)用中,調(diào)用了查找方法列表的C函數(shù)琐驴。下面是精簡版代碼俘种。
.macro MethodTableLookup
// 調(diào)用MethodTableLookup并在內(nèi)部執(zhí)行cache3函數(shù)(C函數(shù))
blx __class_lookupMethodAndLoadCache3
mov r12, r0 // r12 = IMP
.endmacro
在MethodTableLookup
中通過調(diào)用_class_lookupMethodAndLoadCache3
函數(shù),來查找方法列表绝淡。函數(shù)內(nèi)部是通過lookUpImpOrForward
函數(shù)實現(xiàn)的宙刘,在調(diào)用時cache
字段傳入NO
,表示不需要查找緩存了牢酵,因為在cache3
函數(shù)上面已經(jīng)通過匯編查找過了悬包。
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
// 通過cache3內(nèi)部調(diào)用lookUpImpOrForward函數(shù)
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
lookUpImpOrForward
函數(shù)是支持多線程的,所以內(nèi)部會有很多鎖操作馍乙。其內(nèi)部有一個rwlock_t
類型的runtimeLock
變量布近,有runtimeLock
控制讀寫鎖垫释。其內(nèi)部有很多邏輯代碼,這里把函數(shù)內(nèi)部實現(xiàn)做了精簡吊输,把核心代碼貼到下面饶号。
通過類對象的isRealized
函數(shù),判斷當(dāng)前類是否被實現(xiàn)季蚂,如果沒有被實現(xiàn)茫船,則通過realizeClass
函數(shù)實現(xiàn)該類。在realizeClass
函數(shù)中扭屁,會設(shè)置version
算谈、rw
、superClass
等一些信息料滥。
// 執(zhí)行查找imp和轉(zhuǎn)發(fā)的代碼
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// 如果cache是YES然眼,則從緩存中查找IMP。如果是從cache3函數(shù)進(jìn)來葵腹,則不會執(zhí)行cache_getImp()函數(shù)
if (cache) {
// 通過cache_getImp函數(shù)查找IMP高每,查找到則返回IMP并結(jié)束調(diào)用
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
runtimeLock.read();
// 判斷類是否已經(jīng)被創(chuàng)建,如果沒有被創(chuàng)建践宴,則將類實例化
if (!cls->isRealized()) {
// 對類進(jìn)行實例化操作
realizeClass(cls);
}
// 第一次調(diào)用當(dāng)前類的話鲸匿,執(zhí)行initialize的代碼
if (initialize && !cls->isInitialized()) {
// 對類進(jìn)行初始化,并開辟內(nèi)存空間
_class_initialize (_class_getNonMetaClass(cls, inst));
}
retry:
runtimeLock.assertReading();
// 嘗試獲取這個類的緩存
imp = cache_getImp(cls, sel);
if (imp) goto done;
{
// 如果沒有從cache中查找到阻肩,則從方法列表中獲取Method
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
// 如果獲取到對應(yīng)的Method带欢,則加入緩存并從Method獲取IMP
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
{
unsigned attempts = unreasonableClassCount();
// 循環(huán)獲取這個類的緩存IMP 或 方法列表的IMP
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
// 獲取父類緩存的IMP
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// 如果發(fā)現(xiàn)父類的方法,并且不再緩存中烤惊,在下面的函數(shù)中緩存方法
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
break;
}
}
// 在父類的方法列表中乔煞,獲取method_t對象。如果找到則緩存查找到的IMP
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
// 如果沒有找到柒室,則嘗試動態(tài)方法解析
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
runtimeLock.read();
triedResolver = YES;
goto retry;
}
// 如果沒有IMP被發(fā)現(xiàn)渡贾,并且動態(tài)方法解析也沒有處理,則進(jìn)入消息轉(zhuǎn)發(fā)階段
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlockRead();
return imp;
}
在方法第一次調(diào)用時雄右,可以通過cache_getImp
函數(shù)查找到緩存的IMP
剥啤。但如果是第一次調(diào)用,就查不到緩存的IMP
不脯,就會進(jìn)入到getMethodNoSuper_nolock
函數(shù)中執(zhí)行。下面是getMethod
函數(shù)的關(guān)鍵代碼刻诊。
getMethodNoSuper_nolock(Class cls, SEL sel) {
// 根據(jù)for循環(huán)防楷,從methodList列表中,從頭開始遍歷则涯,每次遍歷后向后移動一位地址复局。
for (auto mlists = cls->data()->methods.beginLists(),
end = cls->data()->methods.endLists();
mlists != end;
++mlists)
{
// 對sel參數(shù)和method_t做匹配冲簿,如果匹配上則返回。
method_t *m = search_method_list(*mlists, sel);
if (m) return m;
}
return nil;
}
當(dāng)調(diào)用一個對象的方法時亿昏,查找對象的方法峦剔,本質(zhì)上就是遍歷對象isa
所指向類的方法列表,并用調(diào)用方法的SEL
和遍歷的method_t
結(jié)構(gòu)體的name
字段做對比角钩,如果相等則將IMP
函數(shù)指針返回吝沫。
// 根據(jù)傳入的SEL,查找對應(yīng)的method_t結(jié)構(gòu)體
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)) {
return findMethodInSortedMethodList(sel, mlist);
} else {
for (auto& meth : *mlist) {
// SEL本質(zhì)上就是字符串递礼,查找的過程就是進(jìn)行字符串對比
if (meth.name == sel) return &meth;
}
}
if (mlist->isFixedUp()) {
for (auto& meth : *mlist) {
if (meth.name == sel) {
_objc_fatal("linear search worked when binary search did not");
}
}
}
return nil;
}
在getMethod
函數(shù)中惨险,主要是對Class
的methods
方法列表進(jìn)行查找和匹配。類的方法列表都在Class
的class_data_bits_t
中脊髓,通過data()
函數(shù)從bits
中獲取到class_rw_t
的結(jié)構(gòu)體辫愉,然后獲取到方法列表methods
,并遍歷方法列表将硝。
如果從當(dāng)前類中獲取不到對應(yīng)的IMP
恭朗,則進(jìn)入循環(huán)中。循環(huán)是從當(dāng)前類出發(fā)依疼,沿著繼承者鏈的關(guān)系痰腮,一直向根類查找,直到找到對應(yīng)的IMP
實現(xiàn)涛贯。
查找步驟和上面也一樣诽嘉,先通過cache_getImp
函數(shù)查找父類的緩存,如果找到則調(diào)用對應(yīng)的實現(xiàn)弟翘。如果沒找到緩存虫腋,表示第一次調(diào)用父類的方法,則調(diào)用getMethodNoSuper_nolock
函數(shù)從方法列表中獲取實現(xiàn)稀余。
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
}
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
如果沒有找到方法實現(xiàn)悦冀,則會進(jìn)入動態(tài)方法決議的步驟。在if
語句中會判斷傳入的resolver
參數(shù)是否為YES
睛琳,并且會判斷是否已經(jīng)有過動態(tài)決議盒蟆,因為下面是goto retry
,所以這段代碼可能會執(zhí)行多次师骗。
if (resolver && !triedResolver) {
_class_resolveMethod(cls, sel, inst);
triedResolver = YES;
goto retry;
}
如果滿足條件并且是第一次進(jìn)行動態(tài)方法決議历等,則進(jìn)入if
語句中調(diào)用_class_resolveMethod
函數(shù)。動態(tài)方法決議有兩種辟癌,_class_resolveClassMethod
類方法決議和_class_resolveInstanceMethod
實例方法決議寒屯。
BOOL (*msg)(Class, SEL, SEL) = (__typeof__(msg))objc_msgSend;
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
在這兩個動態(tài)方法決議的函數(shù)實現(xiàn)中,本質(zhì)上都是通過objc_msgSend
函數(shù),調(diào)用NSObject
中定義的resolveInstanceMethod:
和resolveClassMethod:
兩個方法寡夹。
可以在這兩個方法中動態(tài)添加方法处面,添加方法實現(xiàn)后,會在下面執(zhí)行goto retry
菩掏,然后再次進(jìn)入方法查找的過程中魂角。從triedResolver
參數(shù)可以看出,動態(tài)方法決議的機(jī)會只有一次智绸,如果這次再沒有找到野揪,則進(jìn)入消息轉(zhuǎn)發(fā)流程。
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
如果經(jīng)過上面這些步驟传于,還是沒有找到方法實現(xiàn)的話囱挑,則進(jìn)入動態(tài)消息轉(zhuǎn)發(fā)中。在動態(tài)消息轉(zhuǎn)發(fā)中沼溜,還可以對沒有實現(xiàn)的方法做一些彌補(bǔ)措施平挑。
下面是通過objc_msgSend
函數(shù)發(fā)送一條消息后,所經(jīng)過的調(diào)用堆棧系草,調(diào)用順序是從上到下的通熄。
CacheLookup NORMAL, CALL
__objc_msgSend_uncached
MethodTableLookup NORMAL
_class_lookupMethodAndLoadCache3
lookUpImpOrForward
調(diào)用總結(jié)
在調(diào)用objc_msgSend
函數(shù)后,會有一系列復(fù)雜的判斷邏輯找都,總結(jié)如下唇辨。
- 判斷當(dāng)前調(diào)用的
SEL
是否需要忽略,例如Mac OS
中的垃圾處理機(jī)制啟動的話能耻,則忽略retain
赏枚、release
等方法,并返回一個_objc_ignored_method
的IMP
晓猛,用來標(biāo)記忽略饿幅。 - 判斷接收消息的對象是否為
nil
,因為在OC中對nil
發(fā)消息是無效的戒职,這是因為在調(diào)用時就通過判斷條件過濾掉了栗恩。 - 從方法的緩存列表中查找,通過
cache_getImp
函數(shù)進(jìn)行查找洪燥,如果找到緩存則直接返回IMP
磕秤。 - 查找當(dāng)前類的
method list
,查找是否有對應(yīng)的SEL
捧韵,如果有則獲取到Method
對象市咆,并從Method
對象中獲取IMP
,并返回IMP
(這步查找結(jié)果是Method
對象)再来。 - 如果在當(dāng)前類中沒有找到
SEL
床绪,則去父類中查找。首先查找cache list
,如果緩存中沒有則查找method list
癞己,并以此類推直到查找到NSObject
為止。 - 如果在類的繼承體系中梭伐,始終沒有查找到對應(yīng)的
SEL
痹雅,則進(jìn)入動態(tài)方法解析中『叮可以在resolveInstanceMethod
和resolveClassMethod
兩個方法中動態(tài)添加實現(xiàn)绩社。 - 動態(tài)消息解析如果沒有做出響應(yīng),則進(jìn)入動態(tài)消息轉(zhuǎn)發(fā)階段赂苗。此時可以在動態(tài)消息轉(zhuǎn)發(fā)階段做一些處理愉耙,否則就會
Crash
。
整體分析
總體可以被分為三部分:
- 剛調(diào)用
objc_msgSend
函數(shù)后拌滋,內(nèi)部的一些處理邏輯朴沿。 - 復(fù)雜的查找
IMP
的過程,會涉及到cache list
和method list
等败砂。 - 進(jìn)入消息轉(zhuǎn)發(fā)階段赌渣。
在cache list
中找不到方法的情況下,會通過MethodTableLookup
宏定義從類的方法列表中昌犹,查找對應(yīng)的方法坚芜。在MethodTableLookup
中本質(zhì)上也是調(diào)用_class_lookupMethodAndLoadCache3
函數(shù),只是在傳參時cache
字段傳NO
斜姥,表示不從cache list
中查找鸿竖。
在cache3
函數(shù)中,是直接調(diào)用的lookUpImpOrForward
函數(shù)铸敏,這個函數(shù)內(nèi)部實現(xiàn)很復(fù)雜缚忧,可以看一下Runtime Analyze。在這個里面直接搜lookUpImpOrForward
函數(shù)名即可搞坝,可以詳細(xì)看一下內(nèi)部實現(xiàn)邏輯搔谴。
簡書由于排版的問題,閱讀體驗并不好桩撮,布局敦第、圖片顯示、代碼等很多問題店量。所以建議到我Github
上芜果,下載Runtime PDF
合集。把所有Runtime
文章總計九篇融师,都寫在這個PDF
中右钾,而且左側(cè)有目錄,方便閱讀。
下載地址:Runtime PDF
麻煩各位大佬點個贊舀射,謝謝窘茁!??