黑魔法——“低版本中使用高版本中的類”的技術(shù)實(shí)現(xiàn)原理

iOS8中蘋果用UIAlertController來統(tǒng)一管理alert和actionSheet了粱年,之前的UIAlertView和UIActionSheet已經(jīng)廢棄了钦睡。通常我們要兼容iOS7的時(shí)候逆皮,要這樣:

if([[[UIDevicecurrentDevice] systemVersion] floatValue] <=8.0) {//用UIAlertView或UIActionSheet}else{//用UIAlertController}

而GJAlertController解決了這里的系統(tǒng)版本兼容問題,不需要判斷版本柳譬,直接使用:

UIAlertController *alertVC = [UIAlertController alertControllerWithTitle:@"title"message:@"message"preferredStyle:UIAlertControllerStyleAlert];[alertVCaddAction:[UIAlertAction actionWithTitle:@"ok"style:UIAlertActionStyleDefault handler:^(UIAlertAction *_Nonnullaction) {? ? NSLog(@"button ok pressed");}]];[alertVCaddAction:[UIAlertAction actionWithTitle:@"cancel"style:UIAlertActionStyleDefault handler:^(UIAlertAction *_Nonnullaction) {? ? NSLog(@"button cancel pressed);

}]];

[alertVC addTextFieldWithConfigurationHandler:^(UITextField *textField) {

textField.placeholder = @"請(qǐng)輸入用戶名";

}];

[self presentViewController:alertVC animated:YES completion:nil];

如果創(chuàng)建的時(shí)候PreferredStyle傳入U(xiǎn)IAlertControllerStyleActionSheet群扶,則顯示actionSheet。

大家可能很好奇笨使,UIAlertController明明是iOS8才引入的卿樱,怎么能夠在iOS8以下的系統(tǒng)中跑呢?下面進(jìn)入正題硫椰。

原理概述

簡單來說就是三個(gè)字——黑魔法繁调。

利用這種黑魔法的例子已經(jīng)越來越多,我所知道的最早使用這種方法的是一個(gè)老外在三年為了解決NSUUID而使用的靶草。

我們國內(nèi)團(tuán)隊(duì)開發(fā)的FDStackView是一個(gè)非常好的開源庫蹄胰,已經(jīng)有1500+顆星星了,希望大家多多支持我們國內(nèi)的團(tuán)隊(duì)奕翔,在FDStackView庫中也用到了相同的技術(shù)裕寨,網(wǎng)上有人發(fā)出了分析實(shí)現(xiàn)原理的文章,但分析的很淺派继,而且根本沒有說在點(diǎn)子上宾袜,使得這種黑魔法的魅力并沒有被大家欣賞到,我這里做了一些功課驾窟,把這個(gè)原理詳細(xì)的闡述一下庆猫,以及這里的關(guān)鍵點(diǎn)在哪里。如果中間過程中有什么錯(cuò)誤绅络,還請(qǐng)大家指正月培,謝謝。

下面簡單說一下實(shí)現(xiàn)思路昨稼。

1.運(yùn)行時(shí)去判斷系統(tǒng)中是否已經(jīng)存在UIAlertController节视,如果存在,那就什么都不做假栓,靜靜的看著UIAlertController裝逼寻行,這就是iOS8及其之上版本的情況。

2.如果系統(tǒng)中沒有UIAlertController類匾荆,我們?cè)谶\(yùn)行時(shí)中做一些“手腳”拌蜘,讓我們的GJAlertController在低版本中去完成這個(gè)問題。這一步是精華所在牙丽,下面分析代碼的時(shí)候回詳細(xì)說明

詳細(xì)分析

實(shí)現(xiàn)的代碼本身其實(shí)并不重要简卧,下面先講最重要的一個(gè)東西,它是這種黑魔法能夠得以實(shí)現(xiàn)的前提烤芦。

在揭示這個(gè)重要前提之前举娩,我們先來簡單說說內(nèi)存。內(nèi)存有好多種,我們最熟悉的有:棧:函數(shù)的實(shí)現(xiàn)就依賴于棧铜涉,函數(shù)中簡單類型的局部變量也都開辟在棧上智玻;堆:我們平時(shí)用的Object都是開辟在堆上的;數(shù)據(jù)段:這個(gè)對(duì)我們相對(duì)陌生芙代,但是其實(shí)靜態(tài)字符串就是存在數(shù)據(jù)段的eg:

NSString *testStr = @"hello world";NSLog(@"testStr:%p",testStr);testStr:0xb4338 //32位的機(jī)器上testStr:0x106326580 //64位的機(jī)器上

數(shù)據(jù)段的內(nèi)存有些特殊吊奢,并不是我們理解的32上的指針是4Byte=32bit,64位上指針是8Byte=64bit纹烹,大家這里對(duì)數(shù)據(jù)段先有個(gè)概念页滚,一會(huì)要用它來解釋一些現(xiàn)象。

下面開始講這個(gè)黑魔法能夠?qū)崿F(xiàn)的前提铺呵,是很重要的部分裹驰。在編譯的時(shí)候,系統(tǒng)中的每個(gè)類都在數(shù)據(jù)段上有一個(gè)標(biāo)簽(形式是這樣的:OBJC_CLASS$_ClassName)片挂,這個(gè)標(biāo)簽?zāi)憧梢岳斫獬蒶ey邦马,它的value就是該類的類名,舉例:數(shù)據(jù)段中會(huì)有一個(gè)key是OBJC_CLASS$_UIAlertController宴卖,它對(duì)應(yīng)的value就是UIAlertController的類名,當(dāng)然也就會(huì)有OBJC_CLASS$_UIStackView這個(gè)標(biāo)簽邻悬,標(biāo)識(shí)著UIStackView這個(gè)類症昏。

