iOS-底層原理 14:消息流程分析之 動態方法決議 & 消息轉發

iOS 底層原理 文章匯總

引子

在前面兩篇文章iOS-底層原理 12:objc_msgSend流程分析之快速查找iOS-底層原理 13:objc_msgSend流程分析之慢速查找中,分別分析了objc_msgSend快速查找慢速查找,在這兩種都沒找到方法實現的情況下,蘋果給了兩個建議

  • 動態方法決議:慢速查找流程未找到后,會執行一次動態方法決議
  • 消息轉發:如果動態方法決議仍然沒有找到實現,則進行消息轉發

如果這兩個建議都沒有做任何操作,就會報我們日常開發中常見的方法未實現崩潰報錯,其步驟如下

  • 定義LGPerson類,其中say666實例方法 和 sayNB類方法均沒有實現

    自定義LGPerson類

  • main中 分別調用LGPerson實例方法say666類方法sayNB,運行程序,均會報錯,提示方法未實現,如下所示

    • 調用實例方法say666的報錯結果


      實例方法報錯
    • 調用類方法sayNB的報錯結果


      類方法報錯

方法未實現報錯源碼

根據慢速查找的源碼,我們發現,其報錯最后都是走到__objc_msgForward_impcache方法,以下是報錯流程的源碼

STATIC_ENTRY __objc_msgForward_impcache

// No stret specialization.
b   __objc_msgForward

END_ENTRY __objc_msgForward_impcache

//??
ENTRY __objc_msgForward

adrp    x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
    
END_ENTRY __objc_msgForward
  • 匯編實現中查找__objc_forward_handler,并沒有找到,在源碼中去掉一個下劃線進行全局搜索_objc_forward_handler,有如下實現,本質是調用的objc_defaultForwardHandler方法
// Default forward handler halts the process.
__attribute__((noreturn, cold)) 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;

看著objc_defaultForwardHandler有沒有很眼熟,這就是我們在日常開發中最常見的錯誤:沒有實現函數,運行程序,崩潰時報的錯誤提示

下面,我們來講講如何在崩潰前,如何操作,可以防止方法未實現的崩潰。

三次方法查找的挽救機會

根據蘋果的兩個建議,我們一共有三次挽救的機會:

  • 【第一次機會】動態方法決議

  • 消息轉發流程

    • 【第二次機會】快速轉發
    • 【第三次機會】慢速轉發

【第一次機會】動態方法決議

慢速查找流程未找到方法實現時,首先會嘗試一次動態方法決議,其源碼實現如下:

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();
    //對象 -- 類
    if (! cls->isMetaClass()) { //類不是元類,調用對象的解析方法
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {//如果是元類,調用類的解析方法, 類 -- 元類
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        //為什么要有這行代碼? -- 類方法在元類中是對象方法,所以還是需要查詢元類中對象方法的動態方法決議
        if (!lookUpImpOrNil(inst, sel, cls)) { //如果沒有找到或者為空,在元類的對象方法解析方法中查找
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    //如果方法解析中將其實現指向其他方法,則繼續走方法查找流程
    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}

主要分為以下幾步

  • 判斷類是否是元類
    • 如果是,執行實例方法的動態方法決議resolveInstanceMethod
    • 如果是元類,執行類方法的動態方法決議resolveClassMethod,如果在元類中沒有找到或者為,則在元類實例方法的動態方法決議resolveInstanceMethod中查找,主要是因為類方法在元類中是實例方法,所以還需要查找元類中實例方法的動態方法決議
  • 如果動態方法決議中,將其實現指向了其他方法,則繼續查找指定的imp,即繼續慢速查找lookUpImpOrForward流程

其流程如下


方法解析流程

實例方法

針對實例方法調用,在快速-慢速查找均沒有找到實例方法的實現時,我們有一次挽救的機會,即嘗試一次動態方法決議,由于是實例方法,所以會走到resolveInstanceMethod方法,其源碼如下

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);
    
    // look的是 resolveInstanceMethod --相當于是發送消息前的容錯處理
    if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel); //發送resolve_sel消息

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    //查找say666
    IMP imp = lookUpImpOrNil(inst, sel, cls);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

