任意方法的Swizzle的應(yīng)用之一AOP(續(xù))

本篇是是上篇的續(xù)作,請先看上篇。
http://www.reibang.com/p/0eb7238326f5

上一篇博客介紹了如何使用內(nèi)聯(lián)匯編給任意方法添加AOP切面,然后給了一個實現(xiàn),文末曾提到最多僅支持6個顯式(self,_cmd會占用前兩個寄存器)非浮點的參數(shù)和8個浮點參數(shù)梧喷。雖然絕大部分情況下都是少于6個參數(shù)的,但是超過6個情況還是時有發(fā)生。以上只是其一铺敌,其二是對包含匿名參數(shù)的函數(shù)的支持汇歹,匿名參數(shù)都是存儲在棧上的,比如:stringWithFormat:偿凭,第一個參數(shù)以后的那些匿名參數(shù)产弹,因此還是需要提供對更多參數(shù)的支持。

如果對ARM64參數(shù)傳遞規(guī)則了解的就知道弯囊,大于8個參數(shù)會通過棧來傳遞痰哨。我大致畫一個圖就容易理解了。

棧參存儲示意圖

當前sp在最底下匾嘱,x29到sp范圍是當前調(diào)用函數(shù)所需要的所有暫存的空間斤斧,x29往上16個Byte保存的是上一次x29,30,當前函數(shù)調(diào)用完后恢復(fù)x29,30使用霎烙。再往上是上一次sp的地址撬讽,其存儲的就是額外的參數(shù)。所以當前函數(shù)在調(diào)用的時候會去該位置(x29+0x10)獲取額外的參數(shù)悬垃。

了解了這個再看看遇到的情況游昼,原始調(diào)用函數(shù)A調(diào)用函數(shù)B在加入Swizzle的情況變成了A->Swizzle->...->B,所以此時函數(shù)B到同樣的位置拿到的數(shù)據(jù)就不正確了尝蠕。怎么辦呢烘豌?一種是讓B到A存參數(shù)的位置讀取,這明顯行不通看彼,畢竟到了運行時廊佩,B代碼已經(jīng)編譯完成,是固定的靖榕,正常方法無法改變标锄,Swizzle之后B無法知道A存參數(shù)的位置。第二種將A的放置的參數(shù)再拷貝一份到B需要的位置序矩,換句話說就是建立一個偽棧。大致的思路是有了跋破,但可行性呢簸淀?

眾所周知sp寄存器的重要性,我們在函數(shù)中定義的臨時變量一般都需要sp+偏移量來讀取寫入毒返,一旦隨意改變租幕,后果可想而知,所以我們必須在偽棧調(diào)用完后立即還原sp指針拧簸,而且存參數(shù)的位置也需要傳遞過來劲绪。解決了這個問題,還有第二個問題就是棧上到底存了多少參數(shù),不需要知道其如何存儲贾富,只需要知道其大小歉眷。

NSMethodSignature的frameLength方法可以獲取總共參數(shù)大小,然后我從二進制源碼中發(fā)現(xiàn)frameLength-0xe0才是棧參大小颤枪。NSMethodSignature可以根據(jù)簽名字符串構(gòu)建汗捡,字符串可以從Method中獲取,但頻繁創(chuàng)建NSMethodSignature對象還是開銷較大畏纲,這里我將其和Class扇住,selector關(guān)聯(lián)起來緩存。OK盗胀,原理大致如此艘蹋。

ZWFrameLength

先實現(xiàn)frameLength獲取

/*  0xe0是基礎(chǔ)大小,其中包含9個寄存器共0x48票灰,8浮點寄存器共0x80女阀,還有0x18是額外信息,比如frameLength,
    超過0xe0的部分為棧參數(shù)大小
 */