最重要的一點(diǎn)是:在iOS7中,還沒有UIAlertController的時(shí)候父丰,這個(gè)標(biāo)簽OBJC_CLASS$_UIAlertController已經(jīng)存在了肝谭,只是這個(gè)標(biāo)簽對(duì)應(yīng)的value值是nil,因?yàn)闆]有這個(gè)類蛾扇,我們可以認(rèn)為是蘋果在給高版本的這個(gè)類站位攘烛,就是蘋果的這個(gè)站位才使得我們有幸用上了這個(gè)黑魔法。當(dāng)然每個(gè)后出現(xiàn)的類都是有站位的镀首,比如UIStackView坟漱。

if this label is Nil or doesnt exist, the class does not exist and cannot be allocated/used

這是我看到的老外在用該種黑魔法實(shí)現(xiàn)UUID的時(shí)候其中的一句說明,意思是:如果我們沒找找到這個(gè)標(biāo)簽更哄,就不能為該申請(qǐng)內(nèi)存芋齿,也就不能使用了。

我對(duì)這句話的結(jié)論持懷疑態(tài)度成翩,但又無法做實(shí)驗(yàn)驗(yàn)證觅捆,因?yàn)椤皹?biāo)簽站位”在早期版本中就存在了,而要找到“更早期”的版本驗(yàn)證該沒有標(biāo)簽是很困難的麻敌,因?yàn)閄code已經(jīng)不能支持對(duì)“更早期”的版本的編譯了栅炒,這段話表述有些混亂,大家還是往后看吧。

下面我們看看runtime里動(dòng)態(tài)添加類的方法:

Creates a new class and metaclass.

@param superclass The class to use as the new class's superclass, or \c Nil to create a new root class.

@param name The string to use as the new class's name. The string will be copied.

@param extraBytes The number of bytes to allocate for indexed ivars at the end of

the class and metaclass objects. This should usually be \c 0.

@return The new class, or Nil if the class could not be created (for example, the desired name is already in use).

OBJC_EXPORT Classobjc_allocateClassPair(Class superclass,constchar*name,size_textraBytes)__OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);

大家看官方對(duì)函數(shù)的說明可以知道:superClass 是你要添加的類的父類赢赊,name是你要添加的類的名字乙漓,extraBytes一般傳0,它會(huì)返回一個(gè)新類域携,如果名字被占用了會(huì)返回Nil簇秒。

由此要說明的兩個(gè)重要結(jié)論:

1.如果OBJC_CLASS$_ClassName標(biāo)簽存在,但是對(duì)應(yīng)的類不存在(相當(dāng)于有key秀鞭,但是value是nil)此時(shí)動(dòng)態(tài)添加類是可以成功的趋观。

2.如果OBJC_CLASS$_ClassName標(biāo)簽和對(duì)應(yīng)的類都有的話,此時(shí)動(dòng)態(tài)添加類是不成功的锋边,返回nil皱坛。

我們黑魔法的實(shí)現(xiàn)思路就是基于這兩個(gè)重要結(jié)論,下面我們具體看代碼豆巨。

代碼講解

