RunLoop在iOS開發中的應用

概要

RunLoop在iOS開發中的應用范圍并沒有像runtime 那樣廣泛,我們通過CFRuntime的源代碼可知runloop跟線程的是密不可分的,一個線程一定會創建一個對應的runloop,只是主線程創建就自動run了,而子線程只會創建不會自動run。蘋果線程管理 Thread Management也說了在線程中利用runloop,


  此外,runloop并不是一個簡單的do-while,作為OSX/iOS系統中Event Loop表現,runloop需要處理消息事件,在沒有消息的時候休眠,有消息事件的時候立刻喚醒。
  綜上所述,從我個人所接觸到知識面runloop一是處理子線程運行,二是根據runloop的不同的activities來處理問題。當然希望通過我這塊磚頭,引出同學們runloop應用的好玉來。

1.CFRunLoopSourceRef 事件源

在下面代碼中,通過自定義子線程thread,運行結果可知hello China是不會被打印的,子線程在打印完hello world 就exit了。

{
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadFun) object:nil];
    [thread start];
    self.thread = thread;
    [self performSelector:@selector(selectorFun) onThread:thread withObject:nil waitUntilDone:NO];
    NSLog(@"hello Thread");
}

- (void)threadFun {
    NSLog(@"hello world");
    // _pthread_exit
}

- (void)selectorFun {
    NSLog(@"hello China");
}

獲取上面代碼的堆棧可以看到子線程瞬間生命就結束了。類似在threadFun函數塊結束的前面添加了_pthread_exit

    frame #0: 0x000000010232f2b0 CoreFoundation`__CFFinalizeRunLoop
    frame #1: 0x000000010232f264 CoreFoundation`__CFTSDFinalize + 100
    frame #2: 0x0000000104e9f39f libsystem_pthread.dylib`_pthread_tsd_cleanup + 544
    frame #3: 0x0000000104e9f0d9 libsystem_pthread.dylib`_pthread_exit + 152
    frame #4: 0x0000000104e9fc38 libsystem_pthread.dylib`pthread_exit + 30
    frame #5: 0x0000000101a36f1e Foundation`+[NSThread exit] + 11
    frame #6: 0x0000000101ab713f Foundation`__NSThread__start__ + 1218
    frame #7: 0x0000000104e9d93b libsystem_pthread.dylib`_pthread_body + 180
    frame #8: 0x0000000104e9d887 libsystem_pthread.dylib`_pthread_start + 286
    frame #9: 0x0000000104e9d08d libsystem_pthread.dylib`thread_start + 13

根據蘋果線程管理的說法可以利用把線程放入runloop中,我們知道子線程的runloop并沒有自動開啟,需要我們手動開啟,蘋果也提供代碼示例:

- (void)threadMainRoutine
{
    BOOL moreWorkToDo = YES;
    BOOL exitNow = NO;
    NSRunLoop* runLoop = [NSRunLoop currentRunLoop];
    // Add the exitNow BOOL to the thread dictionary.
    NSMutableDictionary* threadDict = [[NSThread currentThread] threadDictionary];
    [threadDict setValue:[NSNumber numberWithBool:exitNow] forKey:@"ThreadShouldExitNow"];
    // Install an input source.
    [self myInstallCustomInputSource];
    while (moreWorkToDo && !exitNow)
    {
        // Do one chunk of a larger body of work here.
        // Change the value of the moreWorkToDo Boolean when done.
        // Run the run loop but timeout immediately if the input source isn't waiting to fire.
        [runLoop runUntilDate:[NSDate date]];
        // Check to see if an input source handler changed the exitNow value.
        exitNow = [[threadDict valueForKey:@"ThreadShouldExitNow"] boolValue];
    }
    
}

因此我們可以把我們上面的代碼修改為,程序可以打印出來hello China

- (void)threadFun {
     NSLog(@"hello world");
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop runUntilDate:[NSDate distantFuture]];
    // _pthread_exit
}

