iOS源碼解析:runtime<二> objc_msgSend()消息機(jī)制的完整過程

iOS源碼解析:runtime<一> isa,class底層結(jié)構(gòu)窺探

iOS方法調(diào)用的過程我們都很清楚,比如下面這個方法調(diào)用:

[person test];

這個方法調(diào)用過程是首先通過person對象的isa指針找到Person類的類對象,由于實(shí)例方法存儲在類對象中,所以我們就去Person類對象中查找這個test方法如果找到了那就拿來調(diào)用,如果沒有找到,那就通過Person類對象的superclass指針找到Person類的父類的類對象,去這里查找這個test,如果還沒找到則繼續(xù)沿著繼承鏈往上找,如果最終還是沒有找到就會報(bào)unrecognized selector sent to instance 0x60000001b830這個經(jīng)典的錯誤。

這樣回答方法的調(diào)用過程也沒有問題,但是顯得淺顯了一些,還不足以應(yīng)付面試。下面我們就一起探討一下iOS中方法的調(diào)用過程。
首先把[person test]轉(zhuǎn)化為c++的源碼:

((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("test"));

化簡一下:

objc_msgSend(person, sel_registerName("test"));

sel_registerName()我在上一篇文章已經(jīng)說過,它就是傳入方法名,返回SEL,sel_registerName("test")就等價于@selector(test)這句代碼就是給消息接收者發(fā)送SEL消息,所以接下來的問題就變成了去探究objc_msgSend()這個函數(shù)的調(diào)用過程。

objc_msgSend()的執(zhí)行流程可以分為三個階段
  • 消息發(fā)送
  • 動態(tài)方法解析
  • 消息轉(zhuǎn)發(fā)

下面通過源碼逐一分析。
首先我們在runtime的源碼中搜索objc_msgSend,我們發(fā)現(xiàn)搜索的結(jié)非常多,那我們要找的是它的實(shí)現(xiàn),最終我們在objc-msg-arm64.s這樣一個匯編文件中找到objc_msgSend()的實(shí)現(xiàn)。runtime的源碼基本都是由c,c++,匯編語言組成,并且很多經(jīng)常使用的都是由匯編語言給出的。

一 消息發(fā)送

objc-msg-arm64.s中。第304-346行是objc_msgSend()的實(shí)現(xiàn)。

//從這里開始
304 ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
    MESSENGER_START
//x0寄存器,消息接收者
308 cmp x0, #0          // nil check and tagged pointer check
309 b.le    LNilOrTagged        //  b是跳轉(zhuǎn),le是小于等于,也就是x0小于等于0時,跳轉(zhuǎn)到LNilOrTagged,x0是objc_msgSend()傳入的第一個參數(shù),也就是消息接收者
    ldr x13, [x0]       // x13 = isa
    and x16, x13, #ISA_MASK // x16 = class  
LGetIsaDone:
313 CacheLookup NORMAL      // 緩存查找

315 LNilOrTagged:
316 b.eq    LReturnZero     // 如果消息接收者為空,直接退出這個函數(shù)

    // tagged
    mov x10, #0xf000000000000000
    cmp x0, x10
    b.hs    LExtTag
    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]
    b   LGetIsaDone

LExtTag:
    // 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
    
LReturnZero:
    // x0 is already zero
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    MESSENGER_END_NIL
    ret

346 END_ENTRY _objc_msgSend
//結(jié)束
  • 1.首先從308行開始,cmp x0, #0這里x0是寄存器,里面是消息接收者。b.le LNilOrTagged,b是跳轉(zhuǎn)的意思,le是如果x0小于等于0,總體意思是若x0小于等于0,則跳轉(zhuǎn)到LNilOrTagged。這里意思就是如果消息接收者是nil,則跳轉(zhuǎn)到LNilOrTagged,我們看315行的LNilOrTagged,執(zhí)行b.eq LReturnZero就是直接退出程序。

  • 2.判斷完了消息接收者是否為nil之后,匯編代碼繼續(xù)執(zhí)行,到313行CacheLookup NORMAL,通過字面意思可以知道這是從緩存中查找方法的實(shí)現(xiàn),我們復(fù)制一下CacheLookup然后去本文件中搜索一下:

    66394697-9FCF-4D47-AE77-96BCB4A9D558.png

  • 3.在緩存中找到了方法那就直接調(diào)用,這沒什么好說的,下面看一下從緩存中沒有找到方法怎么辦。沒有找到方法則會執(zhí)行CheckMiss,我們搜索一下它的實(shí)現(xiàn)。

    9FAC4F96-8798-4867-BC3E-EFC2ABB94AB1.png