__asm(".section? ? ? ? __DATA,__objc_classrefs,regular,no_dead_strip\n"#if? ? TARGET_RT_64_BIT".align? ? ? ? ? 3\n""L_OBJC_CLASS_UIAlertController:\n"".quad? ? ? ? ? _OBJC_CLASS_$_UIAlertController\n"#else".align? ? ? ? ? 2\n""_OBJC_CLASS_UIAlertController:\n"".long? ? ? ? ? _OBJC_CLASS_$_UIAlertController\n"#endif".weak_reference _OBJC_CLASS_$_UIAlertController\n");

這是一段匯編代碼剩辟,不用擔(dān)心看不懂它,我也不懂匯編往扔,這不影響我們分析贩猎,我簡單的解釋一下:

1.__asm是在C、C++源碼中放入?yún)R編代碼(OC是C的超集)萍膛。

2..align是對(duì)指令或數(shù)據(jù)的存放地址進(jìn)行對(duì)齊吭服,有些CPU架構(gòu)要求固定的指令長度,并且存放地址相對(duì)于2的冪指數(shù)圓整蝗罗,否則無法運(yùn)行艇棕,比如arm。有些不要這樣也能運(yùn)行串塑,就是執(zhí)行效率稍微低點(diǎn)沼琉,如i386。

3.64位的對(duì)齊方式是8位(2^3(.align后面的數(shù)))桩匪,32位的對(duì)齊方式是4位(2^2(.align后面的數(shù)))打瘪。對(duì)齊只對(duì)緊挨著它的那條語句起作用,既吸祟,L_OBJC_CLASS_UIAlertController或_OBJC_CLASS_UIAlertController瑟慈。

4..quad聲明一組數(shù)占64位,.long聲明一組數(shù)占32位

5..secton 后是指定參數(shù)用的屋匕,上述匯編的大體意思是在數(shù)據(jù)段(就是我們之前提到的數(shù)據(jù)段)找到OBJC_CLASS$_UIAlertController標(biāo)簽并利用.quad葛碧、.long聲明的一組數(shù)來存放它,取名為:_OBJC_CLASS_UIAlertController过吻。

這是一段枯燥又非重點(diǎn)的代碼进泼,如果大家心情不好直接忽略掉就可以了蔗衡。