int ZWFrameLength(void **sp) {
    id obj = (__bridge id)(*sp);
    SEL sel = *(sp + 1);
    Class class = object_getClass(obj);
    if (!class || !sel) return 0xe0;
    
    [_ZWLock lock];
    NSMutableDictionary *methodSigns = _ZWAllSigns[NSStringFromClass(class)];
    [_ZWLock unlock];
    NSString *selName = class_isMetaClass(class) ? ZWGetMetaSelName(sel) : NSStringFromSelector(sel);
    NSMethodSignature *sign = methodSigns[selName];
    if (sign) {
        return (int)[sign frameLength];
    }
    
    Method method = class_isMetaClass(class) ? class_getClassMethod(class, sel) : class_getInstanceMethod(class, sel);
    const char *type = method_getTypeEncoding(method);
    sign = [NSMethodSignature signatureWithObjCTypes:type];
    [_ZWLock lock];
    if (!methodSigns) {
        _ZWAllSigns[NSStringFromClass(class)] = [NSMutableDictionary dictionaryWithObject:sign forKey:selName];
    } else {
        methodSigns[selName] = sign;
    }
    [_ZWLock unlock];
    return (int)[sign frameLength];
}

函數(shù)實現(xiàn)還是比較簡單的米间,class_getInstanceMethod調(diào)用强品,NSMethodSignature對象創(chuàng)建還是有一定開銷的,特別是frameLength會被頻繁調(diào)用屈糊,所以還是需要緩存一下的榛。這里type作為Key來緩存性能應(yīng)該也不差,特別是內(nèi)存開銷小很多逻锐,type一致夫晌,sign也就一致。但有一點需要注意昧诱,class_getInstanceMethod在類定義的方法少的時候開銷不大晓淀,開銷較大的情況是Swizzle了大量父類的方法,特別是蘋果提供的父類方法較多盏档,所以如果使用這種方式Swizzle需要指明方法實現(xiàn)所在的類凶掰,可以減少循環(huán)。我這里空間換時間蜈亩,按class和selector來映射懦窘。lock前后使用了兩次,主要是為了減少臨界區(qū)的大小稚配,可以提高效率畅涂。

構(gòu)造偽棧

接下來講調(diào)用時候如何構(gòu)造偽棧和注意事項

void ZWAopInvocation(void **sp, NSDictionary *Invocation, ZWInvocationOption option) {
    id obj = (__bridge id)(*sp);
    SEL sel = *(sp + 1);
    if (!obj || !sel) return;
    NSInteger count = ZWGetInvocationCount(Invocation, obj, sel);
    __autoreleasing NSArray *arr = @[obj, [NSValue valueWithPointer:sel], @(option)];
    
    NSInteger frameLenth = ZWFrameLength(sp) - 0xe0;
    for (int i = 0; i < count; ++i) {
        ZWGetAopImp(Invocation, obj, sel, i);
        asm volatile("cbz    x0, LZW_20181107");
        asm volatile("mov    x17, x0");
        asm volatile("ldr    x14, %0": "=m"(arr));
        asm volatile("ldr    x11, %0": "=m"(sp));
        asm volatile("ldr    x13, %0": "=m"(frameLenth));
        asm volatile("cbz    x13, LZW_20181110");
        asm volatile("add    x12, x11, 0xc0");
        
        asm volatile("sub    sp, sp, x13");//增長sp
        asm volatile("bl     _ZWCopyParams");
        asm volatile("LZW_20181110:");
        asm volatile("bl     _ZWLoadParams");
        asm volatile("mov    x1, x14");
        asm volatile("blr    x17");
        asm volatile("sub    sp, x29, 0x1e0");//恢復(fù)sp
        asm volatile("LZW_20181107:");
    }
}

相比較于上篇博客的版本,函數(shù)上半部分是一樣的道川,這里需要調(diào)用frameLength獲取棧幀大小午衰,這里使用NSInteger存儲立宜,其和寄存器大小一致,方便操作臊岸。