再搜索一下__objc_msgSend_uncached:

7CD09FA0-B0BC-4AE3-AE6C-DBB6DE15FACE.png

通過MethodTableLookup這個字面名稱我們就大概知道這是從方法列表中去查找方法。我們再查看一下它的結(jié)構(gòu):

66A23B20-BCEA-4707-B938-475CB43BBEB3.png

然后我們在本文件中搜索__class_lookupMethodAndLoadCache3發(fā)現(xiàn)沒有它的定義,然后我們再在整個文件中搜索,發(fā)現(xiàn)還是沒有,這個時候我們?nèi)サ糸_頭的一個下劃線再搜索,發(fā)現(xiàn)有了結(jié)果,這是因?yàn)閰R編的函數(shù)比c++的多一個下劃線。

  • 4.我們在objc-runtime-new.mm這個文件中找到了_class_lookupMethodAndLoadCache3的實(shí)現(xiàn):
    A2A6063B-72D4-46CD-B491-CD08D3B44D02.png

主要就是實(shí)現(xiàn)了lookUpImpOrForward()這個方法,然后我們再查找一下這個方法:

1730123B-56F0-48C0-BEA1-C50F1E37626B.png

  • 5.我們具體看一下是怎么從類對象中查找方法的,這個主要是在getMethodNoSuper_nolock()這個方法。
    9E58326A-4974-4949-8BE7-99444E5004B5.png

    總結(jié)一下消息發(fā)送的過程就是下圖:
    5EE45D9F-8DA7-400D-A3C7-FAE7E9F212F2.png

二 動態(tài)方法解析

在自己的類對象的緩存和方法列表中都沒有找到方法,并且在父類的類對象的緩存和方法列表中都沒有找到方法時,這時候就會啟動動態(tài)方法解析。

我們再找到lookUpImpOrForward這個方法。在這個方法中前半部分是在自己的類對象以及父類對象中查找方法,后半部分就是處理在自己的類對象和父類對象中都找到不這個方法:

91172CDB-1A7E-49AE-A9E5-FD3DEB951ECE.png

然后我們查看一下_class_resolveMethod()的實(shí)現(xiàn):
AEC48C28-DC5C-433A-8DB1-6628F2E51479.png

其實(shí)實(shí)現(xiàn)很簡單,就是判斷是類對象還是元類對象,如果是類對象則說明調(diào)用的實(shí)例方法,則調(diào)用類的resolveInstanceMethod:方法,如果是元類對象,則說明是調(diào)用的類方法,則調(diào)用類的resolveClassMethod:方法。

那下面就用實(shí)例來演示一下動態(tài)方法解析的過程。
首先在main.m文件中創(chuàng)建person對象并調(diào)用test方法:

        Person *person = [[Person alloc] init];
        [person test];

雖然在Person.h文件中聲明了test方法,但是在Person.m文件中并沒有實(shí)現(xiàn)test.m文件。所以運(yùn)行代碼的話應(yīng)該會崩潰,我們運(yùn)行代碼:
果然崩潰了,并且打印了經(jīng)典錯誤:unrecognized selector sent to instance 0x60400000e3e0
程序崩潰很容易理解,因?yàn)樵诘谝徊讲檎曳椒ㄖ校谧约旱念悓ο笠约案割惖念悓ο笾卸紱]有找到這個方法,所以轉(zhuǎn)向動態(tài)方法解析,動態(tài)方法解析我們什么也沒做,所以會轉(zhuǎn)向消息轉(zhuǎn)發(fā),消息轉(zhuǎn)發(fā)我們也什么都沒做,所以最后產(chǎn)生崩潰。接下來我們實(shí)現(xiàn)一下動態(tài)方法解析。

