深入淺出 RunLoop (1) — 核心機制

前言

這篇文章是兩年前在公司內部分享時候寫的,自己比較滿意文章的成色,就分享出來。

最近在進行社招的過程中,發現很多iOS五年以上的老司機對Runloop機制都不能很好的理解,而Runloop是iOS中非常重要的一個組件,很多性能優化,都需要對Runloop機制有深刻的理解才能進行。所以整理了以前的筆記,抽絲剝繭的分析Runloop的核心機制。

當有持續的異步任務需求時,我們會創建一個獨立的生命周期可控的線程。RunLoop就是控制線程生命周期并接收事件進行處理的機制。是iOS、OSX開發中比較基礎的一個概念。但它也是事件響應與任務處理的核心機制,貫穿整個系統。

RunLoop 基本概念

為什么引入RunLoop機制

一個線程一次只能執行一個任務,執行完成后線程就會退出。RunLoop可以讓線程隨時處理事件但不退出。

RunLoop并不是iOS平臺的專屬概念,在任何平臺的多線程編程中,為控制線程的生命周期,接收處理異步消息都需要類似RunLoop的循環機制實現,Android的Looper就是類似的機制。這種模型還通常被稱作Event Loop。Event Loop在很多系統和框架里都有實現。比如Node.js 的事件處理,Windows程序的消息循環。

實現這種模型的關鍵點在于:

  • 如何管理事件和消息
  • 如何讓線程在沒有處理消息時休眠,避免無謂的資源占用
  • 如何在有消息到來時立即被喚醒。

引入Runloop機制的目的是利用RunLoop機制的特點實現整體省電的效果,并且讓系統和應用可以流暢的運行,提高響應速度,達到極致的用戶體驗。

本質上RunLoop是什么

進程是一家工廠,線程是一個流水線,Run Loop就是流水線上的主管;當工廠接到商家的訂單分配給這個流水線時,Run Loop就啟動這個流水線,讓流水線動起來,生產產品;當產品生產完畢時,Run Loop就會暫時停下流水線,節約資源。
RunLoop管理流水線,流水線才不會因為無所事事被工廠銷毀;而不需要流水線時,就會辭退RunLoop這個主管,即退出線程,把所有資源釋放。

RunLoop實質上是一個對象,這個對象管理了其需要處理的的事件和消息,并提供了入口函數來執行Event Loop 的邏輯。線程執行這個函數后,會一直處于這個函數內部:接受消息->等待->執行 的循環中,直到這個循環結束。

iOS、OSX系統提供了兩個RunLoop系統:

  • Foundation: NSRunLoop 是基于CFRunLoopRef的封裝,提供了面向對象的API,這些API不是線程安全的
  • Core Foundation: CFRunLoopRef 核心部分,代碼開源,C 語言編寫,跨平臺。所有API都是線程安全的
    CFRunLoopRef源代碼: http://opensource.apple.com/tarballs/CF/

RunLoop的特性

  • 主線程的RunLoop在應用啟動的時候就會自動創建
  • 其他線程則需要在該線程下自己啟動
  • 不能直接創建RunLoop
  • RunLoop并不是線程安全的,所以需要避免在其他線程上調用當前線程的RunLoop
  • RunLoop負責管理autorelease pools
  • RunLoop負責處理消息事件,即輸入源事件和計時器事件

RunLoop與線程的關系

iOS開發中有兩個線程對象:pthread_t和NSThread。過去NSThread只是pthread_t的封裝,現在它們都是直接包裝自最底層的mach thread。

pthread_t同NSThread是一一對應的??梢酝ㄟ^ pthread_main_np() 或 [NSThread mainThread]獲取主線程。也可以通過 pthread_self() 或 [NSThread currentThread] 獲取當前線程。CFRunLoop是基于pthread來管理的,NSRunLoop是基于NSThread管理的。

蘋果不允許直接創建RunLoop,但是提供了兩個獲取RunLoop的函數。CFRunLoopGetMain()和CFRunLoopGetCurrent()。

/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 訪問 loopsDic 時的鎖
static CFSpinLock_t loopsLock;
 
/// 獲取一個 pthread 對應的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    
    if (!loopsDic) {
        // 第一次進入時,初始化全局Dic,并先為主線程創建一個 RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    
    /// 直接從 Dictionary 里獲取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
    
    if (!loop) {
        /// 取不到時,創建一個
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        /// 注冊一個回調,當線程銷毀時,順便也銷毀其對應的 RunLoop。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }
    
    OSSpinLockUnLock(&loopsLock);
    return loop;
}
 
CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
 
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}

