Objective-C 消息發(fā)送與轉發(fā)機制原理

消息發(fā)送和轉發(fā)流程可以概括為:消息發(fā)送(Messaging)是 Runtime 通過 selector 快速查找 IMP 的過程酪呻,有了函數(shù)指針就可以執(zhí)行對應的方法實現(xiàn);消息轉發(fā)(Message Forwarding)是在查找 IMP 失敗后執(zhí)行一系列轉發(fā)流程的慢速通道,如果不作轉發(fā)處理,則會打日志和拋出異常贷洲。
本文不講述開發(fā)者在消息發(fā)送和轉發(fā)流程中需要做的事,而是講述原理晋柱。能夠很好地閱讀本文的前提是你對 Objective-C Runtime 已經(jīng)有一定的了解优构,關于什么是消息,Class 的結構雁竞,selector钦椭、IMP、元類等概念將不再贅述碑诉。本文用到的源碼為 objc4-680 和 CF-1153.18玉凯,逆向 CoreFoundation.framework 的系統(tǒng)版本為 macOS 10.11.5,匯編語言架構為 x86_64联贩。

八面玲瓏的 objc_msgSend

此函數(shù)是消息發(fā)送必經(jīng)之路漫仆,但只要一提 objc_msgSend,都會說它的偽代碼如下或類似的邏輯泪幌,反正就是獲取 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;
}
  • 源碼解析
    為啥老用偽代碼盲厌?因為 objc_msgSend 是用匯編語言寫的,針對不同架構有不同的實現(xiàn)祸泪。如下為 x86_64 架構下的源碼吗浩,可以在 objc-msg-x86_64.s 文件中找到,關鍵代碼如下:
    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ā)送消息的對象是否為 nil
    的懂扼。如果為 nil,那就直接返回 nil右蒲。這就是為啥也可以對 nil 發(fā)消息阀湿。
  • GetIsaFast 宏可以『快速地』獲取到對象的 isa 指針地址(放到 r11 寄存器,r10 會被重寫瑰妄;在 arm 架構上是直接賦值到 r9)
  • CacheLookup 這個宏是在類的緩存中查找 selector 對應的 IMP(放到 r10)并執(zhí)行陷嘴。如果緩存沒中,那就得到 Class 的方法表中查找了间坐。
  • MethodTableLookup
    宏是重點灾挨,負責在緩存沒命中時在方法表中負責查找 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劳澄。

  • 為什么使用匯編語言
    其實在 objc-msg-x86_64.s 中包含了多個版本的 objc_msgSend 方法地技,它們是根據(jù)返回值的類型和調(diào)用者的類型分別處理的:

  • objc_msgSendSuper:向父類發(fā)消息,返回值類型為 id

  • objc_msgSend_fpret:返回值類型為 floating-point秒拔,其中包含 objc_msgSend_fp2ret 入口處理返回值類型為 long double 的情況

  • objc_msgSend_stret:返回值為結構體

  • objc_msgSendSuper_stret:向父類發(fā)消息莫矗,返回值類型為結構體

當需要發(fā)送消息時,編譯器會生成中間代碼溯警,根據(jù)情況分別調(diào)用 objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, 或 objc_msgSendSuper_stret 其中之一趣苏。
這也是為什么 objc_msgSend 要用匯編語言而不是 OC狡相、C 或 C++ 語言來實現(xiàn)梯轻,因為單獨一個方法定義滿足不了多種類型返回值,有的方法返回 id尽棕,有的返回 int喳挑。考慮到不同類型參數(shù)返回值排列組合映射不同方法簽名(method signature)的問題滔悉,那 switch 語句得老長了伊诵。。回官。**這些原因可以總結為 Calling Convention曹宴,