動態(tài)方法解析是當(dāng)?shù)谝徊街蟹椒ú檎沂r會進(jìn)行的,當(dāng)調(diào)用的是對象方法時,動態(tài)方法解析是在resolveInstanceMethod:方法中實(shí)現(xiàn)的,當(dāng)調(diào)用的是類方法時,動態(tài)方法解析是在resolveClassMethod:方法中實(shí)現(xiàn)的。利用動態(tài)方法解析和runtime,我們可以給一個沒有實(shí)現(xiàn)的方法添加方法實(shí)現(xiàn)。

與動態(tài)添加方法實(shí)現(xiàn)相關(guān)的runtime的API是

/** 
 * Adds a new method to a class with a given name and implementation.
*/
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                const char * _Nullable types) 

我們看注釋就是可以知道,這個方法是給一個給定的方法名也就是SEL添加方法的實(shí)現(xiàn)。

@cls : 給哪個類對象添加方法
@name : SEL類型的,給哪個方法名添加方法實(shí)現(xiàn)
@imp : IMP類型的,要把哪個方法實(shí)現(xiàn)添加給給定的方法名
@types :在講method_t的結(jié)構(gòu)時講過這個,就是表示返回值和參數(shù)類型的字符串,比如"v16@0:8"

我現(xiàn)在在Person.m文件中實(shí)現(xiàn)了test2方法:

- (void)test2{
    
    NSLog(@"測試動態(tài)方法解析");
}

那我想要把這個方法的方法實(shí)現(xiàn)添加到Person類中,我就需要調(diào)用runtime的class_addMethod這個API,這些參數(shù)中,cls可以傳self,name可以傳@selector(test),types可以傳"v16@0:8",最難的就是imp應(yīng)該傳什么。我們需要獲取test2函數(shù)的imp,這個應(yīng)該怎么獲取呢?

runtime中也有相對應(yīng)的API:

Method _Nullable
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)

這個API返回的就是一個代表方法的Method。我們可以通過

IMP _Nonnull
method_getImplementation(Method _Nonnull m) 

這個runtime的API通過Method結(jié)構(gòu)獲取方法的IMP,所以最終的代碼就是這樣:

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    
    if (sel == @selector(test)) {
        Method method = class_getInstanceMethod(self, @selector(test2));
        class_addMethod(self, sel, method_getImplementation(method), "v16@0:8");
        return YES;
    }
    
    return [super resolveInstanceMethod:sel];
}

- (void)test2{
    
    NSLog(@"測試動態(tài)方法解析");
}

這樣當(dāng)?shù)谝徊椒椒ú檎艺也坏椒椒〞r,就會進(jìn)行第二步動態(tài)方法解析,由于調(diào)用的是對象方法,所以會執(zhí)行resolveInstanceMethod:方法中的代碼,在這個方法中,使用runtime的API,給類對象中動態(tài)添加了test方法的實(shí)現(xiàn),這個實(shí)現(xiàn)是test2方法的實(shí)現(xiàn)。當(dāng)動態(tài)方法解析結(jié)束后還會返回去進(jìn)行方法查找,這次能夠查找到test方法及其實(shí)現(xiàn)了,也就能夠成功調(diào)用test方法了。
用一個圖總結(jié)動態(tài)方法解析的整個過程:

E45B746D-3235-489E-A6D5-CB53313C1F72.png

三 消息轉(zhuǎn)發(fā)

我們再看一下動態(tài)方法解析的過程:

918365CA-0287-4562-AEB8-E3217F7545C1.png

進(jìn)行動態(tài)方法解析結(jié)束之后,會從頭開始再進(jìn)行消息發(fā)送這一步,如果在動態(tài)方法解析的時候有動態(tài)添加方法實(shí)現(xiàn),那么就能找到方法實(shí)現(xiàn)并返回方法實(shí)現(xiàn),不再執(zhí)行下面的代碼;如果在動態(tài)方法解析的時候沒有做什么事,那么就不能找到方法實(shí)現(xiàn),這時候由于triedResolver標(biāo)志位已經(jīng)置為YES,也就不會再進(jìn)入動態(tài)消息解析,而是會進(jìn)入消息轉(zhuǎn)發(fā)。

