Objective-C 消息發(fā)送與轉(zhuǎn)發(fā)機(jī)制原理

消息發(fā)送和轉(zhuǎn)發(fā)流程可以概括為:消息發(fā)送(Messaging)是 Runtime 通過(guò) selector 快速查找 IMP 的過(guò)程蓝晒,有了函數(shù)指針就可以執(zhí)行對(duì)應(yīng)的方法實(shí)現(xiàn);消息轉(zhuǎn)發(fā)(Message Forwarding)是在查找 IMP 失敗后執(zhí)行一系列轉(zhuǎn)發(fā)流程的慢速通道,如果不作轉(zhuǎn)發(fā)處理,則會(huì)打日志和拋出異常坐求。

本文不講述開發(fā)者在消息發(fā)送和轉(zhuǎn)發(fā)流程中需要做的事,而是講述原理晌梨。能夠很好地閱讀本文的前提是你對(duì)Objective-C Runtime已經(jīng)有一定的了解桥嗤,關(guān)于什么是消息,Class 的結(jié)構(gòu)仔蝌,selector泛领、IMP、元類等概念將不再贅述敛惊。本文用到的源碼為 objc4-680 和 CF-1153.18渊鞋,逆向 CoreFoundation.framework 的系統(tǒng)版本為 macOS 10.11.5,匯編語(yǔ)言架構(gòu)為 x86_64。

八面玲瓏的 objc_msgSend

此函數(shù)是消息發(fā)送必經(jīng)之路锡宋,但只要一提objc_msgSend儡湾,都會(huì)說(shuō)它的偽代碼如下或類似的邏輯,反正就是獲取 IMP 并調(diào)用:

id objc_msgSend(id self, SEL _cmd, ...) {

Classclass=object_getClass(self);

IMP imp = class_getMethodImplementation(class,_cmd);

returnimp ? imp(self, _cmd, ...) :0;

}

源碼解析

為啥老用偽代碼执俩?因?yàn)閛bjc_msgSend是用匯編語(yǔ)言寫的徐钠,針對(duì)不同架構(gòu)有不同的實(shí)現(xiàn)。如下為x86_64架構(gòu)下的源碼奠滑,可以在objc-msg-x86_64.s文件中找到丹皱,關(guān)鍵代碼如下:

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

這里面包含一些有意義的宏:

NilTest宏,判斷被發(fā)送消息的對(duì)象是否為nil的宋税。如果為nil摊崭,那就直接返回nil。這就是為啥也可以對(duì)nil發(fā)消息杰赛。

GetIsaFast宏可以『快速地』獲取到對(duì)象的isa指針地址(放到r11寄存器呢簸,r10會(huì)被重寫;在 arm 架構(gòu)上是直接賦值到r9)

CacheLookup這個(gè)宏是在類的緩存中查找 selector 對(duì)應(yīng)的 IMP(放到r10)并執(zhí)行乏屯。如果緩存沒(méi)中根时,那就得到 Class 的方法表中查找了。

MethodTableLookup宏是重點(diǎn)辰晕,負(fù)責(zé)在緩存沒(méi)命中時(shí)在方法表中負(fù)責(zé)查找 IMP:

.macroMethodTableLookup

MESSENGER_END_SLOW

SaveRegisters

// _class_lookupMethodAndLoadCache3(receiver,selector,class)

movq$0, %a1

movq$1, %a2

movq%r11, %a3

call __class_lookupMethodAndLoadCache3

// IMP is now in %rax

movq%rax, %r11

RestoreRegisters

.endmacro

從上面的代碼可以看出方法查找 IMP 的工作交給了 OC 中的_class_lookupMethodAndLoadCache3函數(shù)蛤迎,并將 IMP 返回(從r11挪到rax)。最后在objc_msgSend中調(diào)用 IMP含友。

為什么使用匯編語(yǔ)言

其實(shí)在objc-msg-x86_64.s中包含了多個(gè)版本的objc_msgSend方法替裆,它們是根據(jù)返回值的類型和調(diào)用者的類型分別處理的:

objc_msgSendSuper:向父類發(fā)消息,返回值類型為id

objc_msgSend_fpret:返回值類型為 floating-point窘问,其中包含objc_msgSend_fp2ret入口處理返回值類型為long double的情況

objc_msgSend_stret:返回值為結(jié)構(gòu)體

objc_msgSendSuper_stret:向父類發(fā)消息辆童,返回值類型為結(jié)構(gòu)體

當(dāng)需要發(fā)送消息時(shí),編譯器會(huì)生成中間代碼惠赫,根據(jù)情況分別調(diào)用objc_msgSend,objc_msgSend_stret,objc_msgSendSuper, 或objc_msgSendSuper_stret其中之一把鉴。