線程和RunLoop之間是一一對應的,其關系是保存在一個全局的Dictionary里。線程剛創建時并沒有RunLoop,如果不主動獲取,一直不會有。RunLoop的創建是發生在第一次獲取時

RunLoop的內部組件

一個RunLoop可以包含多個Mode,每個Mode有包含多個Source、Timer、Observer。每次調用RunLoop主入口函數時,只能指定一個Mode,這個Mode被稱作CurrentMode。如果需要切換Mode,需要退出本次循環,再重新指定一個Mode進入。
Source、Timer、Observer 被統稱為 mode item,一個 item 可以被同時加入多個 mode。但一個 item 被重復加入同一個 mode 時是不會有效果的。如果一個 mode 中一個 item 都沒有,則 RunLoop 會直接退出,不進入循環。

Runloop mode

Source

事件源,在CF中的類型是 CFRunLoopSourceRef
事件源是事件產生的地方,有兩個類型:Source0 和 Source1。

  • Source0 只包含了一個回調函數,這個回調函數是一個函數指針,它不能主動觸發事件。在使用時,需要先調用CFRunLoopSourceSignal(source),將這個Source標記為待處理,然后手動調用,然后調用 CFRunLoopWakeUp(runloop) ,喚醒RunLoop。
  • Source1 包含了一個Mach_port 和一個回調函數。主要用于通過內核和其他線程相互發送消息。這種Source能主動喚醒RunLoop的線程。

Timer

計時器,在CF中的類型是 CFRunLoopTimerRef
CF計時器同NSTimer是toll-free bridged 的,可以互相轉換。
包含了一個時間長度和一個回調函數(IMP)。當它加入到RunLoop時,RunLoop會注冊對應的時間點,當時間點到時,RunLoop會被喚醒執行timer中的函數指針。

Observer

觀察者,在CF中的類型是CFRunLoopObserverRef
用于監聽RunLoop的狀態變化
包含了一個回調函數,當RunLoop的狀態發生變化的時候,觀察者可以通過回調接收到這個變化。

有以下狀態:

  • kCFRunLoopEntry 即將進入Loop
  • KCFRunLoopBeforeTimers 即將處理 Timer
  • KCFRunLoopBeforeSources 即將處理 Source
  • KCFRunLoopBeforeWaiting 即將進入休眠
  • KCFRunLoopAfterWaiting 從休眠中喚醒
  • KCFRunLoopExit 退出Loop
RunLoop狀態轉換

Mode

定義

RunLoop Mode就是流水線上支持生產的產品類型,流水線在一個時刻只能在一種模式下運行,生產某一類型的產品。mode item就是訂單。mode主要是為了分隔開不同組的Source、Timer、Observer,讓其互不影響。

結構

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
};
 
struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set
    CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set
};

Mode 類型

  • 1、Default:NSDefaultRunLoopMode,默認模式,在Run Loop沒有指定Mode的時候,默認就跑在Default Mode下
  • 2、Event tracking:UITrackingRunLoopMode,拖動事件
  • 3、Connection:NSConnectionReplyMode,用來監聽處理網絡請求NSConnection的事件
  • 4、Modal:NSModalPanelRunLoopMode,OS X的Modal面板事件
  • 5、 UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用。
  • 6、 GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode。
  • Common mode:NSRunLoopCommonModes,是一個模式集合,當綁定一個事件源到這個模式集合的時候就相當于綁定到了集合內的每一個模式。

Common Mode

將ModeName添加到RunLoop結構體中的commonModes集合中, 這個mode就具有Common屬性,成為了CommonMode。當RunLoop發生變化或者發生事件的時候,會將commonModeItems中的Source、Timer、Observer同步到具有Common屬性的Mode中。

主線程有兩個預置Mode。kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。這兩個Mode都已經具有Common屬性。timer加入到DefaultMode中,默認情況會被調用,但是當ScrollView滾動的時候,RunLoop會將Mode切換為UITrackingRunLoopMode,這是因為為了更好的用戶體驗,在主線程中Event tracking模式的優先級最高。這時timer不會被調用。如果需要這個timer在兩個Mode中都能得到調用,可以將timer分別加入這兩個Mode中。也可以將timer加入到commonModelItems中,這樣會被所有具有Common屬性的Mode調用。

解決方法:

NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                                   target:self
                                                 selector:@selector(timerHandler:)
                                                 userInfo:nil
                                                  repeats:YES];
                                                  
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

//或
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

[timer fire];

特定時間執行mode

