iOS-APP-運(yùn)行時(shí)防Crash工具XXShield練就

原文地址

前言

正在運(yùn)行的 APP 突然 Crash,是一件令人不爽的事,會(huì)流失用戶,影響公司發(fā)展,所以 APP 運(yùn)行時(shí)擁有防 Crash 功能能有效降低 Crash 率,提升 APP 穩(wěn)定性。但是有時(shí)候 APP Crash 是應(yīng)有的表現(xiàn),我們不讓 APPCrash 可能會(huì)導(dǎo)致別的邏輯錯(cuò)誤,不過我們可以抓取到應(yīng)用當(dāng)前的堆棧信息并上傳至相關(guān)的服務(wù)器,分析并修復(fù)這些 BUG。

所以本文介紹的 XXShield 庫有兩個(gè)重要的功能:

  1. 防止Crash
  2. 捕獲異常狀態(tài)下的崩潰信息

類似的相關(guān)技術(shù)分析也有 網(wǎng)易iOS App運(yùn)行時(shí)Crash自動(dòng)防護(hù)實(shí)踐

目前已經(jīng)實(shí)現(xiàn)的功能

  1. Unrecoginzed Selector Crash
  2. KVO Crash
  3. Container Crash
  4. NSNotification Crash
  5. NSNull Crash
  6. NSTimer Crash
  7. 野指針 Crash

1 Unrecoginzed Selector Crash

出現(xiàn)原因

由于 Objective-C 是動(dòng)態(tài)語言,所有的消息發(fā)送都會(huì)放在運(yùn)行時(shí)去解析,有時(shí)候我們把一個(gè)信息傳遞給了錯(cuò)誤的類型,就會(huì)導(dǎo)致這個(gè)錯(cuò)誤。

解決辦法

Objective-C 在出現(xiàn)無法解析的方法時(shí)有三部曲來進(jìn)行消息轉(zhuǎn)發(fā)。
詳見Objective-C Runtime 運(yùn)行時(shí)之三:方法與消息

  1. 動(dòng)態(tài)方法解析
  2. 備用接收者
  3. 完整轉(zhuǎn)發(fā)

1 一般適用與 Dynamic 修飾的 Property
2 一般適用與將方法轉(zhuǎn)發(fā)至其他對象
3 一般適用與消息可以轉(zhuǎn)發(fā)多個(gè)對象,可以實(shí)現(xiàn)類似多繼承或者轉(zhuǎn)發(fā)中心的概念。

這里選擇的是方案二,因?yàn)槿锩嬗玫搅?NSInvocation 對象,此對象性能開銷較大,而且這種異常如果出現(xiàn)必然頻次較高。最適合將消息轉(zhuǎn)發(fā)到一個(gè)備用者對象上。

這里新建一個(gè)智能轉(zhuǎn)發(fā)類。此對象將在其他對象無法解析數(shù)據(jù)時(shí),返回一個(gè) 0 來防止 Crash。返回 0 是因?yàn)檫@個(gè)通用的智能轉(zhuǎn)發(fā)類做的操作接近向 nil 發(fā)送一個(gè)消息。

代碼如下


#import <objc/runtime.h>

/**
 default Implement
 @param target trarget
 @param cmd cmd
 @param ... other param
 @return default Implement is zero
 */
int smartFunction(id target, SEL cmd, ...) {
    return 0;
}

static BOOL __addMethod(Class clazz, SEL sel) {
    NSString *selName = NSStringFromSelector(sel);
    
    NSMutableString *tmpString = [[NSMutableString alloc] initWithFormat:@"%@", selName];
    
    int count = (int)[tmpString replaceOccurrencesOfString:@":"
                                                withString:@"_"
                                                   options:NSCaseInsensitiveSearch
                                                     range:NSMakeRange(0, selName.length)];
    
    NSMutableString *val = [[NSMutableString alloc] initWithString:@"i@:"];
    
    for (int i = 0; i < count; i++) {
        [val appendString:@"@"];
    }
    const char *funcTypeEncoding = [val UTF8String];
    return class_addMethod(clazz, sel, (IMP)smartFunction, funcTypeEncoding);
}

@implementation XXShieldStubObject

+ (XXShieldStubObject *)shareInstance {
    static XXShieldStubObject *singleton;
    if (!singleton) {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            singleton = [XXShieldStubObject new];
        });
    }
    return singleton;
}

- (BOOL)addFunc:(SEL)sel {
    return __addMethod([XXShieldStubObject class], sel);
}

+ (BOOL)addClassFunc:(SEL)sel {
    Class metaClass = objc_getMetaClass(class_getName([XXShieldStubObject class]));
    return __addMethod(metaClass, sel);
}