可以我們把代碼修改成在界面添加一個按鈕點擊事件,點擊事件由我們的子線程出來,同時我們刪除我們的線程的selectorFun函數邏輯,發現我們觸發按鈕的點擊事件并不會打印doSomething。

{
 UIButton *btn = [[UIButton alloc]initWithFrame:CGRectMake(0, 80, 50, 50)];
    btn.backgroundColor = [UIColor redColor];
    [self.view addSubview:btn];
    [btn addTarget:self action:@selector(clicked) forControlEvents:UIControlEventTouchUpInside];
 NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadFun) object:nil];
    [thread start];
    self.thread = thread;
}
- (void)threadFun {
    NSLog(@"hello world");
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop runUntilDate:[NSDate distantFuture]];
    // _pthread_exit
}
- (void)clicked{
    [self performSelector:@selector(doSomething) onThread:self.thread withObject:nil waitUntilDone:NO];
}

- (void)doSomething{
    NSLog(@"doSomething");
}

因為當前runloop運行的model沒有modeItem,run運行的前提條件必須保證當前model是有item( Source/Timer,二者之一,實際是不需要Observer)將代碼修改為下面 :

- (void)threadFun {
    NSLog(@"hello world");
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    [runLoop runUntilDate:[NSDate distantFuture]];
}

RunLoop只處理兩種源:輸入源、時間源。而輸入源又可以分為:NSPort/自定義源/performSelector,我們常用搭到的performSelector方法有:

// 主線程
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
/// 指定線程
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
/// 針對當前線程
performSelector:withObject:afterDelay:         
performSelector:withObject:afterDelay:inModes:
/// 取消,在當前線程,和上面兩個方法對應
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:

而下面這些不是事件源的,相當于是[self xxx]調用

- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;

run運行函數主要以下3個

- (void)run;
- (void)runUntilDate:(NSDate *)limitDate;
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

第一個run循環一旦開啟,就關閉不了,并且之后的代碼就無法執行。api文檔中提到:如果沒有輸入源和定時源加入到runloop中,runloop就馬上退出,否則通過頻繁調用-runMode:beforeDate:方法來讓runloop運行在NSDefaultRunLoopMode模式下。
  第二個run運行在NSDefaultRunLoopMode模式,有超時時間限制。它實際上也是不斷調用-runMode:beforeDate:方法來讓runloop運行在NSDefaultRunLoopMode模式下,直到到達超時時間。調用CFRunLoopStop(runloopRef)無法停止Run Loop的運行,這個方法只會結束當前-runMode:beforeDate:的調用,之后的runMode:beforeDate:該調用的還是會繼續。直到timeout。對應

CFRunLoopRunInMode(kCFRunLoopDefaultMode,limiteDate,false)

第三個run比第二種方法是可以指定運行模式,只執行一次,執行完就退出。可以用CFRunLoopStop(runloopRef)退出runloop。api文檔里面提到:在第一個input source(非timer)被處理或到達limitDate之后runloop退出,對應

CFRunLoopRunInMode(mode,limiteDate,true)

1.1 子線程常駐

給當前子線程的runbloop的mode 添加事件源來實現線程常駐。所有的關于這個的都會拿AF2.X的代碼說明這個常駐的案例,如果同學開發iOS稍微有點年長的話或者古董代碼的都會用到網絡第三方庫ASIHTTPRequest,也用到利用CFRunLoopAddSource 讓當前網絡線程常駐。

 + (NSThread *)threadForRequest:(ASIHTTPRequest *)request
{
    if (networkThread == nil) {
        @synchronized(self) {
            if (networkThread == nil) {
                networkThread = [[NSThread alloc] initWithTarget:self selector:@selector(runRequests) object:nil];
                [networkThread start];
            }
        }
    }
    return networkThread;
}