消息轉(zhuǎn)發(fā)通俗地講就是本類沒有能力去處理這個消息,那么就轉(zhuǎn)發(fā)給其他的類,讓其他類去處理。

接下來我們看一下進(jìn)行消息轉(zhuǎn)發(fā)的函數(shù)_objc_msgForward_impcache的具體實(shí)現(xiàn),去文件中搜索,在匯編中找到了它的實(shí)現(xiàn):

91474E51-E36E-4186-A8ED-655B30AD797A.png

然后我們?nèi)ゲ檎?code>__objc_forward_handler的實(shí)現(xiàn),但是找到了半天好像并不能找到其實(shí)現(xiàn),這個函數(shù)有可能并不是開源的,那我們這條路就行不通了。

網(wǎng)上有人寫了__forwarding__這個函數(shù)的實(shí)現(xiàn)的偽代碼,我們可以拿來學(xué)習(xí)一下。為什么要學(xué)習(xí)這個函數(shù)呢?因?yàn)楫?dāng)[person test]崩潰時調(diào)用棧是這樣的:

2BEAA7F7-8ADB-4CD5-BE3E-5D331193A9B4.png

我們來看一下__forwarding__函數(shù)的第一步:
E876DC6C-6E13-4F6A-8913-1DB21965BB8F.png

下面用例子說明一下:
在Person.h中聲明了test方法,但是Person.m中并沒有去實(shí)現(xiàn)。那這個時候用Person對象去調(diào)用test方法就會產(chǎn)生崩潰。這個時候在Student.m文件中實(shí)現(xiàn)一個test方法,并且在Person.m中通過forwardingTargetForSelector:方法把消息轉(zhuǎn)發(fā)對象設(shè)置為Student對象:

// Student.m
- (void)test{
    
    NSLog(@"轉(zhuǎn)發(fā)給student處理");
}
// Person.m
- (id)forwardingTargetForSelector:(SEL)aSelector{
    
    if (aSelector == @selector(test)) {
        return [[Student alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

這樣的話person對象就成功把@selector(test)這個消息轉(zhuǎn)發(fā)給student對象讓它去處理,自己不管了。相當(dāng)于是調(diào)用了objc_msgSend(student, @selector(test))。我們可以從另外一個角度去驗(yàn)證這個問題,使一個沒有實(shí)現(xiàn)test方法的類的對象成為消息轉(zhuǎn)發(fā)對象:

// Person.m
- (id)forwardingTargetForSelector:(SEL)aSelector{
    
    if (aSelector == @selector(test)) {
        return [[NSObject alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

這個NSObject類是沒有實(shí)現(xiàn)test方法的,我們看一下運(yùn)行結(jié)果:

-[NSObject test]: unrecognized selector sent to instance 0x600000013810

我們看到,現(xiàn)在直接是在NSObject這個類中沒有找到test方法了。

現(xiàn)在有一個問題了,如果- (id)forwardingTargetForSelector:(SEL)aSelector返回為空或者壓根就沒有實(shí)現(xiàn),程序又會如何繼續(xù)呢?我們還是從偽碼中查找答案:

CE05D612-CAEA-4A2A-A3C3-9A0773F5684A.png

下面用代碼實(shí)例來講解:
Person.h中有- (void)testAge:(int)age;但是在Person.m中并沒有實(shí)現(xiàn)。
現(xiàn)在在main.m中去調(diào)用這個方法:

[person testAge:10];

這個時候會產(chǎn)生崩潰,因?yàn)樵谙l(fā)送階段沒有找到該方法的實(shí)現(xiàn),而動態(tài)方法解析和消息轉(zhuǎn)發(fā)階段則什么都沒有做,所以就崩潰了。
第一階段消息發(fā)送結(jié)束后會進(jìn)行第二階段動態(tài)消息解析,動態(tài)消息解析依賴于+ (BOOL)resolveInstanceMethod:(SEL)sel這個函數(shù),當(dāng)這個函數(shù)也沒有動態(tài)添加方法實(shí)現(xiàn)時,就會進(jìn)入第三階段-消息轉(zhuǎn)發(fā)。

消息轉(zhuǎn)發(fā)首先依賴于- (id)forwardingTargetForSelector:(SEL)aSelector這個方法,若是這個方法直接返回了一個消息轉(zhuǎn)發(fā)對象,則會通過objc_msgSend()把這個消息轉(zhuǎn)發(fā)給消息轉(zhuǎn)發(fā)對象了。若是這個方法沒有實(shí)現(xiàn)或者實(shí)現(xiàn)了但是返回值為空,則會跑去執(zhí)行后面的- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector這個函數(shù)以及- (void)forwardInvocation:(NSInvocation *)anInvocation這個函數(shù)。

現(xiàn)在我們在第二階段動態(tài)方法解析階段沒有做任何處理,在- (id)forwardingTargetForSelector:(SEL)aSelector這個函數(shù)中也不做處理。那么代碼就會執(zhí)行到- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector這個函數(shù),在這個函數(shù)中我們要返回一個方法簽名:

Person.m
//方法簽名:返回值類型,參數(shù)類型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    
    if(aSelector == @selector(testAge:)){
        
        return [NSMethodSignature signatureWithObjCTypes:"v20@0:8i16"];
    }
    
    return [super methodSignatureForSelector:aSelector];
}

我們想一下,要完整的表征person對象調(diào)用- (void)testAge:(int)age這個過程,我們就需要知道方法調(diào)用者,方法名,方法參數(shù)。而在Person.m中我們肯定知道方法調(diào)用者是person對象,方法名也知道是"testAge:",那么現(xiàn)在不知道的就是方法參數(shù)了,那么這個方法簽名就是表征這個方法參數(shù)的,包括返回值和參數(shù),這樣方法調(diào)用者,方法名和方法參數(shù)就都知道了。

然后看- (void)forwardInvocation:(NSInvocation *)anInvocation的實(shí)現(xiàn):

//NSInvocation封裝了一個方法調(diào)用,包括:方法調(diào)用者,方法名,方法參數(shù)
@  anInvocation.target 方法調(diào)用者
@   anInvocation.selector 方法名
@   [anInvocation getArgument:NULL atIndex:0];
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    
    NSLog(@"%@ %@", anInvocation.target, NSStringFromSelector(anInvocation.selector));
    int age;
    [anInvocation getArgument:&age atIndex:2];
    NSLog(@"%d", age);
    //這行代碼是把方法的調(diào)用者改變?yōu)閟tudent對象
    [anInvocation invokeWithTarget:[[Student alloc] init]];
    
}

在這個方法中有一個NSInvocation類型的anInvocation參數(shù),這個參數(shù)就是表征一個方法調(diào)用的,我們可以通過這個參數(shù)獲取person對象調(diào)用- (void)testAge:(int)age方法這個過程中的方法調(diào)用者,方法名,方法參數(shù)。然后我們可以通過修改方法調(diào)用者來達(dá)到消息轉(zhuǎn)發(fā)的效果,這里是把方法調(diào)用者修改為了student對象。這樣就完成了成功轉(zhuǎn)發(fā)消息給student對象。

那么我們思考一個問題,在第三階段消息轉(zhuǎn)發(fā)階段為什么會有三個函數(shù)這個復(fù)雜?如果我們想要轉(zhuǎn)發(fā)消息,那么直接在- (id)forwardingTargetForSelector:(SEL)aSelector去返回一個消息轉(zhuǎn)發(fā)對象就可以了呀。設(shè)計(jì)三個函數(shù)的好處就是,當(dāng)來到- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector這個方法時,如果這個方法返回為空,那么走到這里直接結(jié)束方法調(diào)用,產(chǎn)生崩潰,而如果返回不為空,那么就會繼續(xù)去調(diào)用- (void)forwardInvocation:(NSInvocation *)anInvocation這個方法,那么來到這個里面,我們就可以為所欲為,即使我們什么也不做,運(yùn)行程序也不會崩潰了,我們可以在這個方法里面為方法指定新的調(diào)用者,也即是進(jìn)行消息轉(zhuǎn)發(fā),也可以做一些其他的操作,都可以,這就是這樣設(shè)計(jì)的一個好處,我們可以在這個方法里面做一切我們想做的。

總結(jié)一下消息準(zhǔn)發(fā)的過程就是:


570A2126-DDF8-4103-8B9E-AF6B8FEABF44.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,958評論 2 373

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