這也是為什么objc_msgSend要用匯編語(yǔ)言而不是 OC、C 或 C++ 語(yǔ)言來(lái)實(shí)現(xiàn)儿咱,因?yàn)閱为?dú)一個(gè)方法定義滿足不了多種類型返回值庭砍,有的方法返回id,有的返回int混埠。除此之外還有其他原因逗威,比如其可變參數(shù)用匯編處理起來(lái)最方便,因?yàn)檎业?IMP 地址后參數(shù)都在棧上岔冀。要是用 C++ 傳遞可變參數(shù)那就悲劇了凯旭,prologue 機(jī)制會(huì)弄亂地址(比如 i386 上為了存儲(chǔ)ebp向后移位 4byte)概耻,最后還要用 epilogue 打掃戰(zhàn)場(chǎng)。此外還好考慮不同類型參數(shù)排列組合映射不同方法簽名(method signature)的問(wèn)題罐呼,那 switch 語(yǔ)句得老長(zhǎng)了鞠柄。。嫉柴。而且匯編程序執(zhí)行效率高厌杜,在 Objective-C Runtime 中調(diào)用頻率較高的函數(shù)好多都用匯編寫的。

使用 lookUpImpOrForward 快速查找 IMP

上一節(jié)中說(shuō)到的_class_lookupMethodAndLoadCache3函數(shù)其實(shí)只是簡(jiǎn)單的調(diào)用了lookUpImpOrForward函數(shù):

IMP _class_lookupMethodAndLoadCache3(idobj, SEL sel, Class cls)

{

returnlookUpImpOrForward(cls, sel, obj,

YES/*initialize*/,NO/*cache*/,YES/*resolver*/);

}

注意lookUpImpOrForward調(diào)用時(shí)使用緩存參數(shù)傳入為NO计螺,因?yàn)橹耙呀?jīng)嘗試過(guò)查找緩存了夯尽。IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver)實(shí)現(xiàn)了一套查找 IMP 的標(biāo)準(zhǔn)路徑,也就是在消息轉(zhuǎn)發(fā)(Forward)之前的邏輯登馒。

優(yōu)化緩存查找&類的初始化

先對(duì) debug 模式下的 assert 進(jìn)行 unlock:

runtimeLock.assertUnlocked();

runtimeLock本質(zhì)上是對(duì) Darwin 提供的線程讀寫鎖pthread_rwlock_t的一層封裝匙握,提供了一些便捷的方法。

lookUpImpOrForward接著做了如下兩件事:

如果使用緩存(cache參數(shù)為YES)陈轿,那就調(diào)用cache_getImp方法從緩存查找 IMP圈纺。cache_getImp是用匯編語(yǔ)言寫的,也可以在objc-msg-x86_64.s找到麦射,其依然用了之前說(shuō)過(guò)的CacheLookup宏蛾娶。因?yàn)開class_lookupMethodAndLoadCache3調(diào)用lookUpImpOrForward時(shí)cache參數(shù)為NO,這步直接略過(guò)潜秋。

如果是第一次用到這個(gè)類且initialize參數(shù)為YES(initialize && !cls->isInitialized())蛔琅,需要進(jìn)行初始化工作,也就是開辟一個(gè)用于讀寫數(shù)據(jù)的空間峻呛。先對(duì)runtimeLock寫操作加鎖揍愁,然后調(diào)用cls的initialize方法。如果sel == initialize也沒(méi)關(guān)系杀饵,雖然initialize還會(huì)被調(diào)用一次,但不會(huì)起作用啦谬擦,因?yàn)閏ls->isInitialized()已經(jīng)是YES啦切距。

繼續(xù)在類的繼承體系中查找

考慮到運(yùn)行時(shí)類中的方法可能會(huì)增加,需要先做讀操作加鎖惨远,使得方法查找和緩存填充成為原子操作谜悟。添加 category 會(huì)刷新緩存,之后如果舊數(shù)據(jù)又被重填到緩存中北秽,category 添加操作就會(huì)被忽略掉葡幸。

runtimeLock.read();

之后的邏輯整理如下:

如果 selector 是需要被忽略的垃圾回收用到的方法,則將 IMP 結(jié)果設(shè)為_objc_ignored_method贺氓,這是個(gè)匯編程序入口蔚叨,可以理解為一個(gè)標(biāo)記。對(duì)此種情況進(jìn)行緩存填充操作后,跳到第 7 步蔑水;否則執(zhí)行下一步邢锯。

查找當(dāng)前類中的緩存,跟之前一樣搀别,使用cache_getImp匯編程序入口丹擎。如果命中緩存獲取到了 IMP,則直接跳到第 7 步歇父;否則執(zhí)行下一步蒂培。

在當(dāng)前類中的方法列表(method list)中進(jìn)行查找,也就是根據(jù) selector 查找到 Method 后榜苫,獲取 Method 中的 IMP(也就是method_imp屬性)护戳,并填充到緩存中。查找過(guò)程比較復(fù)雜单刁,會(huì)針對(duì)已經(jīng)排序的列表使用二分法查找灸异,未排序的列表則是線性遍歷。如果成功查找到 Method 對(duì)象,就直接跳到第 7 步榜掌;否則執(zhí)行下一步窘游。

在繼承層級(jí)中遞歸向父類中查找,情況跟上一步類似么伯,也是先查找緩存,緩存沒(méi)中就查找方法列表卡儒。這里跟上一步不同的地方在于緩存策略田柔,有個(gè)_objc_msgForward_impcache匯編程序入口作為緩存中消息轉(zhuǎn)發(fā)的標(biāo)記。也就是說(shuō)如果在緩存中找到了 IMP骨望,但如果發(fā)現(xiàn)其內(nèi)容是_objc_msgForward_impcache硬爆,那就終止在類的繼承層級(jí)中遞歸查找,進(jìn)入下一步擎鸠;否則跳到第 7 步缀磕。