也就是說函數(shù)調(diào)用者與被調(diào)用者必須約定好參數(shù)與返回值在不同架構處理器上的存取規(guī)則,比如參數(shù)是以何種順序存儲在棧上歉提,或是存儲在哪些寄存器上笛坦。**除此之外還有其他原因,比如其可變參數(shù)用匯編處理起來最方便苔巨,因為找到 IMP 地址后參數(shù)都在棧上版扩。要是用 C++ 傳遞可變參數(shù)那就悲劇了,prologue 機制會弄亂地址(比如 i386 上為了存儲 ebp向后移位 4byte)侄泽,最后還要用 epilogue 打掃戰(zhàn)場礁芦。而且匯編程序執(zhí)行效率高,在 Objective-C Runtime 中調(diào)用頻率較高的函數(shù)好多都用匯編寫的悼尾。

使用 lookUpImpOrForward 快速查找 IMP

上一節(jié)中說到的 _class_lookupMethodAndLoadCache3
函數(shù)其實只是簡單的調(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ù)傳入為 NO柿扣,因為之前已經(jīng)嘗試過查找緩存了。IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver)實現(xiàn)了一套查找 IMP 的標準路徑闺魏,也就是在消息轉發(fā)(Forward)之前的邏輯窄刘。

優(yōu)化緩存查找&類的初始化

先對 debug 模式下的 assert 進行 unlock:

runtimeLock.assertUnlocked();

runtimeLock
本質上是對 Darwin 提供的線程讀寫鎖 pthread_rwlock_t 的一層封裝,提供了一些便捷的方法舷胜。
lookUpImpOrForward 接著做了如下兩件事:
如果使用緩存(cache 參數(shù)為 YES)娩践,那就調(diào)用 cache_getImp 方法從緩存查找 IMP活翩。cache_getImp是用匯編語言寫的,也可以在 objc-msg-x86_64.s 找到翻伺,其依然用了之前說過的 CacheLookup 宏材泄。因為 _class_lookupMethodAndLoadCache3 調(diào)用 lookUpImpOrForward 時 cache 參數(shù)為 NO,這步直接略過吨岭。
如果是第一次用到這個類且 initialize 參數(shù)為 YES(initialize && !cls->isInitialized())拉宗,需要進行初始化工作,也就是開辟一個用于讀寫數(shù)據(jù)的空間辣辫。先對 runtimeLock 寫操作加鎖旦事,然后調(diào)用 cls
的 initialize 方法。如果 sel == initialize 也沒關系急灭,雖然 initialize 還會被調(diào)用一次姐浮,但不會起作用啦允耿,因為 cls->isInitialized() 已經(jīng)是 YES 啦锡凝。

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

考慮到運行時類中的方法可能會增加,需要先做讀操作加鎖巍举,使得方法查找和緩存填充成為原子操作畴嘶。添加 category 會刷新緩存蛋逾,之后如果舊數(shù)據(jù)又被重填到緩存中,category 添加操作就會被忽略掉窗悯。

runtimeLock.read();

