iOS 事件處理機制與圖像渲染過程

iOS 事件處理機制與圖像渲染過程
原創 2015-11-19 ted WeMobileDev
致歉聲明:
Peter在開發公眾號功能時觸發了一個bug,導致群發錯誤。對此我們深表歉意,并果斷開除了Peter。以下交回給正文時間:

iOS 事件處理機制與圖像渲染過程
iOS RunLoop都干了什么

iOS 為什么必須在主線程中操作UI

事件響應

CALayer

CADisplayLink 和 NSTimer

iOS 渲染過程

渲染時機

CPU 和 GPU渲染

Core Animation

Facebook Pop介紹

AsyncDisplay介紹

參考文章

iOS RunLoop都干了什么
RunLoop是一個接收處理異步消息事件的循環,一個循環中:等待事件發生,然后將這個事件送到能處理它的地方。

如圖1-1所示,描述了一個觸摸事件從操作系統層傳送到應用內的main runloop中的簡單過程。

圖1-1
簡單的說,RunLoop是事件驅動的一個大循環,如下代碼所示

**int main(int argc, char * argv[]) { 
//程序一直運行狀態 while (AppIsRunning) { 
//睡眠狀態,等待喚醒事件 id whoWakesMe = SleepForWakingU p(); 
//得到喚醒事件 id event = GetEvent(whoWakesMe); 
//開始處理事件 HandleEvent(event); } return 0;}

RunLoop主要處理以下6類事件:

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();

Observer事件,runloop中狀態變化時進行通知。(微信卡頓監控就是利用這個事件通知來記錄下最近一次main runloop活動時間,在另一個check線程中用定時器檢測當前時間距離最后一次活動時間過久來判斷在主線程中的處理邏輯耗時和卡主線程)。這里還需要特別注意,CAAnimation是由RunloopObserver觸發回調來重繪,接下來會講到。

Block事件,非延遲的NSObject PerformSelector立即調用,dispatch_after立即調用,block回調。

Main_Dispatch_Queue事件:GCD中dispatch到main queue的block會被dispatch到main loop執行。

Timer事件:延遲的NSObject PerformSelector,延遲的dispatch_after,timer事件。

Source0事件:處理如UIEvent,CFSocket這類事件。需要手動觸發。觸摸事件其實是Source1接收系統事件后在回調 __IOHIDEventSystemClientQueueCallback() 內觸發的 Source0,Source0 再觸發的 _UIApplicationHandleEventQueue()。source0一定是要喚醒runloop及時響應并執行的,如果runloop此時在休眠等待系統的 mach_msg事件,那么就會通過source1來喚醒runloop執行。

Source1事件:處理系統內核的mach_msg事件。(推測CADisplayLink也是這里觸發)。

RunLoop執行順序的偽代碼

**SetupThisRunLoopRunTimeoutTimer(); 
// by GCD timer
//通知即將進入runloop__CFRUNLLOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(KCFRunLoopEntry);
do { __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
 __CFRunLoopDoObservers(kCFRunLoopBeforeSources); 
__CFRunLoopDoBlocks(); //一個循環中會調用兩次,確保非延遲的NSObject PerformSelector調用和非延遲的dispatch_after調用在當前runloop執行。還有回調block __CFRunLoopDoSource0(); 
//例如UIKit處理的UIEvent事件 CheckIfExistMessagesInMainDispatchQueue(); 
//GCD dispatch main queue __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting); 

//即將進入休眠,會重繪一次界面 var wakeUpPort = SleepAndWaitForWakingUpPorts(); 
// mach_msg_trap,陷入內核等待匹配的內核mach_msg事件 // Zzz... 
// Received mach_msg, wake up __CFRunLoopDoObservers(kCFRunLoopAfterWaiting); 
// Handle msgs if (wakeUpPort == timerPort) { __CFRunLoopDoTimers(); } 
else if (wakeUpPort == mainDispatchQueuePort) { //GCD當調用dispatch_async(dispatch_get_main_queue(),block)時,libDispatch會向主線程的runloop發送mach_msg消息喚醒runloop,并在這里執行。這里僅限于執行dispatch到主線程的任務,dispatch到其他線程的仍然是libDispatch來處理
。 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() } 
else { __CFRunLoopDoSource1(); 
//CADisplayLink是source1的mach_msg觸發? } __CFRunLoopDoBlocks();}
 while (!stop && !timeout);
//通知observers,即將退出runloop__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBERVER_CALLBACK_FUNCTION__(CFRunLoopExit);
**
結合上面的Runloop事件執行順序,思考下面代碼邏輯中為什么可以標識tableview是否reload完成
dispatch_async(dispatch_get_main_queue(), ^{ _isReloadDone = NO; [tableView reload]; //會自動設置tableView layoutIfNeeded為YES,意味著將會在runloop結束時重繪table dispatch_async(dispatch_get_main_queue(),^{ _isReloadDone = YES; });});

**提示:**這里在GCD dispatch main queue中插入了兩個任務,一次RunLoop有兩個機會執行GCD dispatch main queue中的任務,分別在休眠前和被喚醒后。

iOS 為什么必須在主線程中操作UI

因為UIKit不是線程安全的。試想下面這幾種情況:
兩個線程同時設置同一個背景圖片,那么很有可能因為當前圖片被釋放了兩次而導致應用崩潰。

兩個線程同時設置同一個UIView的背景顏色,那么很有可能渲染顯示的是顏色A,而此時在UIView邏輯樹上的背景顏色屬性為B。

兩個線程同時操作view的樹形結構:在線程A中for循環遍歷并操作當前View的所有subView,然后此時線程B中將某個subView直接刪除,這就導致了錯亂還可能導致應用崩潰。iOS4之后蘋果將大部分繪圖的方法和諸如 UIColor 和 UIFont 這樣的類改寫為了線程安全可用,但是仍然強烈建議講UI操作保證在主線程中執行。

事件響應

蘋果注冊了一個 Source1 (基于 mach port 的) 用來接收系統事件,其回調函數為 __IOHIDEventSystemClientQueueCallback()。
當一個硬件事件(觸摸/鎖屏/搖晃等)發生后,首先由 IOKit.framework 生成一個 IOHIDEvent 事件并由 SpringBoard 接收。
SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸,加速,接近傳感器等幾種 Event,隨后用 mach port 轉發給需要的App進程。
隨后蘋果注冊的那個 Source1 就會觸發回調,并調用 _UIApplicationHandleEventQueue() 進行應用內部的分發。
_UIApplicationHandleEventQueue() 會把 IOHIDEvent 處理并包裝成 UIEvent 進行處理或分發,其中包括識別 UIGesture/處理屏幕旋轉/發送給 UIWindow 等。通常事件比如 UIButton 點擊、touchesBegin/Move/End/Cancel 事件都是在這個回調中完成的。

CALayer

在iOS當中,所有的視圖都從一個叫做UIVIew的基類派生而來,UIView可以處理觸摸事件,可以支持基于Core Graphics繪圖,可以做仿射變換(例如旋轉或者縮放),或者簡單的類似于滑動或者漸變的動畫。
CALayer類在概念上和UIView類似,同樣也是一些被層級關系樹管理的矩形塊,同樣也可以包含一些內容(像圖片,文本或者背景色),管理子圖層的位置。它們有一些方法和屬性用來做動畫和變換。和UIView最大的不同是CALayer不處理用戶的交互。CALayer并不清楚具體的響應鏈。
UIView和CALayer是一個平行的層級關系,每一個UIView都有一個CALayer實例的圖層屬性,也就是所謂的backing layer,視圖的職責就是創建并管理這個圖層,以確保當子視圖在層級關系中添加或者被移除的時候,他們關聯的圖層也同樣對應在層級關系樹當中有相同的操作。實際上這些背后關聯的Layer圖層才是真正用來在屏幕上顯示和做動畫,UIView僅僅是對它的一個封裝,提供了一些iOS類似于處理觸摸的具體功能,以及Core Animation底層方法的高級接口。
UIView 的 Layer 在系統內部,被維護著三份同樣的樹形數據結構,分別是:
圖層樹(這里是代碼可以操縱的,設置屬性的最終值會立刻在這里更新);

呈現樹(是一個中間層,系統就在這一層上更改屬性,進行各種渲染操作。比如一個動畫是更改alpha值從0到1,那么在邏輯樹上此屬性會被立刻更新為最終屬性1,而在動畫樹上會根據設置的動畫時間從0逐步變化到1);

渲染樹(其屬性值就是當前正被顯示在屏幕上的屬性值);

CADisplayLink 和 NSTimer

NSTimer 其實就是 CFRunLoopTimerRef。一個 NSTimer 注冊到 RunLoop 后,RunLoop 會為其重復的時間點注冊好事件。
RunLoop為了節省資源,并不會在非常準確的時間點回調這個Timer。Timer 有個屬性叫做 Tolerance (寬容度),標示了當時間點到后,容許有多少最大誤差。如果某個時間點被錯過了,例如執行了一個很長的任務,則那個時間點的回調也會跳過去,不會延后執行。
RunLoop 是用GCD的 dispatch_source_t 實現的 Timer。 當調用 NSObject 的 performSelecter:afterDelay: 后,實際上其內部會創建一個 Timer 并添加到當前線程的 RunLoop 中。所以如果當前線程沒有 RunLoop,則這個方法會失效。當調用 performSelector:onThread: 時,實際上其會創建一個 Timer 加到對應的線程去,同樣的,如果對應線程沒有 RunLoop 該方法也會失效。
CADisplayLink 是一個和屏幕刷新率(每秒刷新60次)一致的定時器(但實際實現原理更復雜,和 NSTimer 并不一樣,其內部實際是操作了一個 Source)。如果在兩次屏幕刷新之間執行了一個長任務,那其中就會有一幀被跳過去,造成界面卡頓的感覺。
iOS 渲染過程


圖2-1
通常來說,計算機系統中 CPU、GPU、顯示器是以上面這種方式協同工作的。CPU 計算好顯示內容提交到 GPU,GPU 渲染完成后將渲染結果放入幀緩沖區,隨后視頻控制器會按照 VSync 信號如下圖1-4所示,逐行讀取幀緩沖區的數據,經過可能的數模轉換傳遞給顯示器顯示。


圖2-2
在 VSync 信號到來后,系統圖形服務會通過 CADisplayLink 等機制通知 App,App 主線程開始在 CPU 中計算顯示內容,比如視圖的創建、布局計算、圖片解碼、文本繪制等。隨后 CPU 會將計算好的內容提交到 GPU 去,由 GPU 進行變換、合成、渲染。隨后 GPU 會把渲染結果提交到幀緩沖區去,等待下一次 VSync 信號到來時顯示到屏幕上。由于垂直同步的機制,如果在一個 VSync 時間內,CPU 或者 GPU 沒有完成內容提交,則那一幀就會被丟棄,等待下一次機會再顯示,而這時顯示屏會保留之前的內容不變。這就是界面卡頓的原因。從上圖中可以看到,CPU 和 GPU 不論哪個阻礙了顯示流程,都會造成掉幀現象。所以開發時,也需要分別對 CPU 和 GPU 壓力進行評估和優化。

iOS 的顯示系統是由 VSync 信號驅動的,VSync 信號由硬件時鐘生成,每秒鐘發出 60 次(這個值取決設備硬件,比如 iPhone 真機上通常是 59.97)。iOS 圖形服務接收到 VSync 信號后,會通過 IPC 通知到 App 內。App 的 Runloop 在啟動后會注冊對應的 CFRunLoopSource 通過 mach_port 接收傳過來的時鐘信號通知,隨后 Source 的回調會驅動整個 App 的動畫與顯示。
Core Animation 在 RunLoop 中注冊了一個 Observer,監聽了 BeforeWaiting 和 Exit 事件。當一個觸摸事件到來時,RunLoop 被喚醒,App 中的代碼會執行一些操作,比如創建和調整視圖層級、設置 UIView 的 frame、修改 CALayer 的透明度、為視圖添加一個動畫;這些操作最終都會被 CALayer 標記,并通過 CATransaction 提交到一個中間狀態去。當上面所有操作結束后,RunLoop 即將進入休眠(或者退出)時,關注該事件的 Observer 都會得到通知。這時 Core Animation 注冊的那個 Observer 就會在回調中,把所有的中間狀態合并提交到 GPU 去顯示;如果此處有動畫,通過 DisplayLink 穩定的刷新機制會不斷的喚醒runloop,使得不斷的有機會觸發observer回調,從而根據時間來不斷更新這個動畫的屬性值并繪制出來。
為了不阻塞主線程,Core Animation 的核心是 OpenGL ES 的一個抽象物,所以大部分的渲染是直接提交給GPU來處理。 而Core Graphics/Quartz 2D的大部分繪制操作都是在主線程和CPU上同步完成的,比如自定義UIView的drawRect里用CGContext來畫圖。

渲染時機
上面已經提到過:Core Animation 在 RunLoop 中注冊了一個 Observer 監聽 BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop) 事件 。當在操作 UI 時,比如改變了 Frame、更新了 UIView/CALayer 的層次時,或者手動調用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,這個 UIView/CALayer 就被標記為待處理,并被提交到一個全局的容器去。當Oberver監聽的事件到來時,回調執行函數中會遍歷所有待處理的UIView/CAlayer 以執行實際的繪制和調整,并更新 UI 界面。
這個函數內部的調用棧大概是這樣的:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv() 
QuartzCore:CA::Transaction::observer_callback:
 CA::Transaction::commit(); 
CA::Context::commit_transaction(); 
CA::Layer::layout_and_display_if_needed();
 CA::Layer::layout_if_needed();
 [CALayer layoutSublayers];
 [UIView layoutSubviews]; 
CA::Layer::display_if_needed(); 
[CALayer display]; 
[UIView drawRect];

CPU 和 GPU渲染
OpenGL中,GPU屏幕渲染有以下兩種方式:
On-Screen Rendering意為當前屏幕渲染,指的是GPU的渲染操作是在當前用于顯示的屏幕緩沖區中進行。

Off-Screen Rendering意為離屏渲染,指的是GPU在當前屏幕緩沖區以外新開辟一個緩沖區進行渲染操作。按照這樣的說法,如果將不在GPU的當前屏幕緩沖區中進行的渲染都稱為離屏渲染,那么就還有另一種特殊的“離屏渲染”方式:CPU渲染。如果我們重寫了drawRect方法,并且使用任何Core Graphics的技術進行了繪制操作,就涉及到了CPU渲染。整個渲染過程由CPU在App內同步地完成,渲染得到的bitmap最后再交由GPU用于顯示。

相比于當前屏幕渲染,離屏渲染的代價是很高的,主要體現在兩個方面:
創建新緩沖區要想進行離屏渲染,首先要創建一個新的緩沖區。

上下文切換離屏渲染的整個過程,需要多次切換上下文環境:先是從當前屏幕(On-Screen)切換到離屏(Off-Screen);等到離屏渲染結束以后,將離屏緩沖區的渲染結果顯示到屏幕上有需要將上下文環境從離屏切換到當前屏幕。而上下文環境的切換是要付出很大代價的。

設置了以下屬性時,都會觸發離屏繪制:
shouldRasterize(光柵化)

masks(遮罩)

shadows(陰影)

edge antialiasing(抗鋸齒)

group opacity(不透明)需要注意的是,如果shouldRasterize被設置成YES,在觸發離屏繪制的同時,會將光柵化后的內容緩存起來,如果對應的layer及其sublayers沒有發生改變,在下一幀的時候可以直接復用。這將在很大程度上提升渲染性能。而其它屬性如果是開啟的,就不會有緩存,離屏繪制會在每一幀都發生。

在開發時需要根據實際情況來選擇最優的實現方式,盡量使用On-Screen Rendering。簡單的Off-Screen Rendering可以考慮使用Core Graphics讓CPU來渲染。
Core Animation
1. 隱式動畫
隱式動畫是系統框架自動完成的。Core Animation在每個runloop周期中自動開始一次新的事務,即使你不顯式的用[CATransaction begin]開始一次事務,任何在一次runloop循環中屬性的改變都會被集中起來,然后做一次0.25秒的動畫。在iOS4中,蘋果對UIView添加了一種基于block的動畫方法:+animateWithDuration:animations:。這樣寫對做一堆的屬性動畫在語法上會更加簡單,但實質上它們都是在做同樣的事情。CATransaction的+begin和+commit方法在+animateWithDuration:animations:內部自動調用,這樣block中所有屬性的改變都會被事務所包含。
Core Animation通常對CALayer的所有屬性(可動畫的屬性)做動畫,但是UIView是怎么把它關聯的圖層的這個特性關閉了呢?每個UIView對它關聯的圖層都扮演了一個委托,并且提供了-actionForLayer:forKey的實現方法。當不在一個動畫塊的實現中,UIView對所有圖層行為返回nil,但是在動畫block范圍之內,它就返回了一個非空值。

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *layerView;
@end
@implementation ViewController
- (void)viewDidLoad{ [super viewDidLoad]; 
//test layer action when outside of animation block
 NSLog(@"Outside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]);
 //begin animation block [UIView beginAnimations:nil context:nil]; 
//test layer action when inside of animation block 
NSLog(@"Inside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]); 
//end animation block [UIView commitAnimations];}
@end$ 
LayerTest[21215:c07] Outside: <null>$ 
LayerTest[21215:c07] Inside: <CABasicAnimation: 0x757f090>

2. 顯式動畫
Core Animation提供的顯式動畫類型,既可以直接對退曾屬性做動畫,也可以覆蓋默認的圖層行為。我們經常使用的CABasicAnimation,CAKeyframeAnimation,CATransitionAnimation,CAAnimationGroup等都是顯式動畫類型,這些CAAnimation類型可以直接提交到CALayer上。
無論是隱式動畫還是顯式動畫,提交到layer后,經過一系列處理,最后都經過上文描述的繪制過程最終被渲染出來。
Facebook Pop介紹
在計算機的世界里面,其實并不存在絕對連續的動畫,你所看到的屏幕上的動畫本質上都是離散的,只是在一秒的時間里面離散的幀多到一定的數量人眼就覺得是連續的了,
在iOS中,最大的幀率是60幀每秒。 iOS提供了Core Animation框架,只需要開發者提供關鍵幀信息,比如提供某個animatable屬性終點的關鍵幀信息,然后中間的值則通過一定的算法進行插值計算,從而實現補間動畫。 Core Aniamtion中進行插值計算所依賴的時間曲線由CAMediaTimingFunction提供。
Pop Animation在使用上和Core Animation很相似,都涉及Animation對象以及Animation的載體的概念,不同的是Core Animation的載體只能是CALayer,而Pop Animation可以是任意基于NSObject的對象。當然大多數情況Animation都是界面上顯示的可視的效果,所以動畫執行的載體一般都直接或者間接是UIView或者CALayer。
但是如果你只是想研究Pop Animation的變化曲線,你也完全可以將其應用于一個普通的數據對象。Pop Animation應用于CALayer時,在動畫運行的任何時刻,layer和其presentationLayer的相關屬性值始終保持一致,而Core Animation做不到。 Pop Animation可以應用任何NSObject的對象,而Core Aniamtion必須是CALayer。
下面這個例子就是自定義Pop readBlock和writeBlock處理自定義的動畫屬性:

prop = [POPAnimatableProperty propertyWithName:@"com.foo.radio.volume" initializer:^(POPMutableAnimatableProperty *prop) { 
// read value prop.readBlock = ^(id obj, CGFloat values[]) { values[0] = [obj volume]; };
 // write value prop.writeBlock = ^(id obj, const CGFloat values[]) { [obj setVolume:values[0]]; }; 
// dynamics threshold prop.threshold = 0.01;}];
POPSpringAnimation *anim = [POPSpringAnimation animation];
anim.property = prop;

Pop實現依賴的核心就是CADisplayLink。
最后附上一篇介紹Facebook Pop如何使用的文章 《Introducing Facebook Pop》
AsyncDisplay介紹
阻塞主線程的繪制任務主要是這三大類:Layout計算視圖布局文本寬高、Rendering文本渲染圖片解碼圖片繪制、UIKit對象創建更新釋放。除了UIKit和CoreAnimation相關操作必須在主線程中進行,其他的都可以挪到后臺線程異步執行。
AsyncDisplay通過抽象UIView的關系創建了ASDisplayNode類,ASDisplayNode是線程安全的,它可以在后臺線程創建和修改。Node 剛創建時,并不會在內部新建 UIView 和 CALayer,直到第一次在主線程訪問 view 或 layer 屬性時,它才會在內部生成對應的對象。當它的屬性(比如frame/transform)改變后,它并不會立刻同步到其持有的 view 或 layer 去,而是把被改變的屬性保存到內部的一個中間變量,稍后在需要時,再通過某個機制一次性設置到內部的 view 或 layer。從而可以實現異步并發操作。
AsyncDisplay實現依賴如同Core Animation在runloop中注冊observer事件來觸發。同樣附上一篇介紹AsyncDisplay的好文 《iOS保持界面流暢的技巧和AsyncDisplay介紹》
參考文章
runloop原理 (https://github.com/ming1016/study/wiki/CFRunLoop)

深入理解runloop (http://blog.ibireme.com/2015/05/18/runloop/)

線程安全類的設計 (http://objccn.io/issue-2-4/)

iOS保持界面流暢的技巧和AsyncDisplay介紹 (http://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/

離屏渲染 (http://foggry.com/blog/2015/05/06/chi-ping-xuan-ran-xue-xi-bi-ji/)

ios核心動畫高級技巧 (https://zsisme.gitbooks.io/ios-/content/index.html)

微信掃一掃關注該公眾號

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

推薦閱讀更多精彩內容