當(dāng)傳入lookUpImpOrForward的參數(shù)resolver為YES并且是第一次進(jìn)入第 5 步時(shí),時(shí)進(jìn)入動(dòng)態(tài)方法解析劣光;否則進(jìn)入下一步袜蚕。這步消息轉(zhuǎn)發(fā)前的最后一次機(jī)會(huì)。此時(shí)釋放讀入鎖(runtimeLock.unlockRead())绢涡,接著間接地發(fā)送+resolveInstanceMethod或+resolveClassMethod消息牲剃。這相當(dāng)于告訴程序員『趕緊用 Runtime 給類里這個(gè) selector 弄個(gè)對(duì)應(yīng)的 IMP 吧』,因?yàn)榇藭r(shí)鎖已經(jīng) unlock 了所以不會(huì)緩存結(jié)果雄可,甚至還需要軟性地處理緩存過(guò)期問(wèn)題可能帶來(lái)的錯(cuò)誤凿傅。這里的業(yè)務(wù)邏輯稍微復(fù)雜些缠犀,后面會(huì)總結(jié)。因?yàn)檫@些工作都是在非線程安全下進(jìn)行的狭归,完成后需要回到第 1 步再次查找 IMP夭坪。

此時(shí)不僅沒(méi)查找到 IMP,動(dòng)態(tài)方法解析也不奏效过椎,只能將_objc_msgForward_impcache當(dāng)做 IMP 并寫入緩存室梅。這也就是之前第 4 步中為何查找到_objc_msgForward_impcache就表明了要進(jìn)入消息轉(zhuǎn)發(fā)了。

讀操作解鎖疚宇,并將之前找到的 IMP 返回亡鼠。(無(wú)論是正經(jīng) IMP 還是不正經(jīng)的_objc_msgForward_impcache)這步還偏執(zhí)地做了一些腦洞略大的 assert,很有趣敷待。

對(duì)于第 5 步间涵,其實(shí)是直接調(diào)用_class_resolveMethod函數(shù),在這個(gè)函數(shù)中實(shí)現(xiàn)了復(fù)雜的方法解析邏輯榜揖。如果cls是元類則會(huì)發(fā)送+resolveClassMethod勾哩,然后根據(jù)lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)函數(shù)的結(jié)果來(lái)判斷是否發(fā)送+resolveInstanceMethod;如果不是元類举哟,則只需要發(fā)送+resolveInstanceMethod消息思劳。這里調(diào)用+resolveInstanceMethod或+resolveClassMethod時(shí)再次用到了objc_msgSend,而且第三個(gè)參數(shù)正是傳入lookUpImpOrForward的那個(gè)sel妨猩。在發(fā)送方法解析消息之后還會(huì)調(diào)用lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)來(lái)判斷是否已經(jīng)添加上sel對(duì)應(yīng)的 IMP 了潜叛,打印出結(jié)果。

最后lookUpImpOrForward方法也會(huì)把真正的 IMP 或者需要消息轉(zhuǎn)發(fā)的_objc_msgForward_impcache返回壶硅,并最終專遞到objc_msgSend中威兜。而_objc_msgForward_impcache會(huì)在轉(zhuǎn)化成_objc_msgForward或_objc_msgForward_stret。這個(gè)后面會(huì)講解原理庐椒。

回顧 objc_msgSend 偽代碼

回過(guò)頭來(lái)會(huì)發(fā)現(xiàn)objc_msgSend的偽代碼描述得很傳神啊椒舵,因?yàn)閏lass_getMethodImplementation的實(shí)現(xiàn)如下:

IMP class_getMethodImplementation(Class cls, SEL sel)

{

IMP imp;

if(!cls? ||? !sel)returnnil;

imp = lookUpImpOrNil(cls, sel,nil,YES/*initialize*/,YES/*cache*/,YES/*resolver*/);

// Translate forwarding function to C-callable external version

if(!imp) {

return_objc_msgForward;

}

returnimp;

}

lookUpImpOrNil函數(shù)獲取不到 IMP 時(shí)就返回_objc_msgForward,后面會(huì)講到它约谈。lookUpImpOrNil跟lookUpImpOrForward的功能很相似笔宿,只是將lookUpImpOrForward實(shí)現(xiàn)中的_objc_msgForward_impcache替換成了nil:

IMP lookUpImpOrNil(Class cls, SEL sel, idinst,

boolinitialize,boolcache,boolresolver)

{

IMP imp = lookUpImpOrForward(cls, sel,inst,initialize,cache,resolver);

if (imp == _objc_msgForward_impcache) return nil;

else return imp;

}

lookUpImpOrNil方法可以查找到 selector 對(duì)應(yīng)的 IMP 或是nil,所以如果不考慮返回值類型為結(jié)構(gòu)體的情況窗宇,用那幾行偽代碼來(lái)表示復(fù)雜的匯編實(shí)現(xiàn)還是挺恰當(dāng)?shù)摹?/p>

forwarding中路漫漫的消息轉(zhuǎn)發(fā)

objc_msgForward_impcache 的轉(zhuǎn)換