主要分為以下幾個步驟:

  • 發送resolveInstanceMethod消息前,需要查找cls中是否有該方法的實現,即通過lookUpImpOrNil方法又會進入lookUpImpOrForward慢速查找流程查找resolveInstanceMethod方法
    • 如果沒有,則直接返回
    • 如果有,則發送resolveInstanceMethod消息
  • 再次慢速查找實例方法的實現,即通過lookUpImpOrNil方法又會進入lookUpImpOrForward慢速查找流程查找實例方法

崩潰修改

所以,針對實例方法say666未實現的報錯崩潰,可以通過在重寫``resolveInstanceMethod類方法,并將其指向其他方法的實現,即在LGPerson中重寫resolveInstanceMethod類方法,將實例方法say666的實現指向sayMaster方法實現,如下所示

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(say666)) {
        NSLog(@"%@ 來了", NSStringFromSelector(sel));
        //獲取sayMaster方法的imp
        IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
        //獲取sayMaster的實例方法
        Method sayMethod  = class_getInstanceMethod(self, @selector(sayMaster));
        //獲取sayMaster的豐富簽名
        const char *type = method_getTypeEncoding(sayMethod);
        //將sel的實現指向sayMaster
        return class_addMethod(self, sel, imp, type);
    }
    
    return [super resolveInstanceMethod:sel];
}

重新運行,其打印結果如下


打印結果

從結果中可以發現,resolveInstanceMethod動態決議方法中“來了”打印了兩次,這是為什么呢?通過堆棧信息可以看出

堆棧信息

  • 【第一次動態決議】第一次的“來了”是在查找say666方法時會進入動態方法決議
  • 【第二次動態決議】第二次“來了”是在慢速轉發流程中調用了CoreFoundation框架中的NSObject(NSObject) methodSignatureForSelector:后,會再次進入動態決議

注:詳細的分析流程請看文末的問題探索

類方法

針對類方法,與實例方法類似,同樣可以通過重寫resolveClassMethod類方法來解決前文的崩潰問題,即在LGPerson類中重寫該方法,并將sayNB類方法的實現指向類方法lgClassMethod

+ (BOOL)resolveClassMethod:(SEL)sel{
    
    if (sel == @selector(sayNB)) {
        NSLog(@"%@ 來了", NSStringFromSelector(sel));
        
        IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        Method lgClassMethod  = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        const char *type = method_getTypeEncoding(lgClassMethod);
        return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
    }
    
    return [super resolveClassMethod:sel];
}

resolveClassMethod類方法的重寫需要注意一點,傳入的cls不再是類,而是元類,可以通過objc_getMetaClass方法獲取類的元類,原因是因為類方法在元類中是實例方法

優化

上面的這種方式是單獨在每個類中重寫,有沒有更好的,一勞永逸的方法呢?其實通過方法慢速查找流程可以發現其查找路徑有兩條

  • 實例方法:類 -- 父類 -- 根類 -- nil
  • 類方法:元類 -- 根元類 -- 根類 -- nil

它們的共同點是如果前面沒找到,都會來到根類即NSObject中查找,所以我們是否可以將上述的兩個方法統一整合在一起呢?答案是可以的,可以通過NSObject添加分類的方式來實現統一處理,而且由于類方法的查找,在其繼承鏈,查找的也是實例方法,所以可以將實例方法 和 類方法的統一處理放在resolveInstanceMethod方法中,如下所示

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(say666)) {
        NSLog(@"%@ 來了", NSStringFromSelector(sel));
        
        IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
        Method sayMethod  = class_getInstanceMethod(self, @selector(sayMaster));
        const char *type = method_getTypeEncoding(sayMethod);
        return class_addMethod(self, sel, imp, type);
    }else if (sel == @selector(sayNB)) {
        NSLog(@"%@ 來了", NSStringFromSelector(sel));
        
        IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        Method lgClassMethod  = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        const char *type = method_getTypeEncoding(lgClassMethod);
        return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
    }
    return NO;
}