RunLoop可以通過[acceptInputForMode:beforeDate:]和[runMode:beforeDate:]來指定在一段時間內的運行模式。如果不指定的話,RunLoop默認會運行在Default下(不斷重復調用runMode:NSDefaultRunLoopMode beforDate:)

RunLoop 實現邏輯分析

RunLoop的內部運行邏輯圖

RunLoop核心代碼

// 用DefaultMode啟動
void CFRunLoopRun(void) {
    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 0, false);
}
 
// 用指定的Mode啟動,允許設置RunLoop超時時間
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
 
// RunLoop的實現
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
    
    // 根據modeName找到對應mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
    // 如果mode里沒有source、timer或者observer, 直接返回。
    if (__CFRunLoopModeIsEmpty(currentMode)) return;
    
    // 1. 通知 Observers: RunLoop 即將進入 loop。
    // Observer會創建AutoReleasePool _objc_autoreleasePoolPush()
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
    
    // 進入loop
    __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
        
        Boolean sourceHandledThisLoop = NO;
        int retVal = 0;
        do {
            // 2. 通知 Observers: RunLoop 即將觸發 Timer 回調。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
            // 3. 通知 Observers: RunLoop 即將觸發 Source0 (非port) 回調。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
            // 執行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);
            
            // 4. RunLoop 觸發 Source0 (非port) 回調。
            sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
            // 執行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);
 
            // 5. 如果有 Source1 (基于port) 處于 ready 狀態,直接處理這個 Source1 然后跳轉去處理消息。
            if (__Source0DidDispatchPortLastTime) {
                Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
                if (hasMsg) goto handle_msg;
            }
            
            // 通知 Observers: RunLoop 的線程即將進入休眠(sleep)。
            if (!sourceHandledThisLoop) {
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
            }
            
            // 7. 調用 mach_msg 接收 mach_port 的消息。線程將進入休眠, 直到被下面某一個事件喚醒。
            //  基于 port 的Source 的事件
            //  Timer 的執行時間到了
            //  RunLoop 自身的超時時間到了
            //  被其他調用者手動喚醒
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
                mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
            }
 
            // 8. 通知 Observers: RunLoop 的線程剛剛被喚醒了。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
            
            // 收到消息,處理消息。
            handle_msg:
 
            // 9.1 如果一個 Timer 到時間了,觸發這個Timer的回調。
            if (msg_is_timer) {
                __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
            } 
 
            // 9.2 如果有dispatch到main_queue的block,執行block。
            else if (msg_is_dispatch) {
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } 
 
            // 9.3 如果一個 Source1 (基于port) 發出事件了,處理這個事件
            else {
                CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
                sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
                if (sourceHandledThisLoop) {
                    mach_msg(reply, MACH_SEND_MSG, reply);
                }
            }
            
            // 執行加入到Loop的block
            __CFRunLoopDoBlocks(runloop, currentMode);
            
 
            if (sourceHandledThisLoop && stopAfterHandle) {
                // 進入loop時參數說處理完事件就返回。
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout) {
                // 超出傳入參數標記的超時時間了
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(runloop)) {
                // 被外部調用者強制停止了
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                // source/timer/observer一個都沒有了
                retVal = kCFRunLoopRunFinished;
            }
            
            // 如果沒超時,mode里沒空,loop也沒被停止,那繼續loop。
        } while (retVal == 0);
    }
    
    // 10. 通知 Observers: RunLoop 即將退出。
    // observer 釋放AutoReleasePool _objc_autoreleasePoolPop()
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}

實際上 RunLoop 其內部是一個 do-while 循環。RunLoop的核心就是一個MachMessage的調用過程,RunLoop調用這個函數去接收消息,如果沒有別人發送port消息過來,RunLoop會進入休眠狀態。內核會將線程置于等待狀態,這個時候whild循環是停止的,相比于一直運行的while循環,會很節省CPU資源,進而達到省電的目的。
因為Source1注冊了MachPort端口,當有Source1事件進來的時候會通過這個port端口喚醒RunLoop繼續執行,當計時器到了時候也會喚醒RunLoop,RunLoop喚醒后會先通過Observer 當前已經處于喚醒狀態,之后會先執行Source1事件,執行完成后再執行timer、Source0。執行完所有的Source0事件后,如果有Source1事件則執行Source1,如果沒有則通知Observe 進入到休眠狀態。完成一個循環。

RunLoop 底層機制分析

操作系統架構

