Objective-C中的消息發(fā)送總結(jié)

關(guān)于OC中的消息發(fā)送的實(shí)現(xiàn)压语,在去年也看過(guò)一次褂傀,當(dāng)時(shí)有點(diǎn)不太理解忍啤,但是今年再看卻很容易理解。?

我想這跟知識(shí)體系的構(gòu)建有關(guān)紊服,如果你不認(rèn)識(shí)有磚檀轨、水泥等這些建筑的基本組成部分胸竞,那么我們應(yīng)該很難理解建筑是怎么建造出來(lái)的吧??

學(xué)習(xí)新知識(shí)参萄,應(yīng)該也是同樣的道理卫枝!

資料

今年再看 消息發(fā)送機(jī)制時(shí),也翻了很多文章讹挎,本來(lái)想自己總結(jié)一遍的校赤,但是感覺這篇?Objective-C 消息發(fā)送與轉(zhuǎn)發(fā)機(jī)制原理?實(shí)在寫的太好了,就直接轉(zhuǎn)載了筒溃。?

原文:http://yulingtianxia.com/blog/2016/06/15/Objective-C-Message-Sending-and-Forwarding/

消息發(fā)送和轉(zhuǎn)發(fā)流程可以概括為:消息發(fā)送(Messaging)是Runtime通過(guò)selector 快速查找IMP的過(guò)程马篮,有了函數(shù)指針就可以執(zhí)行對(duì)應(yīng)的方法實(shí)現(xiàn);消息轉(zhuǎn)發(fā)(Message Frowarding)是在查找IMP失敗后一系列轉(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钙皮、元類等概念將不再贅述蜂科。本文用到的源碼為objc-680 和 CF-1153.18,逆向CoreFoundation.framework的系統(tǒng)版本為macOS10.11.5,匯編語(yǔ)言架構(gòu)為x86_64。

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

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

? Class class = object_getClass(self);

? IMP imp = class_getMethodImplementation(class, _cmd);

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

}

源碼解析

為啥老用偽代碼?因?yàn)?objc_msgSend使用匯編語(yǔ)言寫的茸时,針對(duì)不同架構(gòu)有不同的實(shí)現(xiàn)(我們可以在objc-680的Source目錄下看到多個(gè)objc-msg-xxxx的匯編實(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í)行梧喷。如果緩存沒中睛琳,那就得到Class的方法表中查找了。?

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

.macro MethodTableLookup

? ? 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ù)返回值排列組合映射不同方法簽名(method signature)的問(wèn)題铲觉,那switch語(yǔ)句得老長(zhǎng)了壤躲。。备燃。這些原因可以總結(jié)為?Calling Convention (調(diào)用慣例),也就是說(shuō)函數(shù)調(diào)用者與被調(diào)用者必須約定好參數(shù)與返回值在不同架構(gòu)處理器上的存取規(guī)則凌唬,比如參數(shù)是以何種順序存儲(chǔ)在棧上并齐,或是存儲(chǔ)在哪些寄存器上。除此之外還有其他原因客税,比如其可變參數(shù)用匯編處理起來(lái)最方便况褪,因?yàn)檎业絀MP地址后參數(shù)都在棧上。要是用C++傳遞可變參數(shù)那就被拒了更耻,prologue機(jī)制會(huì)弄亂地址(比如i386上為了存儲(chǔ)ebp 向后移位bbyte),最后還要用epilogue打掃戰(zhàn)場(chǎng)测垛。而且匯編程序執(zhí)行效率高,在Objective-C Runtime中調(diào)用頻率較高的函數(shù)好多都用匯編編寫的秧均。

objc_msgSend_fpret?后面fpret 其實(shí)是float point return 的縮寫食侮;stret 就是struct return的縮寫,其他同理目胡。?

關(guān)于 Calling Convention锯七,可以去看Bang 的文章動(dòng)態(tài)調(diào)用C函數(shù)的 Calling Convention一節(jié)

使用 lookUpImpOrForward 快速查找 IMP

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

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)