這種方式的實現,正好與源碼中針對類方法的處理邏輯是一致的,即完美闡述為什么調用了類方法動態方法決議,還要調用對象方法動態方法決議,其根本原因還是類方法在元類中的實例方法

當然,上面這種寫法還是會有其他的問題,比如系統方法也會被更改,針對這一點,是可以優化的,即我們可以針對自定義類中方法統一方法名的前綴,根據前綴來判斷是否是自定義方法,然后統一處理自定義方法,例如可以在崩潰前pop到首頁,主要是用于app線上防崩潰的處理,提升用戶的體驗。

消息轉發流程

在慢速查找的流程中,我們了解到,如果快速+慢速沒有找到方法實現,動態方法決議也不行,就使用消息轉發,但是,我們找遍了源碼也沒有發現消息轉發的相關源碼,可以通過以下方式來了解,方法調用崩潰前都走了哪些方法

  • 通過instrumentObjcMessageSends方式打印發送消息的日志

  • 通過hopper/IDA反編譯

通過instrumentObjcMessageSends

  • 通過lookUpImpOrForward --> log_and_fill_cache --> logMessageSend,在logMessageSend源碼下方找到instrumentObjcMessageSends的源碼實現,所以,在main中調用
    instrumentObjcMessageSends打印方法調用的日志信息,有以下兩點準備工作
    • 1、打開 objcMsgLogEnabled 開關,即調用instrumentObjcMessageSends方法時,傳入YES

    • 2、在main中通過extern 聲明instrumentObjcMessageSends方法

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        LGPerson *person = [LGPerson alloc];
        instrumentObjcMessageSends(YES);
        [person sayHello];
        instrumentObjcMessageSends(NO);
        NSLog(@"Hello, World!");
    }
    return 0;
}
  • 通過logMessageSend源碼,了解到消息發送打印信息存儲在/tmp/msgSends 目錄,如下所示

    消息發送日志路徑

  • 運行代碼,并前往/tmp/msgSends 目錄,發現有msgSends開頭的日志文件,打開發現在崩潰前,執行了以下方法

    • 兩次動態方法決議resolveInstanceMethod方法
    • 兩次消息快速轉發forwardingTargetForSelector方法
    • 兩次消息慢速轉發methodSignatureForSelector + resolveInvocation
      消息發送日志詳情

通過hopper/IDA反編譯

Hopper和IDA是一個可以幫助我們靜態分析可視性文件的工具,可以將可執行文件反匯編成偽代碼、控制流程圖等,下面以Hopper為例(注:hopper高級版本是一款收費軟件,針對比較簡單的反匯編需求來說,demo版本足夠使用了)

  • 運行程序崩潰,查看堆棧信息


    查看堆棧打印信息
  • 發現___forwarding___來自CoreFoundation

    ___forwarding___源碼定位

  • 通過image list,讀取整個鏡像文件,然后搜索CoreFoundation,查看其可執行文件的路徑

    從鏡像文件中查找CoreFoundation

  • 通過文件路徑,找到CoreFoundation可執行文件

    查找CoreFoundation的可執行文件

  • 打開hopper,選擇Try the Demo,然后將上一步的可執行文件拖入hopper進行反匯編,選擇x86(64 bits)

    hopper選擇Demo版本

    hopper反匯編

  • 以下是反匯編后的界面,主要使用上面的三個功能,分別是 匯編、流程圖、偽代碼


    hoppper主要使用的三個功能
  • 通過左側的搜索框搜索__forwarding_prep_0___,然后選擇偽代碼

    • 以下是__forwarding_prep_0___的匯編偽代碼,跳轉至___forwarding___

      偽代碼-___forwarding___

    • 以下是___forwarding___的偽代碼實現,首先是查看是否實現forwardingTargetForSelector方法,如果沒有響應,跳轉至loc_6459b即快速轉發沒有響應,進入慢速轉發流程,

      偽代碼-forwardingTargetForSelector

    • 跳轉至loc_6459b,在其下方判斷是否響應methodSignatureForSelector方法,

      偽代碼-methodSignatureForSelector

      • 如果沒有響應,跳轉至loc_6490b,則直接報錯

      • 如果獲取methodSignatureForSelector方法簽名為nil,也是直接報錯

        偽代碼-methodSignatureForSelector為nil時報錯

  • 如果methodSignatureForSelector返回值不為空,則在forwardInvocation方法中對invocation進行處理

    偽代碼-forwardInvocation

