OC底層原理07--Runtime以及objc_msgSend分析(一)

一尘吗、感受運行時

什么是runtime?

為OC提供運行機制浇坐,用C/C++寫成的摇予,通過底層的API、OC 源碼吗跋、調(diào)用方法、基礎語法、framework跌宛、service接口等為OC層面提供的運行制機制酗宋。
它也是為面向?qū)ο螅∣OP)提供運行時機制;在運行過程中疆拘,讓對象找到真正的執(zhí)行邏輯蜕猫,包括內(nèi)存布局(isa的走位指向)。

再來理解下Apple的
從編譯時間和鏈接時間到運行時哎迄,Objective-C語言會盡可能多地推遲決策回右。 只要有可能,它都會動態(tài)執(zhí)行操作漱挚,例如創(chuàng)建對象和確定要調(diào)用的方法翔烁。 因此,該語言不僅需要編譯器旨涝,還需要運行時系統(tǒng)來執(zhí)行編譯后的代碼蹬屹。 運行時系統(tǒng)充當Objective-C語言的一種操作系統(tǒng)。 這就是使語言有效的原因白华。 但是慨默,一般情況下,您不需要直接與運行時進行交互弧腥。--Apple

  • 與Runtime交互的三種方式
    1.通過Objective-C源代碼厦取;--[book write]
    2.通過Framework & Service的類中定義的方法;--[[Book class] isKindOfClass:[NSObject class]]
    3.通過直接調(diào)用運行時函數(shù)管搪。--objc_msgSendSuper虾攻、sel_registerName
Runtime與OC底層架構關系

Compiler是編譯器,即LLVM抛蚤。比如OC層面的alloc在LLVM的實現(xiàn)就是objc_alloc.

方法的本質(zhì)

利用clang指令編譯main.m文件得到main.cpp台谢,在之前的OC底層原理03— isa探究中稍稍介紹過獲取C++的Clang指令:

// 模擬器sdk路徑替換自己的即可
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-12.0.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.0.sdk ViewController.m

定義一個類Book,編寫兩個方法read岁经,write 朋沮;其中read實現(xiàn),write不實現(xiàn)

Book * book = [Book alloc];
[book read];

執(zhí)行之后缀壤,在main.cpp中找尋read,write方法樊拓。