@end

我們這里需要 Hook NSObject的 - (id)forwardingTargetForSelector:(SEL)aSelector 方法啟動(dòng)消息轉(zhuǎn)發(fā)。
很多人不知道的是如果想要轉(zhuǎn)發(fā)類方法,只需要實(shí)現(xiàn)一個(gè)同名的類方法即可,雖然在頭文件中此方法并未聲明。


XXStaticHookClass(NSObject, ProtectFW, id, @selector(forwardingTargetForSelector:), (SEL)aSelector) {
    // 1 如果是NSSNumber 和NSString沒找到就是類型不對  切換下類型就好了
    if ([self isKindOfClass:[NSNumber class]] && [NSString instancesRespondToSelector:aSelector]) {
        NSNumber *number = (NSNumber *)self;
        NSString *str = [number stringValue];
        return str;
    } else if ([self isKindOfClass:[NSString class]] && [NSNumber instancesRespondToSelector:aSelector]) {
        NSString *str = (NSString *)self;
        NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
        NSNumber *number = [formatter numberFromString:str];
        return number;
    }
    
    BOOL aBool = [self respondsToSelector:aSelector];
    NSMethodSignature *signatrue = [self methodSignatureForSelector:aSelector];
    
    if (aBool || signatrue) {
        return XXHookOrgin(aSelector);
    } else {
        XXShieldStubObject *stub = [XXShieldStubObject shareInstance];
        [stub addFunc:aSelector];
        
        NSString *reason = [NSString stringWithFormat:@"*****Warning***** logic error.target is %@ method is %@, reason : method forword to SmartFunction Object default implement like send message to nil.",
                            [self class], NSStringFromSelector(aSelector)];
        [XXRecord recordFatalWithReason:reason userinfo:nil errorType:EXXShieldTypeUnrecognizedSelector];
        
        return stub;
    }
}
XXStaticHookEnd

這里匯報(bào)了 Crash 信息,出現(xiàn)消息轉(zhuǎn)發(fā)一般是一個(gè) logic 錯(cuò)誤,為必須修復(fù)的Bug,上報(bào)尤為重要。


2 KVO Crash

出現(xiàn)原因

KVOCrash總結(jié)下來有以下2大類。

  1. 不匹配的移除和添加關(guān)系。
  2. 觀察者和被觀察者釋放的時(shí)候沒有及時(shí)斷開觀察者關(guān)系。

解決辦法

尼古拉斯趙四說過 :
趙四

對比到程序世界就是,程序世界沒有什么難以解決的問題都是不可以通過抽象層次來解決的,如果有,那就兩層。
縱觀程序的架構(gòu)設(shè)計(jì),計(jì)算機(jī)網(wǎng)絡(luò)協(xié)議分層設(shè)計(jì),操作系統(tǒng)內(nèi)核設(shè)計(jì)等等都是如此。

問題1 : 不成對的添加觀察者和移除觀察者會(huì)導(dǎo)致 Crash,以往我們使用 KVO,觀察者和被觀察者都是直接交互的。這里的設(shè)計(jì)方案是我們找一個(gè) Proxy 用來做轉(zhuǎn)發(fā), 真正的觀察者是 Proxy,被觀察者出現(xiàn)了通知信息,由 Proxy 做分發(fā)。所以 Proxy 里面要保存一個(gè)數(shù)據(jù)結(jié)構(gòu) {keypath : [observer1, observer2,...]} 。


@interface XXKVOProxy : NSObject {
    __unsafe_unretained NSObject *_observed;
}

/**
 {keypath : [ob1,ob2](NSHashTable)}
 */
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSHashTable<NSObject *> *> *kvoInfoMap;

@end

我們需要 Hook NSObject的 ?KVO 相關(guān)方法。


- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

  1. 在添加觀察者時(shí)


    addObserver
    addObserver
  1. 在移除觀察者時(shí)
removeObserver

問題2: 觀察者和被觀察者釋放的時(shí)候沒有斷開觀察者關(guān)系。
對于觀察者, 既然我們是自己用 Proxy 做的分發(fā),我們自己就需要保存觀察者,這里我們簡單的使用 NSHashTable 指定指針持有策略為 weak 即可。

對于被觀察者,我們使用 iOS 界的毒瘤-MethodSwizzling
一文中到的方法。我們在被觀察者上綁定一個(gè)關(guān)聯(lián)對象,在關(guān)聯(lián)對象的 dealloc 方法中做相關(guān)操作即可。