iOS/OSX操作系統核心是Darwin,Darwin包括了硬件層、XNU內核和Darwin庫等組成部分。
XNU內核由IOKit、BSD、Mach三部分組成。

  • IOKit 主要負責IO提供硬件通訊功能
  • BSD,是一個類Unix系統,負責進程管理、文件系統、網絡等功能。
  • Mach是微內核,提供處理器調度、IPC等基礎服務。

Mach通訊

RunLoop的底層實現基于Mach內核通訊實現的。在Mach中,所有的組件都是一個對象。進程、線程、虛擬內存都是對象Mach對象之間不能直接調用,對象間的通訊是通過消息機制實現的。
Mach 的 IPC 的核心就是消息(message)在兩個端口(port)之間傳遞。
消息實質上是一個二進制數據包,在頭部定義了當前端口和目標端口。

消息的發送接收通過mach_msg()函數實現,mach_msg()函數內部會通過調用mach_msg_trap()函數將現在的用戶態切換到內核態。進入到內核態后,會調用Mach內核的mach_msg()函數完成消息傳遞工作,注意此mach_msg和第一個mach_msg雖然函數名相同,但是實現和意義是完全不同的。

RunLoop的核心機制就是通過調用 mach_msg 等待接受 mach_port 的消息。線程進入休眠, 直到被下面某一個事件喚醒。事件通過mach_port 發送 mach_msg 喚醒RunLoop。

總結

RunLoop是一個do-while 循環,又不是一個do-while 循環。他的工作模式是一個循環,但是他基于mach_port和mach_msg的 休眠\喚醒 機制確保了他可以在無任務的時候休眠,有任務的時候及時喚醒,相比于一個普通循環,不會空轉,不會浪費系統資源。RunLoop又通過不同的工作mode隔離了不同的事件源,使他們的工作互不影響。這才是RunLoop實現省電,流暢,響應速度快,用戶體驗好的根本原因;進而基于RunLoop的組件如計時器、GCD、界面更新、自動釋放池能高效運轉的根本原因。

面試考察點

  • 為什么引入Runloop機制,有什么作用或者好處?

引入Runloop機制的目的是利用RunLoop機制的特點實現整體省電的效果,并且讓系統和應用可以流暢的運行,提高響應速度,達到極致的用戶體驗。

  • 為什么省電?

主要有兩點:一、因為不做任何操作的時候主線程Runloop會處于退出狀態,不會執行任何空轉邏輯,不執行代碼自然不消耗CPU資源,自然省電。二、Runloop提供一種班車機制,限制如頁面刷新等任務的執行頻率,一次Runloop只執行一次,防止多次重復執行代碼帶來的性能損耗。

  • 為什么可以流程運行?

一個app流暢與否的決定性因素是主線程的阻塞率,在iOS系統中runloop每秒執行60次,理論上主線程runloop達到55幀以上的刷新頻率用戶就感覺不到卡頓。

Mode機制,同一時間只執行一個Mode內的Source或者Timer,比如拖動的時候只指定拖動Mode,其他Mode 如Default Mode中的源不會被執行,確保了高速滑動的時候不會有其他邏輯阻礙主線程刷新。

Runloop做的是管理視圖刷新頻率,防止重復運算。由于視圖更新必須在主線程,視圖的重布局和重繪都會占用主線程的執行時間,一次Runloop循環只執行一次可以最大限度的防止重復運算導致的計算浪費。

管理核心動畫。核心動畫有三個樹,其中render tree 是私有的,應用開發無法訪問到。render tree在專用的render server 進程中執行,是真正用來渲染動畫的地方,線程優先級高于主線程。所以即使app主線程阻塞,也不會影響到動畫的繪制工作。既節省了主線程的計算資源,又使動畫可以流暢的執行。

支持異步方法調用,將耗時操作分發到子線程中進行。RunLoop是performSelector的基礎設施。我們使用 performSelector:onThread: 或者 performSelecter:afterDelay: 時,實際上系統會創建一個Timer并添加到當前線程的RunLoop中。

還有其他的點,這里不展開,詳情可閱讀下文應用實踐。

當然Runloop不是萬能的,如果代碼質量差,在一次Runloop循環中執行的時間過久一樣會導致卡頓,所以解決卡頓問題也是程序員能力的體現。

  • 如何提高響應速度?

當發生系統事件時,如觸碰事件,系統通過Mach Port 發送 Mach消息主動喚醒Runloop。Mach是搶占式操作系統內核,Mach系統IPC機制就是依靠消息機制實現的,所以效率非常高。

iOS系統中RunLoop的應用實踐在下一篇文章中闡述:深入淺出 RunLoop (2) — 應用實踐

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

推薦閱讀更多精彩內容