{

? ? return lookUpImpOrForward(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接著做了如下兩件事:?

1.如果使用緩存(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ò)半夷。?

2.如果是第一次用到這個(gè)類且?initialize?參數(shù)為?YES(initialize && !cls->isInitialized()),需要進(jìn)行初始化工作迅细,也就是開辟一個(gè)用于讀寫數(shù)據(jù)的空間巫橄。先對(duì)?runtimeLock?寫操作加鎖,然后調(diào)用?cls?的?initialize?方法茵典。如果?sel == initialize?也沒關(guān)系湘换,雖然?initialize?還會(huì)被調(diào)用一次,但不會(huì)起作用啦统阿,因?yàn)?cls->isInitialized()?已經(jīng)是 YES 啦彩倚。

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

考慮到運(yùn)行時(shí)類中的方法可能會(huì)增加,需要先做讀操作加鎖扶平,使得方法查找和緩存填充成原子操作帆离。添加category 會(huì)刷新緩存,之后如果舊數(shù)據(jù)又被重填到緩存中结澄,category 添加操作就會(huì)被忽略掉哥谷。

runtimeLock.read();

之后的邏輯整理如下:?

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

2.查找當(dāng)前類中的緩存齿桃,跟之前一樣惑惶,使用?cache_getImp?匯編程序入口。如果命中緩存獲取到了 IMP短纵,則直接跳到第 7 步集惋;否則執(zhí)行下一步。?

3.在當(dāng)前類中的方法列表(method list)中進(jìn)行查找踩娘,也就是根據(jù) selector 查找到 Method 后刮刑,獲取 Method 中的 IMP(也就是?method_imp?屬性)喉祭,并填充到緩存中。查找過(guò)程比較復(fù)雜雷绢,會(huì)針對(duì)已經(jīng)排序的列表使用二分法查找泛烙,未排序的列表則是線性遍歷。如果成功查找到 Method 對(duì)象翘紊,就直接跳到第 7 步蔽氨;否則執(zhí)行下一步。?

4.在繼承層級(jí)中遞歸向父類中查找帆疟,情況跟上一步類似鹉究,也是先查找緩存,緩存沒中就查找方法列表踪宠。這里跟上一步不同的地方在于緩存策略自赔,有個(gè)?_objc_msgForward_impcache?匯編程序入口作為緩存中消息轉(zhuǎn)發(fā)的標(biāo)記。也就是說(shuō)如果在緩存中找到了 IMP柳琢,但如果發(fā)現(xiàn)其內(nèi)容是?_objc_msgForward_impcache绍妨,那就終止在類的繼承層級(jí)中遞歸查找,進(jìn)入下一步柬脸;否則跳到第 7 步他去。?

5.當(dāng)傳入lookUpImpOrForward的參數(shù)resolver為YES并且是第一次進(jìn)入第5步時(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.?

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

7.讀操作解鎖拗胜,并將之前找到的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) return nil;

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

? ? // Translate forwarding function to C-callable external version? ? if (!imp) {

? ? ? ? return _objc_msgForward;

? ? }

? ? return imp;

}

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

IMP lookUpImpOrNil(Class cls, SEL sel, id inst,

? ? ? ? ? ? ? ? ? bool initialize, bool cache, bool resolver)

{

? ? 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)用效床。但在 macOS 10.6及更早版本的libobjc.A.dylib中是不能直接調(diào)用的睹酌。況且我們根本不會(huì)直接用到它。帶?stret后綴的函數(shù)依舊是返回值為結(jié)構(gòu)體的版本剩檀。

上一節(jié)最后降到如果沒找到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) for forwarding

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:

IMP class_getMethodImplementation_stret(Classcls,SELsel){ IMP imp = class_getMethodImplementation(cls, sel);

? ? // Translate forwarding function to struct-returning version? ? 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-stret version

movq? ? __objc_forward_handler(%rip), %r11

jmp *%r11

END_ENTRY? __objc_msgForward

ENTRY? __objc_msgForward_stret

// Struct-return version

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ò)程是先將_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(id self, 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

struct stret { int i[100]; };

__attribute__((noreturn)) struct stret

objc_defaultForwardStretHandler(id self, 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)程的同事還能生成日志嵌莉,比調(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)單粗暴筐赔,就是賦值啊:

void objc_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 卵用都沒有咯溜哮,反正不出意外會(huì)被 CF 替換掉):

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

在早起版本的CF源碼中,還是可以看到?__CF_forwarding_prep_0?和?___forwarding_prep_1___的聲明的拂封,但是不會(huì)有實(shí)現(xiàn)源碼茬射,也沒有對(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 to instance 0x1006001a0

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

*** First throw call 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 typeNSException

這個(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(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) {

? ? 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);

? ? }

? ? return rax;

}

int ___forwarding_prep_1___(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) {

? ? 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);

? ? }

? ? return rax;

}

在?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)榧拇嫫?rsp?為棧指針指向棧頂魔招,所以?rsp?的內(nèi)容就是self?啦晰搀,因?yàn)?x86_64?是小端,棧增長(zhǎng)方向是由高地址到低地址办斑,所以從棧頂往下移動(dòng)一個(gè)指針需要加?0x8(64bit)外恕。而將參數(shù)入棧的順序是從后往前的,也就是說(shuō)?arg0?是最后一個(gè)入棧的乡翅,位于棧頂:

__CF_forwarding_prep_0:

0000000000085080 push rbp ; XREF=___CFInitialize+1380000000000085081 mov rbp, rsp

0000000000085084 sub rsp, 0xd0

000000000008508b mov qword [ss:rsp+0xb0], rax

0000000000085093 movq qword [ss:rsp+0xa0], xmm7

000000000008509c movq qword [ss:rsp+0x90], xmm6

00000000000850a5 movq qword [ss:rsp+0x80], xmm5

00000000000850ae movq qword [ss:rsp+0x70], xmm4

00000000000850b4 movq qword [ss:rsp+0x60], xmm3