- (void)dealloc {
    @autoreleasepool {
        NSDictionary<NSString *, NSHashTable<NSObject *> *> *kvoinfos =  self.kvoInfoMap.copy;
        for (NSString *keyPath in kvoinfos) {
            // call original  IMP
            __xx_hook_orgin_function_removeObserver(_observed,@selector(removeObserver:forKeyPath:),self, keyPath);
        }
    }
}


3 Container Crash

出現(xiàn)原因

容器在任何編程語言中都尤為重要,容器是數(shù)據(jù)的載體,很多容器對容器放空值都做了容錯(cuò)處理。不幸的是 Objective-C 并沒有,容器插入了 nil 就會(huì)導(dǎo)致 Crash,容器還有另外一個(gè)最容易 Crash 的原因就是下標(biāo)越界。

解決辦法

常見的容器有 NS(Mutable)Array , NS(Mutable)Dictionary, NSCache 等。我們需要 hook 常見的方法加入檢測功能并且捕獲堆棧信息上報(bào)。

例如


XXStaticHookClass(NSArray, ProtectCont, id, @selector(objectAtIndex:),(NSUInteger)index) {
if (self.count == 0) {
    
    NSString *reason = [NSString stringWithFormat:@"target is %@ method is %@,reason : index %@ out of count %@ of array ",
                        [self class], XXSEL2Str(@selector(objectAtIndex:)), @(index), @(self.count)];
    [XXRecord recordFatalWithReason:reason userinfo:nil errorType:EXXShieldTypeContainer];
    return nil;
}

if (index >= self.count) {
    NSString *reason = [NSString stringWithFormat:@"target is %@ method is %@,reason : index %@ out of count %@ of array ",
                        [self class], XXSEL2Str(@selector(objectAtIndex:)), @(index), @(self.count)];
    [XXRecord recordFatalWithReason:reason userinfo:nil errorType:EXXShieldTypeContainer];
    return nil;
}

return XXHookOrgin(index);
}
XXStaticHookEnd

但是需要注意的是 NSArray 是一個(gè) Class Cluster 的抽象父類,所以我們需要 Hook 到我們真正的子類。

這里給出一個(gè)輔助方法,獲取一個(gè)類的所有直接子類:

+ (NSArray *)findAllOf:(Class)defaultClass {
    
    int count = objc_getClassList(NULL, 0);
    
    if (count <= 0) {
        
        @throw@"Couldn't retrieve Obj-C class-list";
        
        return @[defaultClass];
    }
    
    NSMutableArray *output = @[].mutableCopy;
    
    Class *classes = (Class *) malloc(sizeof(Class) * count);
    
    objc_getClassList(classes, count);
    
    for (int i = 0; i < count; ++i) {
        
        if (defaultClass == class_getSuperclass(classes[i]))//子類
        {
            [output addObject:classes[i]];
        }
        
    }
    
    free(classes);
    
    return output.copy;
    
}

// 對于NSarray :

//[NSarray array] 和 @[] 的類型是__NSArray0
//只有一個(gè)元素的數(shù)組類型 __NSSingleObjectArrayI,
// 其他的大部分是//__NSArrayI,



// 對于NSMutableArray :
//[NSMutableDictionary dictionary] 和 @[].mutableCopy__NSArrayM



// 對于NSDictionary: :

//[NSDictionary dictionary];。 @{}; __NSDictionary0
// 其他一般是  __NSDictionaryI

// 對于NSMutableDictionary: :
// 一般用到的是 __NSDictionaryM

4 NSNotification Crash

出現(xiàn)原因

在 iOS8 及以下的操作系統(tǒng)中添加的觀察者一般需要在 dealloc 的時(shí)候做移除,如果開發(fā)者忘記移除,則在發(fā)送通知的時(shí)候會(huì)導(dǎo)致 Crash,而在 iOS9 上即使移忘記除也無所謂,猜想可能是 iOS9 之后系統(tǒng)將通知中心持有對象由 assign 變?yōu)榱?code>weak。

解決辦法

所以這里兩種解決辦法

  1. 類似 KVO 中間加上 Proxy 層,使用 weak 指針來持有對象
  2. 在 dealloc 的時(shí)候?qū)⑽幢灰瞥挠^察者移除

這里我們使用 iOS 界的毒瘤-MethodSwizzling
一文中到的方法。


5 NSNull Crash

出現(xiàn)原因

雖然 Objecttive-C 不允許開發(fā)者將 nil 放進(jìn)容器內(nèi),但是另外一個(gè)代表用戶態(tài) 的類 NSNull 卻可以放進(jìn)容器,但令人不爽的是這個(gè)類的實(shí)例,并不能響應(yīng)任何方法。