+ (void)runRequests
{
    // Should keep the runloop from exiting
    CFRunLoopSourceContext context = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
    CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);

    BOOL runAlways = YES; // Introduced to cheat Static Analyzer
    while (runAlways) {
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
        CFRunLoopRun();
        [pool release];
    }

    // Should never be called, but anyway
    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    CFRelease(source);
}

1.2 程序crash 彈框提示

這個是算我真正接觸到runloop的,當用戶正在操作我們的APP的時候數據發生異常,程序會瞬間閃退,實際上從產品角度老說是一種非常不好的體驗,而對碼農來說也根本無法知道當前程序crash的堆棧信息,通過利用runloop的線程常駐方式,當程序發生異常的時候,通過異常捕獲然后彈出提示框 而不是立馬閃退,同時也可以讓用戶上傳crash日志,早期我還是看到APP在使用這樣的技術,現在crash收集機制越來越完善,目前來說幾乎有這么使用的了。

- (void)alertView:(UIAlertView *)anAlertView clickedButtonAtIndex:(NSInteger)anIndex
{
    if (anIndex == 0)
    {
        dismissed = YES;
    }else if (anIndex==1) {
        NSLog(@"ssssssss");
    }
}

- (void)handleException:(NSException *)exception
{
    [self validateAndSaveCriticalApplicationData];
    
    UIAlertView *alert =
    [[[UIAlertView alloc]
      initWithTitle:NSLocalizedString(@"抱歉,程序出現了異常", nil)
      message:[NSString stringWithFormat:NSLocalizedString(
                                                           @"如果點擊繼續,程序有可能會出現其他的問題,建議您還是點擊退出按鈕并重新打開\n\n"
                                                           @"異常原因如下:\n%@\n%@", nil),
               [exception reason],
               [[exception userInfo] objectForKey:UncaughtExceptionHandlerAddressesKey]]
      delegate:self
      cancelButtonTitle:NSLocalizedString(@"退出", nil)
      otherButtonTitles:NSLocalizedString(@"繼續", nil), nil]
     autorelease];
    [alert show];
    
    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);
    
    while (!dismissed)
    {
        for (NSString *mode in (NSArray *)allModes)
        {
            CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
        }
    }
    
    CFRelease(allModes);
    
    NSSetUncaughtExceptionHandler(NULL);
    signal(SIGABRT, SIG_DFL);
    signal(SIGILL, SIG_DFL);
    signal(SIGSEGV, SIG_DFL);
    signal(SIGFPE, SIG_DFL);
    signal(SIGBUS, SIG_DFL);
    signal(SIGPIPE, SIG_DFL);
    
    if ([[exception name] isEqual:UncaughtExceptionHandlerSignalExceptionName])
    {
        kill(getpid(), [[[exception userInfo] objectForKey:UncaughtExceptionHandlerSignalKey] intValue]);
    }
    else
    {
        [exception raise];
    }
}

2 CFRunLoopObserverRef

iOS系統會監聽主線程中runloop的的進入/休眠、退出的activities 來處理autoreleasepool,也是同學們長討論的自動釋放池在什么時候釋放的問題。

 <CFRunLoopObserver 0x7fb064418b50 [0x10e005a40]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10e18c4c2), context = <CFArray 0x7fb0644189e0 [0x10e005a40]>{type = mutable-small, count = 0, values = ()}}
<CFRunLoopObserver 0x7fb064418bf0 [0x10e005a40]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10e18c4c2), context = <CFArray 0x7fb0644189e0 [0x10e005a40]>{type = mutable-small, count = 0, values = ()}}

2.1 CFRunLoopObserverRef函數

通過CFRunLoopObserverRef 我們可以監測當前runloop的運行狀態引用YYKit的寫法:其中優先級設置為最小的32位-0x7fffffff 和最大的32位0x7fffffff