00000000000850ba movq qword [ss:rsp+0x50], xmm2

00000000000850c0 movq qword [ss:rsp+0x40], xmm1

00000000000850c6 movq qword [ss:rsp+0x30], xmm0

00000000000850cc mov qword [ss:rsp+0x28], r9

00000000000850d1 mov qword [ss:rsp+0x20], r8

00000000000850d6 mov qword [ss:rsp+0x18], rcx

00000000000850db mov qword [ss:rsp+0x10], rdx

00000000000850e0 mov qword [ss:rsp+0x8], rsi

00000000000850e5 mov qword [ss:rsp], rdi

00000000000850e9 mov rdi, rsp ; argument #1 for method____forwarding___

00000000000850ec movrsi, 0x0; argument #2 for method____forwarding___

00000000000850f3 call____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);

? ?<breakPoint-interrupt>

? }

? // 調(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ā)路徑的邏輯尚洽,概況如下:?

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

2.調(diào)用?methodSignatureForSelector獲取方法簽名后腺毫,判斷返回類型信息是否正確癣疟,再調(diào)用forwardInvocation執(zhí)行?NSInvocation對(duì)象,并將結(jié)果返回潮酒。如果對(duì)象沒實(shí)現(xiàn)methodSignatureForSelector方法睛挚,進(jìn)入第三步。?

3.調(diào)用?doesNotRecognizeSelector方法急黎。

doesNotRecognizeSelector?之前其實(shí)還有個(gè)判斷selector 在Runtime 中是否注冊(cè)過(guò)的邏輯扎狱,但在我們正常發(fā)消息的時(shí)候,不會(huì)出現(xiàn)此問(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)用了___forwarding___函數(shù)删铃,以下方法也會(huì)調(diào)用?___invoking___函數(shù):

-[NSInvocationinvoke]

-[NSInvocationinvokeUsingIMP:]

-[NSInvocationinvokeSuper]

doesNotRecognizeSelector?方法其實(shí)在libobj.A.dylib 中已經(jīng)廢棄了,而是在CF框架中實(shí)現(xiàn)耳贬,而且也不是開源的。從函數(shù)調(diào)用椓匝洌可以發(fā)現(xiàn)?doesNotRecognizeSelector之后會(huì)拋出異常咒劲,而Runtime 中廢棄的實(shí)現(xiàn)只是打印日志后直接殺掉進(jìn)程(__builtin_trap())。下面是CF中實(shí)現(xiàn)的偽代碼:

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

? ? r14 = ___CFFullMethodName([self class], 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 = [NSException exceptionWithName:@"NSInvalidArgumentException" reason:rax userInfo:0x0];

? ? objc_exception_throw(rax);

? ? return;

}void +[NSObject doesNotRecognizeSelector:](void * self, void * _cmd, void * arg2) {

? ? r14 = ___CFFullMethodName([self class], 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 = [NSException exceptionWithName:@"NSInvalidArgumentException" reason:rax userInfo:0x0];

? ? objc_exception_throw(rax);

? ? return;

}

也就是說(shuō)我們可以override?doesNotRecognizeSelector?或者捕獲其爆出的異常诫隅。在這里還是大有文章可做的腐魂。

總結(jié)

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

圖像地址:http://7ni3rk.com1.z0.glb.clouddn.com/MessageForward/消息發(fā)送與轉(zhuǎn)發(fā)路徑流程圖.jpg

介于國(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閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件芍殖,死亡現(xiàn)場(chǎng)離奇詭異豪嗽,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門昵骤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人肯适,你說(shuō)我怎么就攤上這事变秦。” “怎么了框舔?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵蹦玫,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我刘绣,道長(zhǎng)樱溉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任纬凤,我火速辦了婚禮福贞,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘停士。我一直安慰自己挖帘,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布恋技。 她就那樣靜靜地躺著拇舀,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蜻底。 梳的紋絲不亂的頭發(fā)上骄崩,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音薄辅,去河邊找鬼要拂。 笑死,一個(gè)胖子當(dāng)著我的面吹牛站楚,可吹牛的內(nèi)容都是我干的宇弛。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼源请,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼枪芒!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起谁尸,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤舅踪,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后良蛮,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體抽碌,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了货徙。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片左权。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖痴颊,靈堂內(nèi)的尸體忽然破棺而出赏迟,到底是詐尸還是另有隱情,我是刑警寧澤蠢棱,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布锌杀,位于F島的核電站,受9級(jí)特大地震影響泻仙,放射性物質(zhì)發(fā)生泄漏糕再。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一玉转、第九天 我趴在偏房一處隱蔽的房頂上張望突想。 院中可真熱鬧,春花似錦究抓、人聲如沸蒿柳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)垒探。三九已至,卻和暖如春怠李,著一層夾襖步出監(jiān)牢的瞬間圾叼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工捺癞, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留夷蚊,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓髓介,卻偏偏與公主長(zhǎng)得像惕鼓,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子唐础,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

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