容器中出現(xiàn) NSNull 一般是 API 接口返回了含有 null 的 JSON ?數(shù)據(jù),
調(diào)用方通常將其理解為 NSNumber,NSString,NSDictionary 和 NSArray。 這時(shí)開發(fā)者如果沒有做好防御 一旦對 NSNull 這個(gè)類型調(diào)用任何方法都會(huì)出現(xiàn) unrecongized selector 錯(cuò)誤。

解決辦法

我們在 NSNull 的轉(zhuǎn)發(fā)方法中可以判斷?上面的四種類型是否可以解析。如果可以解析直接將其轉(zhuǎn)發(fā)給?這幾種對象,如果不能則調(diào)用父類的默認(rèn)實(shí)現(xiàn)。


XXStaticHookClass(NSNull, ProtectNull, id, @selector(forwardingTargetForSelector:), (SEL) aSelector) {
    static NSArray *sTmpOutput = nil;
    if (sTmpOutput == nil) {
        sTmpOutput = @[@"", @0, @[], @{}];
    }
    
    for (id tmpObj in sTmpOutput) {
        if ([tmpObj respondsToSelector:aSelector]) {
            return tmpObj;
        }
    }
    return XXHookOrgin(aSelector);
}
XXStaticHookEnd

6. NSTimer Crash

出現(xiàn)原因

在使用 + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo 創(chuàng)建定時(shí)任務(wù)的時(shí)候,target? 一般都會(huì)持有 timer,timer又會(huì)持有 target 對象,在我們沒有正確關(guān)閉定時(shí)器的時(shí)候,timer 會(huì)一直持有target 導(dǎo)致內(nèi)存泄漏。

解決辦法

同 KVO 一樣,既然 timer 和 target 直接交互容易出現(xiàn)問題,我們就再找個(gè)代理將 target 和 selctor 等信息保存到 Proxy 里,并且是弱引用 target。
這樣避免因?yàn)檠h(huán)引用造成的內(nèi)存泄漏。然后在觸發(fā)真正 target 事件的時(shí)候如果 target 置為 nil 了這時(shí)候手動(dòng)去關(guān)閉定時(shí)器。


XXStaticHookMetaClass(NSTimer, ProtectTimer,  NSTimer * ,@selector(scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:),
                      (NSTimeInterval)ti , (id)aTarget, (SEL)aSelector, (id)userInfo, (BOOL)yesOrNo ) {
    if (yesOrNo) {
        NSTimer *timer =  nil ;
        @autoreleasepool {
            XXTimerProxy *proxy = [XXTimerProxy new];
            proxy.target = aTarget;
            proxy.aSelector = aSelector;
            timer.timerProxy = proxy;
            timer = XXHookOrgin(ti, proxy, @selector(trigger:), userInfo, yesOrNo);
            proxy.sourceTimer = timer;
        }
        return  timer;
    }
    return XXHookOrgin(ti, aTarget, aSelector, userInfo, yesOrNo);
}
XXStaticHookEnd
@implementation XXTimerProxy

- (void)trigger:(id)userinfo  {
    id strongTarget = self.target;
    if (strongTarget && ([strongTarget respondsToSelector:self.aSelector])) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [strongTarget performSelector:self.aSelector withObject:userinfo];
#pragma clang diagnostic pop
    } else {
        NSTimer *sourceTimer = self.sourceTimer;
        if (sourceTimer) {
            [sourceTimer invalidate];
        }
        NSString *reason = [NSString stringWithFormat:@"*****Warning***** logic error target is %@ method is %@, reason : an object dealloc not invalidate Timer.",
                            [self class], NSStringFromSelector(self.aSelector)];
        
        [XXRecord recordFatalWithReason:reason userinfo:nil errorType:(EXXShieldTypeTimer)];
    }
}

@end

7. 野指針 Crash

出現(xiàn)原因

一般在單線程條件下使用 ARC 正確的處理引用關(guān)系野指針出現(xiàn)的并不頻繁, 但是多線程下則不盡然,通常在一個(gè)線程中釋放了對象,?另外一個(gè)線程還沒有更新指針狀態(tài) 后續(xù)訪問就可能會(huì)造成隨機(jī)性 bug。

之所以是隨機(jī) bug 是因?yàn)楸换厥盏膬?nèi)存不一定立馬被使用。而且崩潰的位置可能也與原來的邏輯相聚很遠(yuǎn),因此收集的堆棧信息也可能是雜亂無章沒有什么價(jià)值。
具體的分類請看Bugly整理的腦圖。


x

更多關(guān)于野指針的文章請參考:

  1. 如何定位Obj-C野指針隨機(jī)Crash(一)
  2. 如何定位Obj-C野指針隨機(jī)Crash(二)
  3. 如何定位Obj-C野指針隨機(jī)Crash(三)