之后的邏輯整理如下:
如果 selector 是需要被忽略的垃圾回收用到的方法区匣,則將 IMP 結果設為 _objc_ignored_method,這是個匯編程序入口蒋院,可以理解為一個標記亏钩。對此種情況進行緩存填充操作后,跳到第 7 步悦污;否則執(zhí)行下一步铸屉。
查找當前類中的緩存,跟之前一樣切端,使用 cache_getImp 匯編程序入口彻坛。如果命中緩存獲取到了 IMP,則直接跳到第 7 步踏枣;否則執(zhí)行下一步昌屉。
在當前類中的方法列表(method list)中進行查找,也就是根據(jù) selector 查找到 Method 后茵瀑,獲取 Method 中的 IMP(也就是 method_imp 屬性)间驮,并填充到緩存中。查找過程比較復雜马昨,會針對已經(jīng)排序的列表使用二分法查找竞帽,未排序的列表則是線性遍歷扛施。如果成功查找到 Method 對象,就直接跳到第 7 步屹篓;否則執(zhí)行下一步疙渣。
在繼承層級中遞歸向父類中查找,情況跟上一步類似堆巧,也是先查找緩存妄荔,緩存沒中就查找方法列表。這里跟上一步不同的地方在于緩存策略谍肤,有個 _objc_msgForward_impcache 匯編程序入口作為緩存中消息轉發(fā)的標記啦租。也就是說如果在緩存中找到了 IMP,但如果發(fā)現(xiàn)其內(nèi)容是 _objc_msgForward_impcache荒揣,那就終止在類的繼承層級中遞歸查找篷角,進入下一步;否則跳到第 7 步乳附。
當傳入 lookUpImpOrForward 的參數(shù) resolver 為 YES 并且是第一次進入第 5 步時内地,時進入動態(tài)方法解析伴澄;否則進入下一步赋除。這步消息轉發(fā)前的最后一次機會。此時釋放讀入鎖(runtimeLock.unlockRead()
)非凌,接著間接地發(fā)送 +resolveInstanceMethod 或 +resolveClassMethod 消息举农。這相當于告訴程序員『趕緊用 Runtime 給類里這個 selector 弄個對應的 IMP 吧』,因為此時鎖已經(jīng) unlock 了所以不會緩存結果敞嗡,甚至還需要軟性地處理緩存過期問題可能帶來的錯誤颁糟。這里的業(yè)務邏輯稍微復雜些,后面會總結喉悴。因為這些工作都是在非線程安全下進行的棱貌,完成后需要回到第 1 步再次查找 IMP。
此時不僅沒查找到 IMP箕肃,動態(tài)方法解析也不奏效婚脱,只能將 _objc_msgForward_impcache 當做 IMP 并寫入緩存。這也就是之前第 4 步中為何查找到 _objc_msgForward_impcache 就表明了要進入消息轉發(fā)了勺像。
讀操作解鎖障贸,并將之前找到的 IMP 返回。(無論是正經(jīng) IMP 還是不正經(jīng)的 _objc_msgForward_impcache)這步還偏執(zhí)地做了一些腦洞略大的 assert吟宦,很有趣篮洁。

對于第 5 步,其實是直接調(diào)用 _class_resolveMethod 函數(shù)殃姓,在這個函數(shù)中實現(xiàn)了復雜的方法解析邏輯袁波。如果 cls 是元類則會發(fā)送 +resolveClassMethod瓦阐,然后根據(jù) lookUpImpOrNil(cls, sel, inst, NO/initialize/, YES/cache/, NO/resolver/) 函數(shù)的結果來判斷是否發(fā)送 +resolveInstanceMethod;如果不是元類篷牌,則只需要發(fā)送 +resolveInstanceMethod 消息垄分。這里用 +resolveInstanceMethod 或 +resolveClassMethod 時再次用到了 objc_msgSend,而且第三個參數(shù)正是傳入 lookUpImpOrForward 的那個 sel娃磺。在發(fā)送方法解析消息之后還會調(diào)用 lookUpImpOrNil(cls, sel, inst, NO/initialize/, YES/cache/, NO/resolver/) 來判斷是否已經(jīng)添加上 sel 對應的 IMP 了薄湿,打印出結果。
最后 lookUpImpOrForward 方法也會把真正的 IMP 或者需要消息轉發(fā)的 _objc_msgForward_impcache 返回偷卧,并最終專遞到 objc_msgSend 中豺瘤。而 _objc_msgForward_impcache 會在轉化成 _objc_msgForward 或 _objc_msgForward_stret。這個后面會講解原理听诸。

回顧 objc_msgSend 偽代碼