新增代碼讀取frameLength-0xe0的值到x13橙数,x12=x11(sp的值)+0xc0。其中0xc0=0xb0+0x10扇单,而0xb0為ZWGlobalOCSwizzle中棧的大小商模。如果x13=0,則直接跳到LZW_20181110蜘澜,否則順序執(zhí)行施流。

接下來就是比較危險的操作sub sp, sp, x13,將sp增長x13的長度鄙信,新增的空間就是我之前說的偽棧瞪醋,此時,在sp未恢復(fù)之前装诡,之前定義的C變量frameLenth银受,sp,arr等全部失效了鸦采,所以不要再使用這些變量了宾巍,當然實在是需要可以強行通過x29+偏移量來讀取,只不過比較麻煩渔伯,需要知道這些臨時變量的具體位置顶霞,再計算相對于x29的偏移量。

跳轉(zhuǎn)ZWCopyParams锣吼,其作用是將x12指向的棧中參數(shù)全部復(fù)制到偽棧上选浑。

不管是否調(diào)用ZWCopyParams都需要調(diào)用ZWLoadParams,加載所有的寄存器參數(shù)玄叠。

blr x17跳轉(zhuǎn)x17古徒,調(diào)用函數(shù)。

恢復(fù)sp读恃,這個時候x29寄存器還是有效的隧膘, 所有可以根據(jù)它恢復(fù)sp。

注意

恢復(fù)sp時寺惫,偏移量0x1e0怎么來的疹吃?將代碼全部匯編,去_ZWAopInvocation入口處找肌蜻,看其開始將sp調(diào)整的大小互墓,需要注意的是編譯器開啟優(yōu)化后這個值可能會改變必尼,比如我這里Xcode10.1開啟Os級優(yōu)化會變成0xa0蒋搜,不同的版本的編譯器篡撵,不同的優(yōu)化等級可能該值都不一樣,這個是我比較頭疼的地方豆挽,該函數(shù)稍有改動就需要處理這個問題育谬,所以這個庫最好在指定的環(huán)境下編譯成.a再使用(不開啟優(yōu)化生成.a庫性能也不差,另外不知道為什么函數(shù)級別的優(yōu)化關(guān)閉選項__attribute__((optnone))無效帮哈,不然可以關(guān)閉Xcode對該函數(shù)的優(yōu)化)膛檀,實在不行可以通過Debug和Release環(huán)境來區(qū)分該值。我再想想看有沒有別的辦法解決這個問題娘侍,人不能被問題憋死咖刃,不要撞死胡同,總是有各種招可以解決的憾筏。

ZWCopyParams

OS_ALWAYS_INLINE void ZWCopyParams(void) {
    //x12=原始棧參數(shù)地址嚎杨,x13=frameLength-0x0e0
    asm volatile("mov    x15, sp");
    asm volatile("LZW_20181108:");
    asm volatile("cbz    x13, LZW_20181109");
    asm volatile("ldr    x0, [x12]");
    asm volatile("str    x0, [x15]");
    asm volatile("add    x15, x15, #0x8");
    asm volatile("add    x12, x12, #0x8");
    asm volatile("sub    x13, x13, #0x8");
    asm volatile("cbnz   x13, LZW_20181108");
    asm volatile("LZW_20181109:");
}

本函數(shù)接收x12,x13作為參數(shù)氧腰,將x12指向的棧中參數(shù)依次復(fù)制到偽棧枫浙,比較簡單就不具體講解了。

這里需要注意的是古拴,寄存器的選擇箩帚。這里我使用的是x9到x15這些寄存器,這些屬于易失性寄存器黄痪,簡單來說就是臨時暫存紧帕,很容易被修改,不可靠满力。使用它們的好處是不用考慮將其之前存的數(shù)據(jù)轉(zhuǎn)存到內(nèi)容焕参,使用完后一般不需要恢復(fù)原數(shù)據(jù)。