__attribute__((constructor))staticvoidGJAlertControllerPatchEntry(void) {staticdispatch_once_tonceToken;dispatch_once(&onceToken, ^{@autoreleasepool{// >= iOS8.if(objc_getClass("UIAlertController")) {return;? ? ? ? ? ? }? ? ? ? ? ? Class *alertController =NULL;#if TARGET_CPU_ARM__asm("movw %0, :lower16:(_OBJC_CLASS_UIAlertController-(LPC0+4))\n""movt %0, :upper16:(_OBJC_CLASS_UIAlertController-(LPC0+4))\n""LPC0: add %0, pc":"=r"(alertController));#elif TARGET_CPU_ARM64__asm("adrp %0, L_OBJC_CLASS_UIAlertController@PAGE\n""add? %0, %0, L_OBJC_CLASS_UIAlertController@PAGEOFF":"=r"(alertController));#elif TARGET_CPU_X86_64__asm("leaq L_OBJC_CLASS_UIAlertController(%%rip), %0":"=r"(alertController));#elif TARGET_CPU_X86void*pc =NULL;? ? __asm("calll L0\n""L0: popl %0\n""leal _OBJC_CLASS_UIAlertController-L0(%0), %1":"=r"(pc),"=r"(alertController));#else#error Unsupported CPU#endifif(alertController && !*alertController) {? ? ? ? ? ? ? ? Classclass= objc_allocateClassPair([GJAlertControllerclass],"UIAlertController",0);if(class) {? ? ? ? ? ? ? ? ? ? objc_registerClassPair(class);? ? ? ? ? ? ? ? ? ? *alertController =class;? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }? ? });}

大家堅(jiān)持住,這是要分析的最后一段代碼了乳绕。

__attribute__((constructor))staticvoidGJAlertControllerPatchEntry(void){

}

總的來說上面的代碼是一個(gè)函數(shù)绞惦,

__attribute__((constructor))只是用來修飾函數(shù)的,它起什么作用呢洋措?這里涉及一個(gè)關(guān)于__attribute__的黑魔法济蝉,有興趣的人可以看我同事的一篇專門介紹__attribute__的文章

__attribute__((constructor))修飾的函數(shù)會(huì)在main函數(shù)之前執(zhí)行菠发,這是我們的最好時(shí)機(jī)王滤,有了runtime環(huán)境,但是main函數(shù)還沒有執(zhí)行滓鸠,一切都“來得及”雁乡。

if(objc_getClass("UIAlertController")) {return;}

系統(tǒng)中有UIAlertController類的話,直接返回糜俗,這個(gè)邏輯之前已經(jīng)提到過了踱稍。

Class *alertController = NULL;#if TARGET_CPU_ARM__asm("movw %0, :lower16:(_OBJC_CLASS_UIAlertController-(LPC0+4))\n""movt %0, :upper16:(_OBJC_CLASS_UIAlertController-(LPC0+4))\n""LPC0: add %0, pc":"=r"(alertController));#elif TARGET_CPU_ARM64__asm("adrp %0, L_OBJC_CLASS_UIAlertController@PAGE\n""add? %0, %0, L_OBJC_CLASS_UIAlertController@PAGEOFF":"=r"(alertController));#elif TARGET_CPU_X86_64__asm("leaq L_OBJC_CLASS_UIAlertController(%%rip), %0":"=r"(alertController));#elif TARGET_CPU_X86void *pc = NULL;? ? __asm("calll L0\n""L0: popl %0\n""leal _OBJC_CLASS_UIAlertController-L0(%0), %1":"=r"(pc),"=r"(alertController));#else#error Unsupported CPU#endif

這段匯編大家直接忽略,意思就是把之前_OBJC_CLASS_UIAlertController中的值拿出來放到alertController里悠抹,之所以這么麻煩是因?yàn)椴煌軜?gòu)的CPU運(yùn)行的指令集不同珠月,例如,32位就要這樣弄:MOVW 把16位立即數(shù)放到寄存器的底16位楔敌,高16位清0

MOVT 把16位立即數(shù)放到寄存器的高16位桥温,低16位不影響。

if(alertController && !*alertController) {? ? Classclass=objc_allocateClassPair([GJAlertControllerclass],"UIAlertController",0);if(class){? ? ? ? objc_registerClassPair(class);*alertController =class;}}

如果alertController存在梁丘,證明OBJC_CLASS$_UIAlertController標(biāo)簽存在,即key存在旺韭,*alertController不存在氛谜,證明當(dāng)前系統(tǒng)中沒有這個(gè)類,即value不存在区端。這正是我們之前說的情況值漫,如果我們此時(shí)打印alertController的地址,會(huì)發(fā)現(xiàn)织盼,它的位數(shù)和上面數(shù)據(jù)段中的一樣而不是32位或64位杨何,也再次印證了標(biāo)簽在數(shù)據(jù)段上。

此時(shí)執(zhí)行最重要的一句代碼——?jiǎng)討B(tài)添加類

Classclass=objc_allocateClassPair([GJAlertControllerclass],"UIAlertController",0);

這決對(duì)是畫龍點(diǎn)睛的一筆沥邻,我們之前用的時(shí)候都是繼承一個(gè)系統(tǒng)類危虱,動(dòng)態(tài)添加一個(gè)自定義的類:

Class person = objc_allocateClassPair([NSObjectclass],"Person",0);

這里正好相反,這里是在判斷了沒有系統(tǒng)類的時(shí)候唐全,添加一個(gè)系統(tǒng)類埃跷,繼承自我們的類:GJAlertController蕊玷,也就是說,在低版本中弥雹,沒有UIAlerController垃帅,我們動(dòng)態(tài)添加這個(gè)類,讓他繼承GJAlertController剪勿,我們?cè)贕JAlertController中贸诚,實(shí)現(xiàn)一套與系統(tǒng)UIAlertController一模一樣的API給人造成的錯(cuò)覺好像是在低版本中也能使用UIAlertController,其實(shí)只是一個(gè)魔術(shù)厕吉。

我們?cè)诘桶姹鞠率褂玫腢IAlertController是我們動(dòng)態(tài)添加的酱固,它什么也沒有做,直接繼承了GJAlertController赴涵,而GJAlertController聲明并實(shí)現(xiàn)了和系統(tǒng)UIAlertController一模一樣的一套API媒怯。我們的GJAlertController根本不是一個(gè)VC是一個(gè)NSObject,只是自己用UIAlertView和UIActionSheet封裝成了UIAlertController的API罷了髓窜,到這里你應(yīng)該對(duì)所有的一切都明白了吧扇苞。

我之所以要寫這篇文章,主要是在欣賞:

Classclass=objc_allocateClassPair([GJAlertControllerclass],"UIAlertController",0);