// main.cpp
Book * book = ((Book *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Book"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)book, sel_registerName("read"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)book, sel_registerName("write"));

通過以上,可以知道底層會將方法編譯供 objc_msgSend調(diào)用塘慕,這就是方法的本質(zhì):消息的發(fā)送(objc_msgSend)筋夏。
既然這樣我們就可以通過直接調(diào)用底層的objc_msgSend來調(diào)用方法;我們先導入頭文件#import <objc/message.h>;為了不報錯图呢,我們在target —— Build Setting —— 搜索msgSend, 把 enable strict checking of obc_msgSend calls由YES 改為NO条篷,將嚴厲的檢查機制關掉骗随。

調(diào)用方法:NSSeletorFromString() (OC層) = @seletor()(OC層)= sel_registerName(runtime)

在調(diào)用Book類方法后調(diào)用objc_msgSend(book,sel_registerName("read"));得到以下的結果


再一次驗證方法的本質(zhì)是通過消息的發(fā)送。

對象方法是否能執(zhí)行父類的實現(xiàn)

定義兩個類:Book 和 English 赴叹,Book中實現(xiàn)read方法鸿染,English中實現(xiàn)write方法

@interface Book : NSObject
-(void)read;
@end
@implementation Book
- (void)read{
    NSLog(@"read some book");
}
@end

@interface English : Book
-(void)write;
@end
@implementation English
-(void)write{
    NSLog(@"write A B C");
}
@end

通過調(diào)用以下:

        English * english = [English alloc];
        [english read];
        
        struct objc_super mysuper;
        mysuper.receiver = English;
        mysuper.super_class = [Book class];
        
        objc_msgSendSuper(&mysuper, sel_registerName("read"));

執(zhí)行結果:


子類調(diào)用父類方法,這很好理解乞巧。消息的發(fā)送流程中涨椒,消息的接收是english,但是具體的實現(xiàn)绽媒,可以執(zhí)行父類Book中的read實現(xiàn)蚕冬。
objc_msgSendSuper方法中有兩個參數(shù)(struct objc_super *,SEL)是辕,其結構體類型是objc_super定義的結構體對象囤热,且需要指定receiver 和 super_class兩個屬性,源碼實現(xiàn) & 定義如下

/** 
 * 將具有簡單返回值的消息發(fā)送到類實例的父類免糕。
 * @param super A pointer to an \c objc_super data structure. Pass values identifying the
 *  context the message was sent to, including the instance of the class that is to receive the
 *  message and the superclass at which to start searching for the method implementation.
 * @param op A pointer of type SEL. Pass the selector of the method that will handle the message.
 * @param ...
 *   A variable argument list containing the arguments to the method.
 * 
 * @return The return value of the method identified by \e op.
 * 
 * @see objc_msgSend
 */
OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
#endif

objc_super定義是:

/// Specifies the superclass of an instance. 
struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};
#endif

由上面印證了赢乓,對象方法能執(zhí)行父類的實現(xiàn)
但是其底層是怎么找的呢石窑?
基本邏輯是:OC 方法是通過消息中的sel(方法編號)牌芋,找到函數(shù)指針imp,再找到底層匯編執(zhí)行其內(nèi)容松逊。
消息接收流程:對象 ->isa -> 方法or類 -> cache_t -> methodlist

objc_msgSend 的匯編流程

objc_msgSend 底層是使用匯編構造的躺屁。
因為匯編特性:1)編譯速度快 ;2)參數(shù)的動態(tài)性经宏,易調(diào)整犀暑。

匯編小補

LSR:邏輯右移
add 加法
b.le :判斷上面cmp的值是小于等于執(zhí)行標號,否則直接往下走
b.eq 等于 執(zhí)行地址 否則往下
cmp a,b 比較a與b
mov a,b 把b的值送給a
ret 返回主程序
nop無作用,英文“no operation”的簡寫烁兰,意思是“do nothing”(機器碼90)
call 調(diào)用子程序
je 或jz 若相等則跳(機器碼74 或0F84)
jne或jnz 若不相等則跳(機器碼75或0F85)
jmp 無條件跳(機器碼EB)
jb 若小于則跳
ja 若大于則跳
jg 若大于則跳
jge 若大于等于則跳
jl 若小于則跳
jle 若小于等于則跳
ldr w10 ,[sp] w10 = sp棧內(nèi)存中的值
pop 出棧
push 壓棧

快速查找流程

781源碼中搜索objc_msgSend,匯編代碼就要找.s文件耐亏,找到objc-msg-arm64.s,以下是主要的匯編代碼:

    // 消息發(fā)送:objc_msgSend的匯編入口,獲取receiver的isa
    ENTRY _objc_msgSend 

    UNWIND _objc_msgSend, NoFrame 
    
    // p0與空做對比沪斟,判斷receiver是否存在广辰,p0為objc_msgSend的第一個參數(shù)(消息接收者receiver,第二個參數(shù)是_cmd)
    cmp p0, #0          // 判空檢查和標記指針檢查
// 是否支持taggedpointers對象
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        
#else
    // p0 等于 0 時主之,直接返回 空
    b.eq    LReturnZero 
#endif 
   // p0即receiver 肯定存在的流程
    // 從x0寄存器指向的地址中取出 isa择吊,存入 p13寄存器
    ldr p13, [x0]      
   // 在64位架構下通過 p16 = isa(p13) & ISA_MASK,拿出shiftcls信息槽奕,得到class信息
    GetClassFromIsa_p16 p13     