x16-x17是調(diào)用暫存的油额,比如我這里就將函數(shù)入口存入x17叠纷,然后通過blr命令跳轉(zhuǎn)的。x18是平臺保留寄存器潦嘶,一般用不上涩嚣。

函數(shù)A{
   返回值C = 函數(shù)B//存儲在x19
   函數(shù)D
   函數(shù)E(C)//mov x0, x19
}

函數(shù)B{}
函數(shù)D{
    使用x19之前需要將x19存儲在內(nèi)存中,之后再恢復(fù)
}
函數(shù)E(C){}

x19-x28是調(diào)用上下文暫存數(shù)據(jù)的掂僵,其是非易失性航厚,具體來講就是在函數(shù)若干范圍內(nèi)的,例如:我在函數(shù)A開始處調(diào)用了函數(shù)B獲得了一個返回值C锰蓬,函數(shù)A實現(xiàn)代碼體接下來需要多次使用C幔睬,就可以將C存在x19-x28中,存在棧上也是可以的芹扭,但效率較差麻顶。然而如果在函數(shù)A的某處調(diào)用了函數(shù)D赦抖,D中也使用x19-x28中相同的寄存器,這就需要先暫存之前數(shù)據(jù)辅肾,之后再恢復(fù)队萤,比較麻煩。

對于OC這類的動態(tài)調(diào)用語言矫钓,調(diào)用關(guān)系不固定的情況下要尔,最好不要使用x0-x8,q0-q7之外的寄存器傳參新娜。我這里之所以使用x11赵辕,x12,x13這些易失性寄存器傳參是因為調(diào)用關(guān)系簡單且固定概龄,調(diào)用上下文也不會有其他操作來破壞其內(nèi)容(什么系統(tǒng)中斷啥的匆帚,就不用我們操心了,其會保存上下文并恢復(fù)的)旁钧,同時省下暫存原數(shù)據(jù)的操作吸重。

ZWInvocation

void ZWInvocation(void **sp) {
    __autoreleasing id obj;
    SEL sel;
    void *obj_p = &obj;
    void *sel_p = &sel;
    NSInteger frameLenth = ZWFrameLength(sp)- 0xe0;

    asm volatile("ldr    x11, %0": "=m"(sp));
    asm volatile("ldr    x10, %0": "=m"(obj_p));
    asm volatile("ldr    x0, [x11]");
    asm volatile("str    x0, [x10]");
    asm volatile("ldr    x10, %0": "=m"(sel_p));
    asm volatile("ldr    x0, [x11, #0x8]");
    asm volatile("str    x0, [x10]");
    
    asm volatile("ldr    x11, %0": "=m"(sp));
    asm volatile("ldr    x0, [x11]");
    asm volatile("ldr    x1, [x11, #0x8]");
    asm volatile("bl     _ZWGetOriginImp");
    asm volatile("cbnz   x0, LZW_20181105");
    
    __autoreleasing NSArray *arr = @[obj, [NSValue valueWithPointer:sel], @(ZWInvocationOptionReplace)];

    asm volatile("ldr    x11, %0": "=m"(sp));
    asm volatile("ldr    x0, [x11]");
    asm volatile("ldr    x1, [x11, #0x8]");
    asm volatile("bl     _ZWGetCurrentImp");
    asm volatile("cbz    x0, LZW_20181106");
    
    asm volatile("mov    x17, x0");
    asm volatile("ldr    x14, %0": "=m"(arr));
    asm volatile("ldr    x11, %0": "=m"(sp));
    asm volatile("ldr    x13, %0": "=m"(frameLenth));
    asm volatile("cbz    x13, LZW_20181111");
    asm volatile("add    x12, x11, 0xc0");//0xb0 + 0x10
    
    asm volatile("sub    sp, sp, x13");
    asm volatile("bl     _ZWCopyParams");
    asm volatile("LZW_20181111:");
    asm volatile("bl     _ZWLoadParams");
    asm volatile("mov    x1, x14");
    asm volatile("blr    x17");
    asm volatile("sub    sp, x29, 0x70");
    asm volatile("b      LZW_20181106");
   
    asm volatile("LZW_20181105:");
    asm volatile("mov    x17, x0");
    asm volatile("ldr    x11, %0": "=m"(sp));
    asm volatile("ldr    x13, %0": "=m"(frameLenth));
    asm volatile("cbz    x13, LZW_20181112");
    asm volatile("add    x12, x11, 0xc0");
    asm volatile("sub    sp, sp, x13");//增長sp
    asm volatile("bl     _ZWCopyParams");
    asm volatile("LZW_20181112:");
    asm volatile("bl     _ZWLoadParams");
    asm volatile("blr    x17");
    asm volatile("sub    sp, x29, 0x70");//恢復(fù)sp
    asm volatile("LZW_20181106:");
}