_objc_msgForward_impcache只是個(gè)內(nèi)部的函數(shù)指針,只存儲(chǔ)于上節(jié)提到的類的方法緩存中特纤,需要被轉(zhuǎn)化為_objc_msgForward和_objc_msgForward_stret才能被外部調(diào)用军俊。但在Mac OS XmacOS 10.6 及更早版本的 libobjc.A.dylib 中是不能直接調(diào)用的,況且我們根本不會(huì)直接用到它捧存。帶stret后綴的函數(shù)依舊是返回值為結(jié)構(gòu)體的版本粪躬。

上一節(jié)最后講到如果沒(méi)找到 IMP担败,就會(huì)將_objc_msgForward_impcache返回到objc_msgSend函數(shù),而正是因?yàn)樗怯脜R編語(yǔ)言寫的镰官,所以將內(nèi)部使用的_objc_msgForward_impcache轉(zhuǎn)化成外部可調(diào)用的_objc_msgForward或_objc_msgForward_stret也是由匯編代碼來(lái)完成提前。實(shí)現(xiàn)原理很簡(jiǎn)單,就是增加個(gè)靜態(tài)入口__objc_msgForward_impcache泳唠,然后根據(jù)此時(shí) CPU 的狀態(tài)寄存器的內(nèi)容來(lái)決定轉(zhuǎn)換成哪個(gè)狈网。如果是NE(Not Equal) 則轉(zhuǎn)換成_objc_msgForward_stret,反之是EQ(Equal) 則轉(zhuǎn)換成_objc_msgForward:

jne__objc_msgForward_stret

jmp__objc_msgForward

為何根據(jù)狀態(tài)寄存器的值來(lái)判斷轉(zhuǎn)換成哪個(gè)函數(shù)指針呢笨腥?回過(guò)頭來(lái)看看objc_msgSend中調(diào)用完MethodTableLookup之后干了什么:

MethodTableLookup %a1, %a2 // r11 = IMP

cmp%r11, %r11// set eq (nonstret)forforwarding

jmp*%r11//goto*imp

再看看返回值為結(jié)構(gòu)體的objc_msgSend_stret這里的邏輯:

MethodTableLookup%a2,%a3// r11 = IMP

test%r11,%r11// set ne (stret) for forward; r11!=0

jmp*%r11// goto *imp

稍微懂匯編的人一眼就看明白了拓哺,不懂的看注釋也懂了,我就不墨跡了〔蹦福現(xiàn)在總算是把消息轉(zhuǎn)發(fā)前的邏輯繞回來(lái)構(gòu)成閉環(huán)了士鸥。

上一節(jié)中提到class_getMethodImplementation函數(shù)的實(shí)現(xiàn),在查找不到 IMP 時(shí)返回_objc_msgForward谆级,而_objc_msgForward_stret正好對(duì)應(yīng)著class_getMethodImplementation_stret:

IMPclass_getMethodImplementation_stret(Class cls,SELsel)

{

IMP imp = class_getMethodImplementation(cls,sel);

// Translate forwardingfunctiontostruct-returningversion

if(imp == (IMP)&_objc_msgForward/* not _internal! */) {

return (IMP)&_objc_msgForward_stret;

}

return imp;

}

也就是說(shuō)_objc_msgForward*系列本質(zhì)都是函數(shù)指針烤礁,都用匯編語(yǔ)言實(shí)現(xiàn),都可以與 IMP 類型的值作比較肥照。_objc_msgForward和_objc_msgForward_stret聲明在message.h文件中脚仔。_objc_msgForward_impcache在早期版本的 Runtime 中叫做_objc_msgForward_internal。

objc_msgForward 也只是個(gè)入口

從匯編源碼可以很容易看出_objc_msgForward和_objc_msgForward_stret會(huì)分別調(diào)用_objc_forward_handler和_objc_forward_handler_stret

ENTRY__objc_msgForward

// Non-stretversion

movq__objc_forward_handler(%rip), %r11

jmp*%r11

END_ENTRY__objc_msgForward

ENTRY__objc_msgForward_stret

//Struct-returnversion

movq__objc_forward_stret_handler(%rip), %r11

jmp*%r11

END_ENTRY__objc_msgForward_stret

這兩個(gè) handler 函數(shù)的區(qū)別從字面上就能看出來(lái)建峭,不再贅述玻侥。

也就是說(shuō),消息轉(zhuǎn)發(fā)過(guò)程是現(xiàn)將_objc_msgForward_impcache強(qiáng)轉(zhuǎn)成_objc_msgForward或_objc_msgForward_stret亿蒸,再分別調(diào)用_objc_forward_handler或_objc_forward_handler_stret凑兰。

objc_setForwardHandler 設(shè)置了消息轉(zhuǎn)發(fā)的回調(diào)

在 Objective-C 2.0 之前,默認(rèn)的_objc_forward_handler或_objc_forward_handler_stret都是nil边锁,而新版本的默認(rèn)實(shí)現(xiàn)是這樣的:

// Default forward handler halts the process.

__attribute__((noreturn))void

objc_defaultForwardHandler(idself, SEL sel)

{

_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "

"(no message forward handler is installed)",

class_isMetaClass(object_getClass(self)) ?'+':'-',

object_getClassName(self), sel_getName(sel),self);

}