回過頭來會發(fā)現(xiàn) objc_msgSend 的偽代碼描述得很傳神啊坐求,因為class_getMethodImplementation 的實現(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 時就返回 _objc_msgForward,后面會講到它晌梨。lookUpImpOrNil 跟 lookUpImpOrForward 的功能很相似桥嗤,只是將 lookUpImpOrForward 實現(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 對應的 IMP 或是 nil,所以如果不考慮返回值類型為結構體的情況仔蝌,用那幾行偽代碼來表示復雜的匯編實現(xiàn)還是挺恰當?shù)摹?/p>

forwarding 中路漫漫的消息轉發(fā)

  • objc_msgForward_impcache 的轉換
    _objc_msgForward_impcache 只是個內(nèi)部的函數(shù)指針泛领,只存儲于上節(jié)提到的類的方法緩存中,需要被轉化為 _objc_msgForward 和 _objc_msgForward_stret 才能被外部調(diào)用敛惊。但在 Mac OS X macOS 10.6 及更早版本的 libobjc.A.dylib 中是不能直接調(diào)用的渊鞋,況且我們根本不會直接用到它。帶 stret 后綴的函數(shù)依舊是返回值為結構體的版本瞧挤。
    上一節(jié)最后講到如果沒找到 IMP锡宋,就會將 _objc_msgForward_impcache 返回到 objc_msgSend 函數(shù),而正是因為它是用匯編語言寫的特恬,所以將內(nèi)部使用的 _objc_msgForward_impcache 轉化成外部可調(diào)用的 _objc_msgForward 或 _objc_msgForward_stret 也是由匯編代碼來完成执俩。實現(xiàn)原理很簡單,就是增加個靜態(tài)入口 __objc_msgForward_impcache癌刽,然后根據(jù)此時 CPU 的狀態(tài)寄存器的內(nèi)容來決定轉換成哪個役首。如果是 NE
    (Not Equal) 則轉換成 _objc_msgForward_stret,反之是 EQ
    (Equal) 則轉換成 _objc_msgForward:
jne __objc_msgForward_stret
jmp __objc_msgForward

為何根據(jù)狀態(tài)寄存器的值來判斷轉換成哪個函數(shù)指針呢妒穴?回過頭來看看 objc_msgSend 中調(diào)用完 MethodTableLookup 之后干了什么:

MethodTableLookup %a1, %a2  // r11 = IMP
cmp %r11, %r11      // set eq (nonstret) for forwarding
jmp *%r11           // goto *imp

再看看返回值為結構體的 objc_msgSend_stret 這里的邏輯:

MethodTableLookup %a2, %a3  // r11 = IMP
test    %r11, %r11      // set ne (stret) for forward; r11!=0
jmp *%r11           // goto *imp

稍微懂匯編的人一眼就看明白了宋税,不懂的看注釋也懂了,我就不墨跡了∷嫌停現(xiàn)在總算是把消息轉發(fā)前的邏輯繞回來構成閉環(huán)了杰赛。
上一節(jié)中提到 class_getMethodImplementation 函數(shù)的實現(xiàn),在查找不到 IMP 時返回 _objc_msgForward矮台,而 _objc_msgForward_stret 正好對應著 class_getMethodImplementation_stret:

IMP class_getMethodImplementation_stret(Class cls, SEL sel)
{
    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;
}

也就是說 _objc_msgForward* 系列本質都是函數(shù)指針乏屯,都用匯編語言實現(xiàn)根时,都可以與 IMP 類型的值作比較。_objc_msgForward 和 _objc_msgForward_stret 聲明在 message.h 文件中辰晕。_objc_msgForward_impcache
在早期版本的 Runtime 中叫做 _objc_msgForward_internal
蛤迎。

objc_msgForward 也只是個入口

從匯編源碼可以很容易看出 **_objc_msgForward 和 _objc_msgForward_stret 會分別調(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

這兩個 handler 函數(shù)的區(qū)別從字面上就能看出來,不再贅述含友。
也就是說替裆,消息轉發(fā)過程是現(xiàn)將 _objc_msgForward_impcache 強轉成 _objc_msgForward 或 _objc_msgForward_stret,再分別調(diào)用 _objc_forward_handler 或 _objc_forward_handler_stret
窘问。

objc_setForwardHandler 設置了消息轉發(fā)的回調(diào)

在 Objective-C 2.0 之前辆童,默認的 _objc_forward_handler 或 _objc_forward_handler_stret 都是 nil
,而新版本的默認實現(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() 在殺掉進程的同時還能生成日志,比調(diào)用 exit() 更好儿咱。objc_defaultForwardStretHandler
就是裝模作樣搞個形式主義庭砍,把 objc_defaultForwardHandler 包了一層。attribute((noreturn)) 屬性通知編譯器函數(shù)從不返回值混埠,當遇到類似函數(shù)需要返回值而卻不可能運行到返回值處就已經(jīng)退出來的情況怠缸,該屬性可以避免出現(xiàn)錯誤信息。這里正適合此屬性岔冀,因為要求返回結構體噠凯旭。
因為默認的 Handler 干的事兒就是打日志觸發(fā) crash概耻,我們想要實現(xiàn)消息轉發(fā)使套,就需要替換掉 Handler 并賦值給 _objc_forward_handler 或 _objc_forward_handler_stret,賦值的過程就需要用到 objc_setForwardHandler 函數(shù)鞠柄,實現(xiàn)也是簡單粗暴侦高,就是賦值啊:

void objc_setForwardHandler(void *fwd, void *fwd_stret)
{
    _objc_forward_handler = fwd;
#if SUPPORT_STRET
    _objc_forward_stret_handler = fwd_stret;
#endif
}

總結

我將整個實現(xiàn)流程繪制出來厌杜,過濾了一些不會進入的分支路徑和跟主題無關的細節(jié):

消息發(fā)送與轉發(fā)路徑流程圖
介于國內(nèi)關于這塊知識的好多文章描述不夠準確和詳細奉呛,或是對消息轉發(fā)的原理描述理解不夠深刻,或是側重貼源碼而欠思考夯尽,所以我做了一個比較全面詳細的講解瞧壮。

參考文獻
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
轉載鏈接
http://yulingtianxia.com/blog/2016/06/15/Objective-C-Message-Sending-and-Forwarding/

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市匙握,隨后出現(xiàn)的幾起案子咆槽,更是在濱河造成了極大的恐慌,老刑警劉巖圈纺,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件秦忿,死亡現(xiàn)場離奇詭異麦射,居然都是意外死亡,警方通過查閱死者的電腦和手機灯谣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門潜秋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人胎许,你說我怎么就攤上這事峻呛。” “怎么了辜窑?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵杀饵,是天一觀的道長。 經(jīng)常有香客問我谬擦,道長切距,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任惨远,我火速辦了婚禮谜悟,結果婚禮上,老公的妹妹穿的比我還像新娘北秽。我一直安慰自己葡幸,他們只是感情好,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布贺氓。 她就那樣靜靜地躺著蔚叨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪辙培。 梳的紋絲不亂的頭發(fā)上蔑水,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天,我揣著相機與錄音扬蕊,去河邊找鬼搀别。 笑死,一個胖子當著我的面吹牛尾抑,可吹牛的內(nèi)容都是我干的歇父。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼再愈,長吁一口氣:“原來是場噩夢啊……” “哼榜苫!你這毒婦竟也來了?” 一聲冷哼從身側響起翎冲,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤垂睬,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體羔飞,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡肺樟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了逻淌。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片么伯。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖卡儒,靈堂內(nèi)的尸體忽然破棺而出田柔,到底是詐尸還是另有隱情,我是刑警寧澤骨望,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布硬爆,位于F島的核電站,受9級特大地震影響擎鸠,放射性物質發(fā)生泄漏缀磕。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一劣光、第九天 我趴在偏房一處隱蔽的房頂上張望袜蚕。 院中可真熱鬧,春花似錦绢涡、人聲如沸牲剃。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽凿傅。三九已至,卻和暖如春数苫,著一層夾襖步出監(jiān)牢的瞬間聪舒,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工文判, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留过椎,地道東北人。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓戏仓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親亡鼠。 傳聞我的和親對象是個殘疾皇子赏殃,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354

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