所以,通過上面兩種查找方式可以驗證,消息轉發的方法有3個

  • 【快速轉發】forwardingTargetForSelector
  • 【慢速轉發】
    • methodSignatureForSelector
    • forwardInvocation

所以,綜上所述,消息轉發整體的流程如下


消息轉發整體流程

消息轉發的處理主要分為兩部分:

  • 【快速轉發】當慢速查找,以及動態方法決議均沒有找到實現時,進行消息轉發,首先是進行快速消息轉發,即走到forwardingTargetForSelector方法
    • 如果返回消息接收者,在消息接收者中還是沒有找到,則進入另一個方法的查找流程

    • 如果返回nil,則進入慢速消息轉發

  • 【慢速轉發】執行到methodSignatureForSelector方法
    • 如果返回的方法簽名nil,則直接崩潰報錯

    • 如果返回的方法簽名不為nil,走到forwardInvocation方法中,對invocation事務進行處理,如果不處理也不會報錯

【第二次機會】快速轉發

針對前文的崩潰問題,如果動態方法決議也沒有找到實現,則需要在LGPerson中重寫forwardingTargetForSelector方法,將LGPerson的實例方法的接收者指定為LGStudent的對象(LGStudent類中有say666的具體實現),如下所示

- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));

//     runtime + aSelector + addMethod + imp
    //將消息的接收者指定為LGStudent,在LGStudent中查找say666的實現
    return [LGStudent alloc];
}

執行結果如下


快速轉發-指定消息接收者

也可以直接不指定消息接收者,直接調用父類的該方法,如果還是沒有找到,則直接報錯

快速轉發-調用父類

【第三次機會】慢速轉發

針對第二次機會即快速轉發中還是沒有找到,則進入最后的一次挽救機會,即在LGPerson中重寫methodSignatureForSelector,如下所示

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s - %@",__func__,anInvocation);
}

打印結果如下,發現forwardInvocation方法中不對invocation進行處理,也不會崩潰報錯

invocation未處理的打印

也可以處理invocation事務,如下所示,修改invocationtarget[LGStudent alloc],調用 [anInvocation invoke] 觸發 即LGPerson類的say666實例方法的調用會調用LGStudentsay666方法

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s - %@",__func__,anInvocation);
    anInvocation.target = [LGStudent alloc];
    [anInvocation invoke];
}

打印結果如下


invocation處理后的打印

所以,由上述可知,無論在forwardInvocation方法中是否處理invocation事務,程序都不會崩潰

“動態方法決議為什么執行兩次?” 問題探索

在前文中提及了動態方法決議方法執行了兩次,有以下兩種分析方式

啟用上帝視角的探索

在慢速查找流程中,我們了解到resolveInstanceMethod方法的執行是通過lookUpImpOrForward --> resolveMethod_locked --> resolveInstanceMethod來到resolveInstanceMethod源碼,在源碼中通過發送resolve_sel消息觸發,如下所示

resolveInstanceMethod方法觸發原理