本函數(shù)與ZWAopInvocation改動類似,這里就不作具體講解了歪今。

給出測試

- (void)viewDidLoad {    
    ZWAddAop(self, @selector(aMethod2::::::::), ZWInvocationOptionAfter, ^(NSArray *info, NSString *str ,NSString *a2 ,NSString *a3 ,NSString *a4 ,NSString *a5 ,NSString *a6 ,NSString *a7 ,NSString *a8){
        NSLog(@"after2: %@\n%@\n%@\n%@\n%@", str, a5, a6, a7, a8);
    });

    ZWAddAop(self, @selector(aMethod2::::::::), ZWInvocationOptionReplace | ZWInvocationOptionOnly, ^(NSArray *info, NSString *str,NSString *a2 ,NSString *a3 ,NSString *a4 ,NSString *a5 ,NSString *a6 ,NSString *a7 ,NSString *a8){
        NSLog(@"replace2 | after2: %@\n%@\n%@\n%@\n%@", str, a5, a6, a7, a8);
    });
    
    ZWAddAop(self, @selector(aMethod4::::::::), ZWInvocationOptionAfter, ^int (NSArray *info,NSInteger str, NSInteger a2,  NSInteger a3, NSInteger a4, NSInteger a5, NSInteger a6, NSInteger a7, NSInteger a8){
        NSLog(@"after43: %ld %ld %ld %ld %ld",str, a5, a6, a7, a8);
        return 11034;
    });
    
    
    [self aMethod2:@"test str" :@"this is a test" :@"this is a test":@"this is a test":@"this is a test":@"this is a test":@"this is a test a7":@"this is a test a8"];
    [self aMethod4:1 :2 :3 :4 :5 :6 :7 :8];
}   
- (void)aMethod2:(NSString *)str :(NSString *)a2 :(NSString *)a3 :(NSString *)a4 :(NSString *)a5 :(NSString *)a6 :(NSString *)a7 :(NSString *)a8 {
    NSLog(@"method2: %@\n%@\n%@\n%@\n%@", str, a5, a6, a7, a8);
}
- (void)aMethod4:(NSInteger)str :(NSInteger)a2 :(NSInteger)a3 :(NSInteger)a4 :(NSInteger)a5 :(NSInteger)a6 :(NSInteger)a7 :(NSInteger)a8 {
    NSLog(@"method4: %ld %ld %ld %ld %ld",str, a5, a6, a7, a8);
}
2018-11-23 14:54:56.943007+0800 DEMO[12814:3984030] replace2 | after2: test str
this is a test
this is a test
this is a test a7
this is a test a8
2018-11-23 14:54:56.943074+0800 DEMO[12814:3984030] after2: test str
this is a test
this is a test
this is a test a7
this is a test a8
2018-11-23 14:54:56.943584+0800 DEMO[12814:3984030] method4: 1 5 6 7 8
2018-11-23 14:54:56.943607+0800 DEMO[12814:3984030] after43: 1 5 6 7 8