static void YYRunloopAutoreleasePoolSetup() {
    CFRunLoopRef runloop = CFRunLoopGetCurrent();
    
    CFRunLoopObserverRef pushObserver;
    pushObserver = CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopEntry,
                                           true,         // repeat
                                           -0x7FFFFFFF,  // before other observers
                                           YYRunLoopAutoreleasePoolObserverCallBack, NULL);
    CFRunLoopAddObserver(runloop, pushObserver, kCFRunLoopCommonModes);
    CFRelease(pushObserver);
    
    CFRunLoopObserverRef popObserver;
    popObserver = CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopBeforeWaiting | kCFRunLoopExit,
                                          true,        // repeat
                                          0x7FFFFFFF,  // after other observers
                                          YYRunLoopAutoreleasePoolObserverCallBack, NULL);
    CFRunLoopAddObserver(runloop, popObserver, kCFRunLoopCommonModes);
    CFRelease(popObserver);
}

另外一種是block方式

// 創建observer
  CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"----監聽到RunLoop狀態發生改變---%zd", activity);
    });
    // 添加觀察者:監聽RunLoop的狀態
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);   
    // 釋放Observer
    CFRelease(observer);

2.2 利用空閑時間緩存數據

UITableView+FDTemplateLayoutCell的作者sunnyxx曾在優化UITableViewCell高度計算的那些事提到利用runloop來緩存cell的高度。

作者所說的代碼如下:



但是這段代碼在1.4版本之后就被去掉了,sunnyxx解釋是:


2.3 檢測UI卡頓

第一種方法通過子線程監測主線程的 runLoop,判斷兩個狀態區域之間的耗時是否達到一定閾值。ANREye就是在子線程設置flag 標記為YES, 然后在主線程中將flag設置為NO。利用子線程時闕值時長,判斷標志位是否成功設置成NO。

private class AppPingThread: Thread {
    
    func start(threshold:Double, handler: @escaping AppPingThreadCallBack) {
        self.handler = handler
        self.threshold = threshold
        self.start()
    }
    
    override func main() {
        
        while self.isCancelled == false {
            self.isMainThreadBlock = true
            DispatchQueue.main.async {
                self.isMainThreadBlock = false
                self.semaphore.signal()
            }
            
            Thread.sleep(forTimeInterval: self.threshold)
            if self.isMainThreadBlock  {
                self.handler?()
            }
            
            self.semaphore.wait(timeout: DispatchTime.distantFuture)
        }
    }
    
    private let semaphore = DispatchSemaphore(value: 0)
    
    private var isMainThreadBlock = false
    
    private var threshold: Double = 0.4
    
    fileprivate var handler: (() -> Void)?
}

NSRunLoop調用方法主要就是在kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之間,還有kCFRunLoopAfterWaiting之后,也就是如果我們發現這兩個時間內耗時太長,那么就可以判定出此時主線程卡頓,下面的代碼片段來源iOS實時卡頓監控

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    MyClass *object = (__bridge MyClass*)info;
    
    // 記錄狀態值
    object->activity = activity;
    
    // 發送信號
    dispatch_semaphore_t semaphore = moniotr->semaphore;
    dispatch_semaphore_signal(semaphore);
}
- (void)registerObserver
{
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                            kCFRunLoopAllActivities,
                                                            YES,
                                                            0,
                                                            &runLoopObserverCallBack,
                                                            &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    
    // 創建信號
    semaphore = dispatch_semaphore_create(0);
    
    // 在子線程監控時長
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (YES)
        {
            // 假定連續5次超時50ms認為卡頓(當然也包含了單次超時250ms)
            long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
            if (st != 0)
            {
                if (activity==kCFRunLoopBeforeSources || activity==kCFRunLoopAfterWaiting)
                {
                    if (++timeoutCount < 5)
                        continue;
                    
                    NSLog(@"好像有點兒卡哦");
                }
            }
            timeoutCount = 0;
        }
    });

第二種方式就是FPS監控,App 刷新率應該當努力保持在 60fps,通過CADisplayLink記錄兩次刷新時間間隔,就可以計算出當前的 FPS。

 _link = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkTick:)];
  [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];