所以可以在resolveInstanceMethod方法中IMP imp = lookUpImpOrNil(inst, sel, cls);處加一個斷點,通過bt打印堆棧信息來看到底發生了什么

  • resolveInstanceMethod方法中IMP imp = lookUpImpOrNil(inst, sel, cls);處加一個斷點,運行程序,直到第一次“來了”,通過bt查看第一次動態方法決議的堆棧信息,此時的sel是say666

    第一次動態方法決議堆棧信息

  • 繼續往下執行,直到第二次“來了”打印,查看堆棧信息,在第二次中,我們可以看到是通過CoreFoundation-[NSObject(NSObject) methodSignatureForSelector:]方法,然后通過class_getInstanceMethod再次進入動態方法決議,

    第二次動態方法決議堆棧信息

  • 通過上一步的堆棧信息,我們需要去看看CoreFoundation中到底做了什么?通過Hopper反匯編CoreFoundation的可執行文件,查看methodSignatureForSelector方法的偽代碼

    methodSignatureForSelector偽代碼進入方式

  • 通過methodSignatureForSelector偽代碼進入___methodDescriptionForSelector的實現

    methodDescriptionForSelector方法的偽代碼

  • 進入 ___methodDescriptionForSelector的偽代碼實現,結合匯編的堆棧打印,可以看到,在___methodDescriptionForSelector這個方法中調用了objc4-781class_getInstanceMethod

    ___methodDescriptionForSelector方法的偽代碼調用了class_getInstanceMethod

  • 在objc中的源碼中搜索class_getInstanceMethod,其源碼實現如下所示

    class_getInstanceMethod方法源碼

這一點可以通過代碼調試來驗證,如下所示,在class_getInstanceMethod方法處加一個斷點,在執行了methodSignatureForSelector方法后,返回了簽名,說明方法簽名是生效的,蘋果在走到invocation之前,給了開發者一次機會再去查詢,所以走到class_getInstanceMethod這里,又去走了一遍方法查詢say666,然后會再次走到動態方法決議

class_getInstanceMethod方法調試驗證

所以,上述的分析也印證了前文中resolveInstanceMethod方法執行了兩次的原因

無上帝視角的探索

如果在沒有上帝視角的情況下,我們也可以通過代碼推導在哪里再次調用了動態方法決議

  • LGPerson中重寫resolveInstanceMethod方法,并加上class_addMethod操作即賦值IMP,此時resolveInstanceMethod會走兩次嗎?
    resolveInstanceMethod方法調試驗證

    【結論】:通過運行發現,如果賦值了IMP,動態方法決議只會走一次,說明不是在這里走第二次動態方法決議,

繼續往下探索

  • 去掉resolveInstanceMethod方法中的賦值IMP,在LGPerson類中重寫forwardingTargetForSelector方法,并指定返回值為[LGStudent alloc],重新運行,如果resolveInstanceMethod打印了兩次,說明是在forwardingTargetForSelector方法之前執行了 動態方法決議,反之,在forwardingTargetForSelector方法之后

    forwardingTargetForSelector方法調試驗證

    【結論】:發現resolveInstanceMethod中的打印還是只打印了一次,數排名第二次動態方法決議 在forwardingTargetForSelector方法后

  • 在LGPerson中重寫 methodSignatureForSelectorforwardInvocation,運行

    methodSignatureForSelector+forwardInvocation方法調試驗證

    【結論】:第二次動態方法決議methodSignatureForSelectorforwardInvocation方法之間

第二種分析同樣可以論證前文中resolveInstanceMethod執行了兩次的原因

經過上面的論證,我們了解到其實在慢速小子轉發流程中,在methodSignatureForSelectorforwardInvocation方法之間還有一次動態方法決議,即蘋果再次給的一個機會,如下圖所示

消息轉發流程-2

總結

到目前為止,objc_msgSend發送消息的流程就分析完成了,在這里簡單總結下

  • 【快速查找流程】首先,在類的緩存cache中查找指定方法的實現

  • 【慢速查找流程】如果緩存中沒有找到,則在類的方法列表中查找,如果還是沒找到,則去父類鏈的緩存和方法列表中查找

  • 【動態方法決議】如果慢速查找還是沒有找到時,第一次補救機會就是嘗試一次動態方法決議,即重寫resolveInstanceMethod/resolveClassMethod 方法

  • 【消息轉發】如果動態方法決議還是沒有找到,則進行消息轉發,消息轉發中有兩次補救機會:快速轉發+慢速轉發

  • 如果轉發之后也沒有,則程序直接報錯崩潰unrecognized selector sent to instance

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容