總結(jié)

之所以費力的做AOP嚎幸,是因為這貨確實好用。例如:debug寄猩,日志輸出嫉晶,代碼優(yōu)化,非侵入式埋點田篇,網(wǎng)絡(luò)接口記錄等等替废。之后如果有時間我會將代碼再優(yōu)化,增加些必要功能泊柬,提高其效率和可靠性椎镣。

最后聊幾句:使用內(nèi)聯(lián)匯編,可以結(jié)合高級語言和低級語言兩者的優(yōu)點兽赁,低級語言完成高級語言無法完成的工作状答,提高效率,高級語言則減少開發(fā)難度刀崖,從這個角度看確實是很棒惊科。
呵呵,一切聽上去美好的故事總是有曲折可怖的過程亮钦。兩者混合使用馆截,各自的缺點也就糅在一起了。匯編書寫易出錯蜂莉,C/OC會生成額外的復(fù)雜操作蜡娶,稍微操作不當堪唐,程序就Crash了,一臉懵逼翎蹈,而且錯誤難以排查,如果要增加功能男公,修改代碼也比較麻煩荤堪,很多東西都是寫死的(不寫死就意味著更多的工作量,更復(fù)雜的代碼邏輯枢赔,同時內(nèi)聯(lián)匯編不支持匯編宏澄阳,當然宏還是支持的,這倒是可以簡化不少代碼)踏拜。所以一般只有在必須使用的時候才使用碎赢,比如極致的效率優(yōu)化,完成后不怎么修改的庫速梗。隨隨便便嵌入?yún)R編肮塞,就是自己給自己挖坑,當然要是有特殊用途??????姻锁,比如吹牛逼枕赵,整人啥的,就多多益善了位隶。因此內(nèi)聯(lián)匯編謹慎使用拷窜,書寫的時候要遵循兩者的機制,如果不知道C/OC背地里偷偷干了什么就將其匯編排查涧黄,這可以解決很多問題篮昧。

Github源碼地址

性能高度優(yōu)化版
Github源碼地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市笋妥,隨后出現(xiàn)的幾起案子懊昨,更是在濱河造成了極大的恐慌,老刑警劉巖春宣,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件疚颊,死亡現(xiàn)場離奇詭異,居然都是意外死亡信认,警方通過查閱死者的電腦和手機材义,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來嫁赏,“玉大人其掂,你說我怎么就攤上這事×视” “怎么了款熬?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵深寥,是天一觀的道長。 經(jīng)常有香客問我贤牛,道長惋鹅,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任殉簸,我火速辦了婚禮闰集,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘般卑。我一直安慰自己武鲁,他們只是感情好,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布蝠检。 她就那樣靜靜地躺著沐鼠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪叹谁。 梳的紋絲不亂的頭發(fā)上饲梭,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天,我揣著相機與錄音焰檩,去河邊找鬼排拷。 笑死,一個胖子當著我的面吹牛锅尘,可吹牛的內(nèi)容都是我干的监氢。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼藤违,長吁一口氣:“原來是場噩夢啊……” “哼浪腐!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起顿乒,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤议街,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后璧榄,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體特漩,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年骨杂,在試婚紗的時候發(fā)現(xiàn)自己被綠了涂身。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡搓蚪,死狀恐怖蛤售,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤悴能,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布揣钦,位于F島的核電站,受9級特大地震影響漠酿,放射性物質(zhì)發(fā)生泄漏冯凹。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一炒嘲、第九天 我趴在偏房一處隱蔽的房頂上張望宇姚。 院中可真熱鬧,春花似錦摸吠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至紊选,卻和暖如春啼止,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背兵罢。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工献烦, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人卖词。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓巩那,卻偏偏與公主長得像,于是被迫代替她去往敵國和親此蜈。 傳聞我的和親對象是個殘疾皇子即横,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354

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