LGetIsaDone:
    // 調(diào)用imp或objc_msgSend_uncached
    //如果獲取到isa几睛,跳轉(zhuǎn)CacheLookup 執(zhí)行緩存查找流程(sel-imp的快速查找)
    CacheLookup NORMAL, _objc_msgSend

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    //如果為nil,則返回空
    b.eq    LReturnZero     // nil check 

    // tagged
    adrp    x10, _objc_debug_taggedpointer_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
    ubfx    x11, x0, #60, #4
    ldr x16, [x10, x11, LSL #3]
    adrp    x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
    add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
    cmp x10, x16
    b.ne    LGetIsaDone

    // ext tagged
    adrp    x10, _objc_debug_taggedpointer_ext_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
    ubfx    x11, x0, #52, #8
    ldr x16, [x10, x11, LSL #3]
    b   LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

LReturnZero: // 不支持taggedpointer對象粤攒,或者為空所森,就返回空:returnZero
    // x0 is already zero
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    ret
    END_ENTRY _objc_msgSend
  • CacheLookup
    看定義.macro CacheLookup囱持,跳轉(zhuǎn)至LLookupStart$1
.macro CacheLookup
    //重新啟動協(xié)議:
    //一旦我們經(jīng)過LLookupStart $ 1標簽,我們可能已經(jīng)加載了
    //無效的緩存指針或掩碼必峰。
    //
    //調(diào)用task_restartable_ranges_synchronize()時洪唐,
    //(或當有信號到達我們時),直到我們經(jīng)過LLookupEnd$1吼蚁,
    //然后我們的電腦將重置為LLookupRecover $ 1
    //跳轉(zhuǎn)到具有以下內(nèi)容的cache-miss代碼路徑
    // 要求:
    //
    // GETIMP:
    //緩存未命中只是返回NULL(將x0設置為0)
    //
    // NORMAL和LOOKUP:
    //-x0包含接收者
    //-x1包含選擇器
    //-x16包含isa
    //-根據(jù)調(diào)用約定設置其他寄存器
LLookupStart$1:
    // #define CACHE (2 * __SIZEOF_POINTER__),其中 __SIZEOF_POINTER__表示pointer的大小 问欠,即 2*8 = 16
    // p1 = SEL, p16 = isa
    //  x16(即isa)中平移16字節(jié)得到cache肝匆,取出cache 存入p11寄存器; isa(8字節(jié)),superClass(8字節(jié))顺献,cache(mask高16位 + buckets低48位)
    // p11 = mask|buckets
    ldr p11, [x16, #CACHE]              
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16  
// arm64--對應cache_t的HIGH_16位宏
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets = cacahe & 0x0000ffffffffffff   把mask高16位抹零旗国,得到buckets 存入p10寄存器,去掉mask注整,留下buckets
    //  把p11邏輯右移48位得到mask能曾,mask & p1,得到sel-imp的下標index(即搜索下標) 
    //  存入p12(cache insert寫入時的哈希下標計算是 通過 sel & mask,讀取時也需要通過這種方式)
    and p12, p1, p11, LSR #48       // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    and p10, p11, #~0xf         // p10 = buckets
    and p11, p11, #0xf          // p11 = maskShift
    mov p12, #0xffff
    lsr p11, p12, p11               // p11 = mask = 0xffff >> p11
    and p12, p1, p11                // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif
    // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
    // PTRSHIFT: arm64-asm.h 中定義 1 << 3 ,二進制 1 左移3位肿轨,結果為8.
    //  buckets 是一個數(shù)組寿冕,要想獲得其中某個元素值,利用內(nèi)存偏移 椒袍,((_cmd & mask ) << (1 + PTRSHIFT)) = 2 <<  4 =  2 ^4 = 16  驼唱。(_cmd & mask )  為2,內(nèi)存偏移相當于找buckets的第三個元素驹暑。
    //(_cmd & mask )  : 由于mask= ocuupi - 1= 4-1 = 3玫恳,在0,1,2,3 中去取,& 的結果就相當于在取余數(shù)优俘,0,1,2,3
    add p12, p10, p12, LSL #(1+PTRSHIFT)
    // 通過取出p12的bucket結構體得到 * bucket = {imp,sel},p9 存sel京办, p17 存 imp
    ldp p17, p9, [x12]      // {imp, sel} = *bucket
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop
3:  // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
                    // p12 = buckets + (mask << 1+PTRSHIFT)
// p11:cache 右移44位,將結果存入p12帆焕,p12 = 高16位mask + 一個buckets (2中第一個bucket == 最后一個bucket情況惭婿,在下面的情況中依然沒有則該循環(huán)結束)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    add p12, p12, p11, LSL #(1+PTRSHIFT)
                    // p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif

      //當緩存損壞時,克隆掃描循環(huán)會丟失而不是掛起视搏。
      //緩慢的路徑可能會檢測到任何損壞并在以后暫停审孽。

    // 再查找一遍緩存()
    // 拿到x12(即p12)bucket中的 imp-sel 分別存入 p17-p9
    ldp p17, p9, [x12]      // {imp, sel} = *bucket 
    
   // 比較 sel 與 p1(傳入的參數(shù)cmd)
1:  cmp p9, p1          // if (bucket->sel != _cmd) 
   //如果不相等,即走到第二步
    b.ne    2f          //     scan more 
   // 如果相等 即命中浑娜,直接返回imp
    CacheHit $0         // call or return imp  
    
2:  // not hit: p12 = not-hit bucket
   // 如果一直找不到佑力,則CheckMiss
    CheckMiss $0            // miss if bucket->sel == 0 
   // 判斷p12(下標對應的bucket) 是否 等于 p10(buckets數(shù)組第一個元素)-- 表示前面已經(jīng)沒有了,但是還是沒有找到
    cmp p12, p10        // wrap if bucket == buckets 
    b.eq    3f //如果等于筋遭,跳轉(zhuǎn)至第3步
   // 從x12(即p12 buckets首地址)- 實際需要平移的內(nèi)存大小BUCKET_SIZE打颤,得到得到第二個bucket元素暴拄,imp-sel分別存入p17-p9,即向前查找
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket 
   // 跳轉(zhuǎn)至第1步编饺,繼續(xù)對比 sel 與 cmd
    b   1b          // loop 

LLookupEnd$1:
LLookupRecover$1:
3:  // double wrap
   // 跳轉(zhuǎn)至JumpMiss 因為是normal 乖篷,跳轉(zhuǎn)至__objc_msgSend_uncached

    JumpMiss $0 
.endmacro

//以下是最后跳轉(zhuǎn)的匯編函數(shù)
.macro CacheHit
.if $0 == NORMAL
    TailCallCachedImp x17, x12, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP
    mov p0, p17
    cbz p0, 9f          // don't ptrauth a nil imp
    AuthAndResignAsIMP x0, x12, x1, x16 // authenticate imp and re-sign as IMP
9:  ret             // return IMP
.elseif $0 == LOOKUP
    // No nil check for ptrauth: the caller would crash anyway when they
    // jump to a nil IMP. We don't care if that jump also fails ptrauth.
    AuthAndResignAsIMP x17, x12, x1, x16    // authenticate imp and re-sign as IMP
    ret             // return imp via x17
.else
.abort oops
.endif
.endmacro

.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP 
   //如果為GETIMP ,則跳轉(zhuǎn)至 LGetImpMiss
    cbz p9, LGetImpMiss
.elseif $0 == NORMAL 
   // 如果為NORMAL 透且,則跳轉(zhuǎn)至 __objc_msgSend_uncached
    cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP 
    //如果為LOOKUP 撕蔼,則跳轉(zhuǎn)至 __objc_msgLookup_uncached
    cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

.macro JumpMiss
.if $0 == GETIMP
    b   LGetImpMiss
.elseif $0 == NORMAL
    b   __objc_msgSend_uncached
.elseif $0 == LOOKUP
    b   __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

下面是objc_msgSend的匯編流程圖


objc_msgSend匯編流程

總結:Objective-C 方法的實現(xiàn)本質(zhì)上是一個 C 函數(shù),而方法的調(diào)用過程則涉及到消息的傳遞和轉(zhuǎn)發(fā)秽誊。
在 Objective-C 中鲸沮,每個方法都有一個方法選擇器(selector),它是一個指向方法名稱的指針锅论。調(diào)用方法時讼溺,實際上是向?qū)ο蟀l(fā)送一條消息,消息中包含方法選擇器和參數(shù)列表等信息最易。Objective-C 運行時系統(tǒng)會根據(jù)方法選擇器查找對應的方法實現(xiàn)怒坯,然后執(zhí)行該方法。方法實現(xiàn)是一個 C 函數(shù)藻懒,它接收兩個參數(shù)剔猿,分別是方法的接收者和方法選擇器。因此束析,可以說 Objective-C 方法的實現(xiàn)本質(zhì)上是一個 C 函數(shù)艳馒。
但是,方法調(diào)用過程并不僅僅涉及到方法實現(xiàn)的調(diào)用员寇,還包括消息的傳遞和轉(zhuǎn)發(fā)過程弄慰。如果對象無法響應某個消息,Objective-C 運行時系統(tǒng)會嘗試將消息轉(zhuǎn)發(fā)給其他對象蝶锋。這種消息傳遞和轉(zhuǎn)發(fā)的機制是 Objective-C 方法的重要特性之一陆爽,它允許在運行時動態(tài)地修改方法的實現(xiàn),以及在多態(tài)和繼承等方面提供靈活性和可擴展性扳缕。因此慌闭,可以說 Objective-C 方法的本質(zhì)不僅是一個 C 語言函數(shù),還涉及到消息傳遞和轉(zhuǎn)發(fā)的機制躯舔。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末驴剔,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子粥庄,更是在濱河造成了極大的恐慌丧失,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件惜互,死亡現(xiàn)場離奇詭異布讹,居然都是意外死亡琳拭,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進店門描验,熙熙樓的掌柜王于貴愁眉苦臉地迎上來白嘁,“玉大人,你說我怎么就攤上這事膘流⌒趺澹” “怎么了?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵呼股,是天一觀的道長盟蚣。 經(jīng)常有香客問我,道長卖怜,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任阐枣,我火速辦了婚禮马靠,結果婚禮上,老公的妹妹穿的比我還像新娘蔼两。我一直安慰自己甩鳄,他們只是感情好,可當我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布额划。 她就那樣靜靜地躺著妙啃,像睡著了一般。 火紅的嫁衣襯著肌膚如雪俊戳。 梳的紋絲不亂的頭發(fā)上揖赴,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天,我揣著相機與錄音抑胎,去河邊找鬼燥滑。 笑死,一個胖子當著我的面吹牛阿逃,可吹牛的內(nèi)容都是我干的铭拧。 我是一名探鬼主播,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼恃锉,長吁一口氣:“原來是場噩夢啊……” “哼搀菩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起破托,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤肪跋,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后炼团,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體澎嚣,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡疏尿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了易桃。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片褥琐。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖晤郑,靈堂內(nèi)的尸體忽然破棺而出敌呈,到底是詐尸還是另有隱情,我是刑警寧澤造寝,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布磕洪,位于F島的核電站,受9級特大地震影響诫龙,放射性物質(zhì)發(fā)生泄漏析显。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一签赃、第九天 我趴在偏房一處隱蔽的房頂上張望谷异。 院中可真熱鬧,春花似錦锦聊、人聲如沸歹嘹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽尺上。三九已至,卻和暖如春圆到,著一層夾襖步出監(jiān)牢的瞬間怎抛,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工构资, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留抽诉,地道東北人。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓吐绵,卻偏偏與公主長得像迹淌,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子己单,可洞房花燭夜當晚...
    茶點故事閱讀 44,843評論 2 354