void*_objc_forward_handler = (void*)objc_defaultForwardHandler;

#if SUPPORT_STRET

structstret {inti[100]; };

__attribute__((noreturn))structstret

objc_defaultForwardStretHandler(idself, SEL sel)

{

objc_defaultForwardHandler(self, sel);

}

void*_objc_forward_stret_handler = (void*)objc_defaultForwardStretHandler;

#endif

objc_defaultForwardHandler中的_objc_fatal作用就是打日志并調(diào)用__builtin_trap()觸發(fā) crash姑食,可以看到我們最熟悉的那句 “unrecognized selector sent to instance” 日志。__builtin_trap()在殺掉進(jìn)程的同時(shí)還能生成日志茅坛,比調(diào)用exit()更好音半。objc_defaultForwardStretHandler就是裝模作樣搞個(gè)形式主義,把objc_defaultForwardHandler包了一層贡蓖。__attribute__((noreturn))屬性通知編譯器函數(shù)從不返回值曹鸠,當(dāng)遇到類似函數(shù)需要返回值而卻不可能運(yùn)行到返回值處就已經(jīng)退出來(lái)的情況,該屬性可以避免出現(xiàn)錯(cuò)誤信息斥铺。這里正適合此屬性彻桃,因?yàn)橐蠓祷亟Y(jié)構(gòu)體噠。

因?yàn)槟J(rèn)的 Handler 干的事兒就是打日志觸發(fā) crash晾蜘,我們想要實(shí)現(xiàn)消息轉(zhuǎn)發(fā)邻眷,就需要替換掉 Handler 并賦值給_objc_forward_handler或_objc_forward_handler_stret眠屎,賦值的過(guò)程就需要用到objc_setForwardHandler函數(shù),實(shí)現(xiàn)也是簡(jiǎn)單粗暴肆饶,就是賦值案鸟谩:

voidobjc_setForwardHandler(void*fwd,void*fwd_stret)

{

_objc_forward_handler = fwd;

#ifSUPPORT_STRET

_objc_forward_stret_handler = fwd_stret;

#endif

}

逆向工程助力刨根問(wèn)底

重頭戲在于對(duì)objc_setForwardHandler的調(diào)用,以及之后的消息轉(zhuǎn)發(fā)調(diào)用棧驯镊。這回不是在 Objective-C Runtime (libobjc.dylib)中啦葫督,而是在 Core Foundation(CoreFoundation.framework)中。雖然 CF 是開源的阿宅,但有意思的是蘋果故意在開源的代碼中刪除了在CFRuntime.c文件__CFInitialize()中調(diào)用objc_setForwardHandler的代碼候衍。__CFInitialize()函數(shù)是在 CF runtime 連接到進(jìn)程時(shí)初始化調(diào)用的。從反編譯得到的匯編代碼中可以很容易跟 C 源碼對(duì)比出來(lái)洒放,我用紅色標(biāo)出了同一段代碼的差異蛉鹿。

匯編語(yǔ)言還是比較好理解的,紅色標(biāo)出的那三個(gè)指令就是把__CF_forwarding_prep_0和___forwarding_prep_1___作為參數(shù)調(diào)用objc_setForwardHandler方法(那么之前那兩個(gè) DefaultHandler 卵用都沒(méi)有咯往湿,反正不出意外會(huì)被 CF 替換掉):

反編譯后的 __CFInitialize() 匯編代碼

然而在源碼中對(duì)應(yīng)的代碼卻被刪掉啦:

蘋果提供的 __CFInitialize() 函數(shù)源碼

在早期版本的 CF 源碼中妖异,還是可以看到__CF_forwarding_prep_0和___forwarding_prep_1___的聲明的,但是不會(huì)有實(shí)現(xiàn)源碼领追,也沒(méi)有對(duì)objc_setForwardHandler的調(diào)用他膳。這些細(xì)節(jié)從函數(shù)調(diào)用棧中無(wú)法看出,只能逆向工程看匯編指令绒窑。但從函數(shù)調(diào)用椬厮铮可以看出__CF_forwarding_prep_0和___forwarding_prep_1___這兩個(gè) Forward Handler 做了啥:

2016-06-14 12:50:15.385 MessageForward[67364:7174239] -[MFObject sendMessage]: unrecognized selector sent toinstance0x1006001a0

2016-06-14 12:50:15.387 MessageForward[67364:7174239] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[MFObject sendMessage]: unrecognized selector sent toinstance0x1006001a0'

*** Firstthrowcall stack:

(

0? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x00007fff8fa554f2 __exceptionPreprocess + 178

1? libobjc.A.dylib? ? ? ? ? ? ? ? ? ? 0x00007fff98396f7e objc_exception_throw + 48

2? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x00007fff8fabf1ad -[NSObject(NSObject) doesNotRecognizeSelector:] + 205

3? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x00007fff8f9c5571 ___forwarding___ + 1009

4? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x00007fff8f9c50f8 _CF_forwarding_prep_0 + 120

5? MessageForward? ? ? ? ? ? ? ? ? ? ? 0x0000000100000f1f main + 79

6? libdyld.dylib? ? ? ? ? ? ? ? ? ? ? 0x00007fff8bc2c5ad start + 1

7? ???? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x0000000000000001 0x0 + 1

)