這段代碼的美麗與魅力寄纵,表達(dá)我對(duì)這段代碼鳖敷,及其想到這樣使用這段代碼的人的敬佩當(dāng)然其實(shí)用其他的runtime函數(shù)在這里也也可以做相同的事情,具體看我剛剛發(fā)的那個(gè)老外的鏈接程拭。

幾點(diǎn)說明:

1.為什么要使用匯編定踱?

因?yàn)樵趯ふ覕?shù)據(jù)段上OBJC_CLASS$_ClassName標(biāo)簽的時(shí)候不支持C、C++恃鞋、OC等高級(jí)語言崖媚,只能用匯編。

2.代碼中出現(xiàn)的OBJC_CLASS$_UIAlertController與_OBJC_CLASS_UIAlertController有什么關(guān)系恤浪?

沒有任何關(guān)系畅哑,OBJC_CLASS$_UIAlertController這個(gè)是系統(tǒng)中類標(biāo)簽的格式,必須是這樣子才可以水由,而_OBJC_CLASS_UIAlertController只是一個(gè)參數(shù)名荠呐,你叫hellworld也可以(已經(jīng)測(cè)試過可以),大家不要被它倆弄暈了砂客,_OBJC_CLASS_UIAlertController這個(gè)寫法只是約定俗稱的寫法泥张,就像我們?cè)贕CD中用到的onceToken一樣,沒多大意義鞠值。

3.文中我用了老外一詞媚创,給人的感覺像是帶有些許輕蔑和嘲諷的口氣,我這里正式聲明彤恶,其實(shí)并沒有此意筝野,我們應(yīng)該理解為國外友人晌姚,相反,正是他們無私的分享才使得開源具有很大的意義歇竟,給人類進(jìn)步做出了巨大貢獻(xiàn)挥唠,在這里也對(duì)一切做出分享、開源的朋友們加以感謝焕议。

后記

這里可能我表達(dá)不是很清晰宝磨,我們使用GJAlertController來使得UIAlertController兼容低版本其實(shí)是基于Xcode的baseSDK的,就是Xcode編譯的SDK盅安,我們要使用UIAlertController唤锉,那我們編譯的SDK肯定要大于等于iOS8.0,否則都沒有UIAlertController這個(gè)類别瞭,當(dāng)我們baseSDK的版本大于等于iOS8的時(shí)候能夠確定有這個(gè)類窿祥,我們基于這個(gè)baseSDK打包出來的app如果運(yùn)行在低版本中的時(shí)候就是有代表UIAlertController這個(gè)類的標(biāo)簽,但是沒有值蝙寨,也就是沒有類晒衩,因?yàn)槭堑陀趇OS8的系統(tǒng),這時(shí)我們才執(zhí)行上面說的邏輯墙歪,正式由于基于baseSDK听系,所以編譯時(shí)不會(huì)報(bào)警告的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末虹菲,一起剝皮案震驚了整個(gè)濱河市靠胜,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌毕源,老刑警劉巖浪漠,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異霎褐,居然都是意外死亡缰雇,警方通過查閱死者的電腦和手機(jī)茬高,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門壶栋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來沫屡,“玉大人拌牲,你說我怎么就攤上這事俱饿。” “怎么了塌忽?”我有些...
    開封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵拍埠,是天一觀的道長。 經(jīng)常有香客問我土居,道長枣购,這世上最難降的妖魔是什么嬉探? 我笑而不...
    開封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮棉圈,結(jié)果婚禮上涩堤,老公的妹妹穿的比我還像新娘。我一直安慰自己分瘾,他們只是感情好胎围,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著德召,像睡著了一般白魂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上上岗,一...
    開封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天福荸,我揣著相機(jī)與錄音,去河邊找鬼肴掷。 笑死敬锐,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的捆等。 我是一名探鬼主播滞造,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼栋烤!你這毒婦竟也來了谒养?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤明郭,失蹤者是張志新(化名)和其女友劉穎买窟,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體薯定,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡始绍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了话侄。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片亏推。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖年堆,靈堂內(nèi)的尸體忽然破棺而出吞杭,到底是詐尸還是另有隱情,我是刑警寧澤变丧,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布芽狗,位于F島的核電站,受9級(jí)特大地震影響痒蓬,放射性物質(zhì)發(fā)生泄漏童擎。R本人自食惡果不足惜滴劲,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望顾复。 院中可真熱鬧班挖,春花似錦、人聲如沸捕透。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽乙嘀。三九已至末购,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間虎谢,已是汗流浹背盟榴。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留婴噩,地道東北人擎场。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像几莽,于是被迫代替她去往敵國和親迅办。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

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