- (void)displayLinkTick:(CADisplayLink *)link {
    if (lastTime == 0) {
        lastTime = link.timestamp;
        return;
    }
    count++;
    NSTimeInterval interval = link.timestamp - lastTime;
    if (interval < 1) return;
    lastTime = link.timestamp;
    float fps = count / interval;
    count = 0;
  NSString *text = [NSString stringWithFormat:@"%d FPS",(int)round(fps)];

3 CFRunLoopModeRef

每次啟動RunLoop時,只能指定其中一個 Mode,這個就是CurrentMode。要切換 Mode,只能退出 Loop,再重新指定一個 Mode 進入。系統默認注冊了5個mode,以下兩個是比較常用的:
1.kCFRunLoopDefaultMode (NSDefaultRunLoopMode),默認模式
2.UITrackingRunLoopMode, scrollview滑動時就是處于這個模式下。保證界面滑動時不受其他mode影響。
  CFRunLoop對外暴露的管理 Mode 接口只有下面2個:

CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set
    CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set
    ...
};

3.1 解決NSTime和scrollView糾葛

如果利用scrollView類型的做自動廣告滾動條 需要把定時器加入當前runloop的模式NSRunLoopCommonModes

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
    if (self.autoScroll) {
        [self invalidateTimer];
    }
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
    if (self.autoScroll) {
        [self setupTimer];
    }
}
 (void)setupTimer
{
    [self invalidateTimer]; 
    
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:self.autoScrollTimeInterval target:self selector:@selector(automaticScroll) userInfo:nil repeats:YES];
    _timer = timer;
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

- (void)invalidateTimer
{
    [_timer invalidate];
    _timer = nil;
}

3.2 RunLoopCommonModes

一個mode可以標記為common屬性(用CFRunLoopAddCommonMode函數),然后它就會保存在_commonModes。主線程有kCFRunLoopDefaultMode 和 UITrackingRunLoopMode 都已經是CommonModes了,而子線程只有kCFRunLoopDefaultMode。

_commonModeItems里面存放的source, observer, timer等,在每次runLoop運行的時候都會被同步到具有Common標記的Modes里。比如這樣:[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];就是把timer放到commonItem里。

kCFRunLoopCommonModes是一個占位用的mode,它不是真正意義上的mode。如果要在線程中開啟runloop,這樣寫是不對的:

[[NSRunLoop currentRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate distantFuture]];

上面的runMode beforeDate回調用CFrunloop的CFRunLoopRunSpecific函數,函數中回根據當前的name去查找當前的運營的mode,可是根本就不會存在CommonMode的。

image.png

3.3 TableView中實現平滑滾動延遲加載圖片

順帶提一下,這個我在開發中沒有用到。是利用CFRunLoopMode的特性,可以將圖片的加載放到NSDefaultRunLoopMode的mode里,這樣在滾動UITrackingRunLoopMode這個mode時不會被加載而影響到。這個主要受到Github的RunLoopWorkDistribution影響,

DWURunLoopWorkDistribution_demo.gif

其主要代碼片段如下:

- (instancetype)init
{
    if ((self = [super init])) {
        _maximumQueueLength = 30;
        _tasks = [NSMutableArray array];
        _tasksKeys = [NSMutableArray array];
        _timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(_timerFiredMethod:) userInfo:nil repeats:YES];
    }
    return self;
}
static void _registerObserver(CFOptionFlags activities, CFRunLoopObserverRef observer, CFIndex order, CFStringRef mode, void *info, CFRunLoopObserverCallBack callback) {
    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    CFRunLoopObserverContext context = {
        0,
        info,
        &CFRetain,
        &CFRelease,
        NULL
    };
    observer = CFRunLoopObserverCreate(     NULL,
                                            activities,
                                            YES,
                                            order,
                                            callback,
                                            &context);
    CFRunLoopAddObserver(runLoop, observer, mode);
    CFRelease(observer);
}

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

推薦閱讀更多精彩內容