libc++abi.dylib: terminating with uncaught exception of type NSException

這個(gè)日志場(chǎng)景熟悉得不能再熟悉了,可以看出_CF_forwarding_prep_0函數(shù)調(diào)用了___forwarding___函數(shù)些膨,接著又調(diào)用了doesNotRecognizeSelector方法蟀俊,最后拋出異常。但是靠這些是無(wú)法說(shuō)服看客的订雾,還得靠逆向工程反編譯后再反匯編成偽代碼來(lái)一探究竟肢预,刨根問(wèn)底。

__CF_forwarding_prep_0和___forwarding_prep_1___函數(shù)都調(diào)用了___forwarding___洼哎,只是傳入?yún)?shù)不同烫映。___forwarding___有兩個(gè)參數(shù),第一個(gè)參數(shù)為將要被轉(zhuǎn)發(fā)消息的棧指針(可以簡(jiǎn)單理解成 IMP)噩峦,第二個(gè)參數(shù)標(biāo)記是否返回結(jié)構(gòu)體锭沟。__CF_forwarding_prep_0第二個(gè)參數(shù)傳入0,___forwarding_prep_1___傳入的是1识补,從函數(shù)名都能看得出來(lái)族淮。下面是這兩個(gè)函數(shù)的偽代碼:

int__CF_forwarding_prep_0(intarg0,intarg1,intarg2,intarg3,intarg4,intarg5) {

rax= ____forwarding___(rsp,0x0);

if (rax!=0x0) { // 轉(zhuǎn)發(fā)結(jié)果不為空,將內(nèi)容返回

rax= *rax;

}

else { // 轉(zhuǎn)發(fā)結(jié)果為空,調(diào)用 objc_msgSend(id self, SEL _cmd,...);

rsi= *(rsp+0x8);

rdi= *rsp;

rax= objc_msgSend(rdi,rsi);

}

returnrax;

}

int___forwarding_prep_1___(intarg0,intarg1,intarg2,intarg3,intarg4,intarg5) {

rax= ____forwarding___(rsp,0x1);

if (rax!=0x0) {// 轉(zhuǎn)發(fā)結(jié)果不為空瞧筛,將內(nèi)容返回

rax= *rax;

}

else {// 轉(zhuǎn)發(fā)結(jié)果為空,調(diào)用 objc_msgSend_stret(void * st_addr, id self, SEL _cmd, ...);

rdx= *(rsp+0x10);

rsi= *(rsp+0x8);

rdi= *rsp;

rax= objc_msgSend_stret(rdi,rsi,rdx);

}

returnrax;

}

在x86_64架構(gòu)中导盅,rax寄存器一般是作為返回值较幌,rsp寄存器是棧指針。在調(diào)用objc_msgSend函數(shù)時(shí)白翻,參數(shù)arg0(self), arg1(_cmd), arg2, arg3, arg4, arg5分別使用寄存器rdi, rsi, rdx, rcx, r8, r9的值乍炉。在調(diào)用objc_msgSend_stret時(shí)第一個(gè)參數(shù)為st_addr,其余參數(shù)依次后移滤馍。為了能夠打包出NSInvocation實(shí)例并傳入后續(xù)的forwardInvocation:方法岛琼,在調(diào)用___forwarding___函數(shù)之前會(huì)先將所有參數(shù)壓入棧中。因?yàn)榧拇嫫鱮sp為棧指針指向棧頂巢株,所以rsp的內(nèi)容就是self啦槐瑞,因?yàn)閤86_64是小端,棧增長(zhǎng)方向是由高地址到低地址阁苞,所以從棧頂往下移動(dòng)一個(gè)指針需要0x8(64bit)困檩。而將參數(shù)入棧的順序是從后往前的,也就是說(shuō)arg0是最后一個(gè)入棧的那槽,位于棧頂:

__CF_forwarding_prep_0:

0000000000085080pushrbp; XREF=___CFInitialize+138

0000000000085081movrbp,rsp

0000000000085084subrsp,0xd0

000000000008508bmovqword[ss:rsp+0xb0],rax

0000000000085093movqqword[ss:rsp+0xa0],xmm7

000000000008509cmovqqword[ss:rsp+0x90],xmm6

00000000000850a5movqqword[ss:rsp+0x80],xmm5

00000000000850aemovqqword[ss:rsp+0x70],xmm4

00000000000850b4movqqword[ss:rsp+0x60],xmm3

00000000000850bamovqqword[ss:rsp+0x50],xmm2

00000000000850c0movqqword[ss:rsp+0x40],xmm1

00000000000850c6movqqword[ss:rsp+0x30],xmm0

00000000000850ccmovqword[ss:rsp+0x28],r9

00000000000850d1movqword[ss:rsp+0x20],r8

00000000000850d6movqword[ss:rsp+0x18],rcx

00000000000850dbmovqword[ss:rsp+0x10],rdx

00000000000850e0movqword[ss:rsp+0x8],rsi

00000000000850e5movqword[ss:rsp],rdi

00000000000850e9movrdi,rsp; argument #1 for method ____forwarding___

00000000000850ecmovrsi,0x0; argument #2 for method ____forwarding___