解決辦法

這里我們可以借用系統(tǒng)的NSZombies對象的設(shè)計(jì)。
參考buildNSZombie

解決過程

  1. 建立白名單機(jī)制,由于系統(tǒng)的類基本不會(huì)出現(xiàn)野指針,而且 hook 所有的類開銷較大。所以我們只過濾開發(fā)者自定義的類。

  2. hook dealloc 方法 這些需要保護(hù)的類我們并不讓其釋放,而是調(diào)用objc_desctructInstance 方法釋放實(shí)例內(nèi)部所持有屬性的引用和關(guān)聯(lián)對象。

  3. 利用 object_setClass(id,Class) 修改 isa 指針將其指向一個(gè)Proxy 對象(類比?系統(tǒng)的 KVO 實(shí)現(xiàn)),此 Proxy 實(shí)現(xiàn)了一個(gè)和前面所說的智能轉(zhuǎn)發(fā)類一樣的 return 0的函數(shù)。

  4. 在 Proxy 對象內(nèi)的 - (void)forwardInvocation:(NSInvocation *)anInvocation 中收集 Crash 信息。

  5. 緩存的對象是有成本的,我們在緩存對象到達(dá)一定數(shù)量時(shí)候?qū)⑵溽尫?object_dispose)。

存在問題

  1. 延遲釋放內(nèi)存會(huì)造成性能浪費(fèi),所以默認(rèn)緩存會(huì)造成野指針的Class實(shí)例的對象限制是50,超出之后會(huì)釋放,如果這時(shí)候再此觸發(fā)了剛好釋放掉的野指針,還是會(huì)造成Crash的,

  2. 建議使用的時(shí)候如果近期沒有野指針的Crash可以不必開啟,如果野指針類型的Crash突然增多,可以考慮在 hot Patch 中開啟野指針防護(hù),待收取異常信息之后,再關(guān)閉此開關(guān)。


收集信息

由于希望此庫沒有任何外部依賴,所以并未實(shí)現(xiàn)響應(yīng)的上報(bào)邏輯。使用者如果需要上報(bào)信息 只需要自行實(shí)現(xiàn) XXRecordProtocol 即可,然后在開啟 SDK 之前將其注冊進(jìn)入 SDK。
在實(shí)現(xiàn)方法里面會(huì)接收到 XXShield 內(nèi)部定義的錯(cuò)誤信息。
開發(fā)者無論可以使用諸如 CrashLytics,友盟, bugly等第三庫,或者自行 dump堆棧信息都可。

@protocol XXRecordProtocol <NSObject>

- (void)recordWithReason:(NSError * )reason userInfo:(NSDictionary *)userInfo;

@end

使用方法

示例工程


git clone git@github.com:ValiantCat/XXShield.git
cd Example
pod install 
open XXShield.xcworkspace

Install

    
  pod "XXShield"
    

Usage


/**
 注冊匯報(bào)中心
 
 @param record 匯報(bào)中心
 */
+ (void)registerRecordHandler:(id<XXRecordProtocol>)record;

/**
 注冊SDK,默認(rèn)只要開啟就打開防Crash,如果需要DEBUG關(guān)閉,請?jiān)谡{(diào)用處使用條件編譯
 本注冊方式不包含EXXShieldTypeDangLingPointer類型
 */
+ (void)registerStabilitySDK;

/**
 本注冊方式不包含EXXShieldTypeDangLingPointer類型
 
 @param ability ability
 */
+ (void)registerStabilityWithAbility:(EXXShieldType)ability;

/**
 ///注冊EXXShieldTypeDangLingPointer需要傳入存儲類名的array,暫時(shí)請不要傳入系統(tǒng)框架類
 
 @param ability ability description
 @param classNames 野指針類列表
 */
+ (void)registerStabilityWithAbility:(EXXShieldType)ability withClassNames:(nonnull NSArray<NSString *> *)classNames;


ChangeLog

ChangeLog

單元測試

相關(guān)的單元測試在示例工程的Test Target下,有興趣的開發(fā)者可以自行查看。并且已經(jīng)接入 TrivisCI保證了?代碼質(zhì)量。

?Bug&Feature

如果有相關(guān)的 Bug 請?zhí)?Issue

如果覺得可以擴(kuò)充新的防護(hù)類型,請?zhí)?PR 給我。

作者

?ValiantCat, 519224747@qq.com
個(gè)人博客
南梔傾寒的簡書

License

XXShield 使用 Apache-2.0 開源協(xié)議.

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

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