00000000000850f3call____forwarding___

消息轉(zhuǎn)發(fā)的邏輯幾乎都寫在___forwarding___函數(shù)中了悼沿,實(shí)現(xiàn)比較復(fù)雜,反編譯出的偽代碼也不是很直觀骚灸。我對(duì)arigrant.com的結(jié)果完善如下:

int __forwarding__(void*frameStackPointer, int isStret) {

id receiver = *(id*)frameStackPointer;

SEL sel = *(SEL*)(frameStackPointer + 8);

const char *selName = sel_getName(sel);

Class receiverClass = object_getClass(receiver);

// 調(diào)用 forwardingTargetForSelector:

if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {

id forwardingTarget = [receiver forwardingTargetForSelector:sel];

if (forwardingTarget&&forwarding != receiver) {

if (isStret==1) {

int ret;

objc_msgSend_stret(&ret,forwardingTarget, sel, ...);

return ret;

}

return objc_msgSend(forwardingTarget, sel, ...);

}

}

// 僵尸對(duì)象

const char *className = class_getName(receiverClass);

const char *zombiePrefix ="_NSZombie_";

size_t prefixLen = strlen(zombiePrefix); // 0xa

if (strncmp(className, zombiePrefix, prefixLen) ==0) {

CFLog(kCFLogLevelError,

@"*** -[%s %s]: message sent to deallocated instance %p",

className + prefixLen,

selName,

receiver);

}

// 調(diào)用 methodSignatureForSelector 獲取方法簽名后再調(diào)用 forwardInvocation

if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {

NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];

if (methodSignature) {

BOOL signatureIsStret = [methodSignature _frameDescriptor]->returnArgInfo.flags.isStruct;

if (signatureIsStret != isStret) {

CFLog(kCFLogLevelWarning ,

@"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'.? Signature thinks it does%s return a struct, and compiler thinks it does%s.",

selName,

signatureIsStret ? "" : not,

isStret ? "" : not);

}

if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {

NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];

[receiver forwardInvocation:invocation];

void *returnValue = NULL;

[invocation getReturnValue:&value];

return returnValue;

} else {

CFLog(kCFLogLevelWarning ,

@"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message",

receiver,

className);

return 0;

}

}

}

SEL *registeredSel = sel_getUid(selName);

// selector 是否已經(jīng)在 Runtime 注冊(cè)過(guò)

if (sel!= registeredSel) {

CFLog(kCFLogLevelWarning,

@"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort",

sel,

selName,

registeredSel);

} // doesNotRecognizeSelector

else if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {

[receiver doesNotRecognizeSelector:sel];

}

else {

CFLog(kCFLogLevelWarning,

@"*** NSForwarding: warning: object %p of class '%s' does not implement doesNotRecognizeSelector: -- abort",

receiver,

className);

}

// The point of no return.

kill(getpid(),9);

}

這么一大坨代碼就是整個(gè)消息轉(zhuǎn)發(fā)路徑的邏輯糟趾,概括如下:

先調(diào)用forwardingTargetForSelector方法獲取新的 target 作為 receiver 重新執(zhí)行 selector,如果返回的內(nèi)容不合法(為nil或者跟舊 receiver 一樣)甚牲,那就進(jìn)入第二步义郑。

調(diào)用methodSignatureForSelector獲取方法簽名后,判斷返回類型信息是否正確鳖藕,再調(diào)用forwardInvocation執(zhí)行NSInvocation對(duì)象魔慷,并將結(jié)果返回。如果對(duì)象沒(méi)實(shí)現(xiàn)methodSignatureForSelector方法著恩,進(jìn)入第三步院尔。

調(diào)用doesNotRecognizeSelector方法。

doesNotRecognizeSelector之前其實(shí)還有個(gè)判斷 selector 在 Runtime 中是否注冊(cè)過(guò)的邏輯喉誊,但在我們正常發(fā)消息的時(shí)候不會(huì)出此問(wèn)題邀摆。但如果手動(dòng)創(chuàng)建一個(gè)NSInvocation對(duì)象并調(diào)用invoke,并將第二個(gè)參數(shù)設(shè)置成一個(gè)不存在的 selector伍茄,那就會(huì)導(dǎo)致這個(gè)問(wèn)題栋盹,并輸入日志 “does not match selector known to Objective C runtime”。較真兒的讀者可能會(huì)有疑問(wèn):何這段邏輯判斷干脆用不到卻還存在著敷矫?難道除了__CF_forwarding_prep_0和___forwarding_prep_1___函數(shù)還有其他函數(shù)也調(diào)用___forwarding___么例获?莫非消息轉(zhuǎn)發(fā)還有其他路徑汉额?其實(shí)并不是!原因是___forwarding___調(diào)用了___invoking___函數(shù)榨汤,所以上面的偽代碼直接把___invoking___函數(shù)的邏輯也『翻譯』過(guò)來(lái)了蠕搜。除了___forwarding___函數(shù),以下方法也會(huì)調(diào)用___invoking___函數(shù):

-[NSInvocation invoke]

-[NSInvocationinvokeUsingIMP:]

-[NSInvocation invokeSuper]

doesNotRecognizeSelector方法其實(shí)在 libobj.A.dylib 中已經(jīng)廢棄了收壕,而是在 CF 框架中實(shí)現(xiàn)妓灌,而且也不是開源的。從函數(shù)調(diào)用椕巯埽可以發(fā)現(xiàn)doesNotRecognizeSelector之后會(huì)拋出異常虫埂,而 Runtime 中廢棄的實(shí)現(xiàn)知識(shí)打日志后直接殺掉進(jìn)程(__builtin_trap())。下面是 CF 中實(shí)現(xiàn)的偽代碼:

void-[NSObjectdoesNotRecognizeSelector:](void*self,void* _cmd,void* arg2) {

r14 = ___CFFullMethodName([selfclass],self, arg2);

_CFLog(0x3,@"%@: unrecognized selector sent to instance %p", r14,self, r8, r9, stack[2048]);

rbx = _CFMakeCollectable(_CFStringCreateWithFormat(___kCFAllocatorSystemDefault,0x0,@"%@: unrecognized selector sent to instance %p"));

if(*(int8_t *)___CFOASafe!=0x0) {

___CFRecordAllocationEvent();

}

rax = _objc_rootAutorelease(rbx);

rax = [NSExceptionexceptionWithName:@"NSInvalidArgumentException"reason:rax userInfo:0x0];

objc_exception_throw(rax);

return;

}

void+[NSObjectdoesNotRecognizeSelector:](void*self,void* _cmd,void* arg2) {

r14 = ___CFFullMethodName([selfclass],self, arg2);

_CFLog(0x3,@"%@: unrecognized selector sent to class %p", r14,self, r8, r9, stack[2048]);

rbx = _CFMakeCollectable(_CFStringCreateWithFormat(___kCFAllocatorSystemDefault,0x0,@"%@: unrecognized selector sent to class %p"));

if(*(int8_t *)___CFOASafe!=0x0) {

___CFRecordAllocationEvent();

}

rax = _objc_rootAutorelease(rbx);

rax = [NSExceptionexceptionWithName:@"NSInvalidArgumentException"reason:rax userInfo:0x0];

objc_exception_throw(rax);

return;

}

也就是說(shuō)我們可以 overridedoesNotRecognizeSelector或者捕獲其拋出的異常圃验。在這里還是大有文章可做的掉伏。

總結(jié)

我將整個(gè)實(shí)現(xiàn)流程繪制出來(lái),過(guò)濾了一些不會(huì)進(jìn)入的分支路徑和跟主題無(wú)關(guān)的細(xì)節(jié):

消息發(fā)送與轉(zhuǎn)發(fā)路徑流程圖

介于國(guó)內(nèi)關(guān)于這塊知識(shí)的好多文章描述不夠準(zhǔn)確和詳細(xì)澳窑,或是對(duì)消息轉(zhuǎn)發(fā)的原理描述理解不夠深刻岖免,或是側(cè)重貼源碼而欠思考,所以我做了一個(gè)比較全面詳細(xì)的講解照捡。

參考文獻(xiàn)

Why objc_msgSend Must be Written in Assembly

Hmmm, What’s that Selector?

A Look Under the Hood of objc_msgSend()

Printing Objective-C Invocations in LLDB

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末颅湘,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子栗精,更是在濱河造成了極大的恐慌闯参,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件悲立,死亡現(xiàn)場(chǎng)離奇詭異鹿寨,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)薪夕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門脚草,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人原献,你說(shuō)我怎么就攤上這事馏慨。” “怎么了姑隅?”我有些...
    開封第一講書人閱讀 162,823評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵写隶,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我讲仰,道長(zhǎng)慕趴,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮冕房,結(jié)果婚禮上躏啰,老公的妹妹穿的比我還像新娘。我一直安慰自己耙册,他們只是感情好丙唧,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著觅玻,像睡著了一般。 火紅的嫁衣襯著肌膚如雪培漏。 梳的紋絲不亂的頭發(fā)上溪厘,一...
    開封第一講書人閱讀 51,190評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音牌柄,去河邊找鬼畸悬。 笑死,一個(gè)胖子當(dāng)著我的面吹牛珊佣,可吹牛的內(nèi)容都是我干的蹋宦。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼咒锻,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼冷冗!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起惑艇,我...
    開封第一講書人閱讀 38,923評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤蒿辙,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后滨巴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體思灌,經(jīng)...
    沈念sama閱讀 45,334評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評(píng)論 2 333
  • 正文 我和宋清朗相戀三年恭取,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了泰偿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,727評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蜈垮,死狀恐怖耗跛,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情攒发,我是刑警寧澤课兄,帶...
    沈念sama閱讀 35,428評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站晨继,受9級(jí)特大地震影響烟阐,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評(píng)論 3 326
  • 文/蒙蒙 一蜒茄、第九天 我趴在偏房一處隱蔽的房頂上張望唉擂。 院中可真熱鬧,春花似錦檀葛、人聲如沸玩祟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)空扎。三九已至,卻和暖如春润讥,著一層夾襖步出監(jiān)牢的瞬間转锈,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工楚殿, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留撮慨,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,734評(píng)論 2 368
  • 正文 我出身青樓脆粥,卻偏偏與公主長(zhǎng)得像砌溺,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子变隔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容