iOS 從源碼解析Run Loop :run loop 基本概念理解篇

Run loops 是與 threads 關(guān)聯(lián)的基本基礎(chǔ)結(jié)構(gòu)的一部分松嘶。Run loop 是一個(gè) event processing loop (事件處理循環(huán))涝开,可用于計(jì)劃工作并協(xié)調(diào)收到的事件的接收。Run loop 的目的是讓 thread 在有工作要做時(shí)保持忙碌,而在沒有工作時(shí)讓 thread 進(jìn)入睡眠狀態(tài)。(官方解釋初次看時(shí)顯的過于生澀,不過我們?nèi)匀豢梢宰プ∫恍╆P(guān)鍵點(diǎn)锭汛,原本我們的 thread 執(zhí)行完任務(wù)后就要釋放銷毀了笨奠,但是在 run loop 的加持下,線程不再自己主動(dòng)去銷毀而是處于待命狀態(tài)等待著我們?cè)俳唤o它任務(wù)唤殴,換句話說就是 run loop 使我的線程保持了活性般婆,下面我們?cè)噲D對(duì) run loop 的概念進(jìn)行理解。)

runloop 概念

一般來講朵逝,一個(gè)線程一次只能執(zhí)行一個(gè)任務(wù)蔚袍,執(zhí)行完成后線程就會(huì)退出。如果我們需要一個(gè)機(jī)制配名,讓線程能隨時(shí)處理事件但并不退出啤咽,這種模型通常被稱作 Event Loop。 Event Loop 在很多系統(tǒng)和框架里都有實(shí)現(xiàn)渠脉,比如 Node.js 的事件處理宇整,比如 Windows 程序的消息循環(huán),再比如 OSX/iOS 里的 Run Loop芋膘。實(shí)現(xiàn)這種模型的關(guān)鍵點(diǎn)在于基于消息機(jī)制:管理事件/消息鳞青,讓線程在沒有消息需時(shí)休眠以避免資源占用、在有消息到來時(shí)立刻被喚醒執(zhí)行任務(wù)为朋。

那什么是 run loop臂拓?顧名思義,run loop 就是在 “跑圈”习寸,run loop 運(yùn)行的核心代碼是一個(gè)有狀態(tài)的 do while 循環(huán)胶惰,每循環(huán)一次就相當(dāng)于跑了一圈,線程就會(huì)對(duì)當(dāng)前這一圈里面產(chǎn)生的事件進(jìn)行處理霞溪,do while 循環(huán)我們可能已經(jīng)寫過無數(shù)次孵滞,當(dāng)然我們?nèi)粘T诤瘮?shù)中寫的都是會(huì)明確結(jié)束的循環(huán),并且循環(huán)的內(nèi)容是我們一開始就編寫好的威鹿,我們并不能動(dòng)態(tài)的改變或者插入循環(huán)的內(nèi)容剃斧,只要不是超時(shí)或者故意退出狀態(tài)下 run loop 就會(huì)一直執(zhí)行 do while 循環(huán),所以可以保證線程不退出忽你,并且可以讓我們根據(jù)自己需要向線程中添加任務(wù)。

那么為什么線程要有 run loop 呢臂容?其實(shí)我們的 APP 可以理解為是靠 event 驅(qū)動(dòng)的(包括 iOS 和 Android 應(yīng)用)科雳。我們觸摸屏幕、網(wǎng)絡(luò)回調(diào)等都是一個(gè)個(gè)的 event脓杉,也就是事件糟秘。這些事件產(chǎn)生之后會(huì)分發(fā)給我們的 APP,APP 接收到事件之后分發(fā)給對(duì)應(yīng)的線程球散。通常情況下尿赚,如果線程沒有 run loop,那么一個(gè)線程一次只能執(zhí)行一個(gè)任務(wù),執(zhí)行完成后線程就會(huì)退出凌净。要想 APP 的線程一直能夠處理事件或者等待事件(比如異步事件)悲龟,就要保活線程冰寻,也就是不能讓線程早早的退出须教,此時(shí) run loop 就派上用場了,其實(shí)也不是必須要給線程指定一個(gè) run loop斩芭,如果需要我們的線程能夠持續(xù)的處理事件轻腺,那么就需要給線程綁定一個(gè) run loop。也就是說划乖,run loop 能夠保證線程一直可以處理事件贬养。

通常情況下,事件并不是永無休止的產(chǎn)生琴庵,所以也就沒必要讓線程永無休止的運(yùn)行误算,run loop 可以在無事件處理時(shí)進(jìn)入休眠狀態(tài),避免無休止的 do while 跑空圈细卧,看到這里我們注意到線程和 run loop 都是能進(jìn)入休眠狀態(tài)的尉桩,這里為了便于理解概念我們看一些表示 run loop 運(yùn)行狀態(tài)的代碼:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), // 進(jìn)入 Run Loop 循環(huán) (這里其實(shí)還沒進(jìn)入)
    kCFRunLoopBeforeTimers = (1UL << 1), // Run Loop 即將開始處理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // Run Loop 即將開始處理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // Run Loop 即將進(jìn)入休眠
    kCFRunLoopAfterWaiting = (1UL << 6), // Run Loop 從休眠狀態(tài)喚醒
    kCFRunLoopExit = (1UL << 7), // Run Loop 退出(和 kCFRunLoop Entry 對(duì)應(yīng))
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

run loop 與線程 的關(guān)系了:一個(gè)線程對(duì)應(yīng)一個(gè) run loop,程序運(yùn)行是主線程的 main run loop 默認(rèn)啟動(dòng)了贪庙,所以我們的程序才不會(huì)退出蜘犁,子線程的 run loop 按需啟動(dòng)(調(diào)用 run 方法)。run loop 是線程的事件管理者止邮,或者說是線程的事件管家这橙,它會(huì)按照順序管理線程要處理的事件,決定哪些事件在什么時(shí)候提交給線程處理导披。

看到這里我們大概也明白了 run loop 和線程大概是個(gè)怎么回事了屈扎,其實(shí)這里最想搞明白的是:run loop 是如何進(jìn)行狀態(tài)切換的,例如它是怎么進(jìn)入休眠怎樣被喚醒的撩匕?還有它和線程之間是怎么進(jìn)行信息傳遞的鹰晨?怎么讓線程保持活性的?等等止毕,搜集到的資料看到是 run loop 內(nèi)部是基于內(nèi)核基于 mach port 進(jìn)行工作的模蜡,涉及的太深?yuàn)W了,這里暫時(shí)先進(jìn)行上層的學(xué)習(xí)扁凛,等我們把基礎(chǔ)應(yīng)用以及一些源碼實(shí)現(xiàn)搞明白了再深入學(xué)習(xí)它的底層內(nèi)容忍疾。????

下面我們開始從代碼層面對(duì) run loop 進(jìn)行學(xué)習(xí),而學(xué)習(xí)的主線則是 run loop 是如何作用與線程使其保持活性的谨朝?

main run loop 啟動(dòng)

前面我們學(xué)習(xí)線程時(shí)卤妒,多次提到主線程主隊(duì)列都是在 app 啟動(dòng)時(shí)默認(rèn)創(chuàng)建的甥绿,而恰恰主線程的 main run loop 也是在 app 啟動(dòng)時(shí)默認(rèn)跟著創(chuàng)建并啟動(dòng)的,那么我們從 main.m 文件中找出一些端倪则披,使用 Xcode 創(chuàng)建一個(gè) OC 語言的 Single View App 時(shí)會(huì)自動(dòng)生成如下的 main.m 文件:

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        NSLog(@"?????♀??????♀?..."); // 這里插入一行打印語句
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
    // return 0;
    
    // 把上面的 return UIApplicationMain(argc, argv, nil, appDelegateClassName); 語句拆開如下:
    // int result = UIApplicationMain(argc, argv, nil, appDelegateClassName);
    // return result; // ?? 在此行打一個(gè)斷點(diǎn)共缕,執(zhí)行程序會(huì)發(fā)現(xiàn)此斷點(diǎn)是無效的,因?yàn)?main 函數(shù)根本不會(huì)執(zhí)行到這里
}

main函數(shù)最后一行return語句是返回 UIApplicationMain 函數(shù)的執(zhí)行結(jié)果收叶,我們把此行注釋骄呼,然后添加一行 return 0;,運(yùn)行程序后會(huì)看到執(zhí)行 NSLog 語句后程序就結(jié)束了直接回到了手機(jī)桌面判没,而最后一行是 return UIApplicationMain(argc, argv, nil, appDelegateClassName); 的話運(yùn)行程序后就進(jìn)入了 app 的首頁而并不會(huì)結(jié)束程序蜓萄,那么我們大概想到了這個(gè) UIApplicationMain函數(shù)是不會(huì)返回的,它不會(huì)返回澄峰,所以 main 函數(shù)也就不會(huì)返回了嫉沽,main 函數(shù)不會(huì)返回,所以我們的 app 就不會(huì)自己主動(dòng)結(jié)束運(yùn)行回到桌面了(當(dāng)然這里的函數(shù)不會(huì)返回是不同于我們線程學(xué)習(xí)時(shí)看到的線程被阻塞甚至死鎖時(shí)的函數(shù)不返回)俏竞。下面看一下 UIApplicationMain函數(shù)的聲明绸硕,看到是一個(gè)返回值是int類型的函數(shù)。

UIKIT_EXTERN int UIApplicationMain(int argc, char * _Nullable argv[_Nonnull], NSString * _Nullable principalClassName, NSString * _Nullable delegateClassName);

UIApplicationMain
?Creates the application object and the application delegate and sets up the event cycle.
?
?Return Value
?Even though an integer return type is specified, this function never returns. When users exits an iOS app by pressing the Home button, the application moves to the background.
?即使指定了整數(shù)返回類型魂毁,此函數(shù)也從不返回玻佩。當(dāng)用戶通過按 Home 鍵退出 iOS 應(yīng)用時(shí),該應(yīng)用將移至后臺(tái)席楚。
?Discussion
?... It also sets up the main event loop, including the application’s run loop, and begins processing events. ... Despite the declared return type, this function never returns.
?... 它還設(shè)置 main event loop咬崔,包括應(yīng)用程序的 run loop(main run loop),并開始處理事件烦秩。... 盡管聲明了返回類型垮斯,但此函數(shù)從不返回。

在開發(fā)者文檔中查看 UIApplicationMain 函數(shù)只祠,摘要告訴我們 UIApplicationMain 函數(shù)完成:創(chuàng)建應(yīng)用程序?qū)ο蠛蛻?yīng)用程序代理并設(shè)置 event cycle兜蠕,看到 Return Value 一項(xiàng) Apple 已經(jīng)明確告訴我們 UIApplicationMain 函數(shù)是不會(huì)返回的,并且在 Discussion 中也告訴我們 UIApplicationMain 函數(shù)啟動(dòng)了 main run loop 并開始著手為我們處理事件抛寝。

main 函數(shù)是我們應(yīng)用程序的啟動(dòng)入口熊杨,然后調(diào)用 UIApplicationMain 函數(shù)其內(nèi)部幫我們開啟了 main run loop,換個(gè)角度試圖理解為何我們的應(yīng)用程序不退出時(shí)盗舰,是不是可以理解為我們的應(yīng)用程序自啟動(dòng)開始就被包裹在 main run loop 的 do while 循環(huán) 中呢猴凹?

那么根據(jù)上面 UIApplicationMain 函數(shù)的功能以及我們對(duì) runloop 概念的理解,大概可以書寫出如下 runloop 的偽代碼:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        int retVal = 0;
        do {
            // 在睡眠中等待消息
            int message = sleep_and_wait();
            // 處理消息
            retVal = process_message(message);
        } while (retVal == 0);
        return 0;
    }
}

添加一個(gè)CFRunLoopGetMain 的符號(hào)斷點(diǎn)運(yùn)行程序岭皂,然后在控制臺(tái)使用 bt 命令打印線程的堆棧信息可看到在 UIApplicationMain 函數(shù)中啟動(dòng)了 main run loop。

(lldb) bt 
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 // ?? 'com.apple.main-thread' 當(dāng)前是我們的主線程
  * frame #0: 0x00000001de70a26c CoreFoundation`CFRunLoopGetMain // ?? CFRunLoopGetMain 獲取主線程
    frame #1: 0x000000020af6d864 UIKitCore`UIApplicationInitialize + 84 
    frame #2: 0x000000020af6ce30 UIKitCore`_UIApplicationMainPreparations + 416
    frame #3: 0x000000020af6cc04 UIKitCore`UIApplicationMain + 160 // ?? UIApplicationMain 函數(shù)
    frame #4: 0x00000001008ba1ac Simple_iOS`main(argc=1, argv=0x000000016f54b8e8) at main.m:20:12 // ?? main 函數(shù)
    frame #5: 0x00000001de1ce8e0 libdyld.dylib`start + 4 // ?? 加載 dyld 和動(dòng)態(tài)庫
(lldb) 

如何對(duì)子線程進(jìn)行闭油罚活--手動(dòng)啟動(dòng)線程的 run loop
首先對(duì) “一般來講爷绘,一個(gè)線程一次只能執(zhí)行一個(gè)任務(wù)书劝,執(zhí)行完成后線程就會(huì)退出⊥林粒” 這個(gè)結(jié)論進(jìn)行證明购对。這里我們使用 NSThread 作為線程對(duì)象,首先創(chuàng)建一個(gè)繼承自 NSThread 的 CommonThread 類陶因,然后重寫它的 dealloc 函數(shù)(之所以不直接在一個(gè) NSThread 的分類中重寫 dealloc 函數(shù)骡苞,是因?yàn)?app 內(nèi)部的 NSThread 對(duì)象的創(chuàng)建和銷毀會(huì)影響我們的觀察) 。

// CommonThread 定義

// CommonThread.h 
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface CommonThread : NSThread
@end
NS_ASSUME_NONNULL_END

// CommonThread.m
#import "CommonThread.h"
@implementation CommonThread
- (void)dealloc {
    NSLog(@"?????? %@ CommonThread %s", self, __func__);
}
@end

然后我們?cè)诟刂破鞯?viewDidLoad 函數(shù)中編寫如下測試代碼:

NSLog(@"?? START: %@", [NSThread currentThread]);
{
    CommonThread *commonThread = [[CommonThread alloc] initWithBlock:^{
        NSLog(@"???♀????♀? %@", [NSThread currentThread]);
    }];
    [commonThread start];
}
NSLog(@"?? END: %@", [NSThread currentThread]);

// 控制臺(tái)打印:
?? START: <NSThread: 0x282801a40>{number = 1, name = main}
?? END: <NSThread: 0x282801a40>{number = 1, name = main}
???♀????♀? <CommonThread: 0x2825b6e00>{number = 5, name = (null)} // 子線程
?????? <CommonThread: 0x2825b6e00>{number = 5, name = (null)} CommonThread -[CommonThread dealloc] // commonThread 線程對(duì)象被銷毀(線程退出)

根據(jù)控制臺(tái)打印我們可以看到在 commonThread 線程中的任務(wù)執(zhí)行完畢后楷扬,commonThread 線程就被釋放銷毀了(線程退出)解幽。那么下面我們?cè)噲D使用 run loop 讓 commonThread 不退出,同時(shí)為了便于觀察 run loop 的退出(NSRunLoop 對(duì)象的銷毀)烘苹,我們添加一個(gè) NSRunLoop 的分類并在分類中重寫 dealloc 函數(shù)(這里之所以直接用 NSRunLoop 類的分類是因?yàn)槎阒辏琣pp 除了 main run loop 外是不會(huì)自己主動(dòng)為線程開啟 run loop 的,所以這里我們不用擔(dān)心 app 內(nèi)部的 NSRunLoop 對(duì)象對(duì)我們的影響)镣衡。那么我們?cè)谏厦娴拇a基礎(chǔ)上為線程添加 run loop 的獲取和 run霜定。

NSLog(@"?? START: %@", [NSThread currentThread]);
{
    CommonThread *commonThread = [[CommonThread alloc] initWithBlock:^{
        NSLog(@"???♀????♀? %@", [NSThread currentThread]);
        
        // 為當(dāng)前線程獲取 run loop
        NSRunLoop *commonRunLoop = [NSRunLoop currentRunLoop];
        [commonRunLoop run]; // 不添加任何事件直接 run 

        NSLog(@"???? %p %@", commonRunLoop, commonRunLoop);
    }];
    [commonThread start];
}
NSLog(@"?? END: %@", [NSThread currentThread]);

// 控制臺(tái)打印:
?? START: <NSThread: 0x282efdac0>{number = 1, name = main}
?? END: <NSThread: 0x282efdac0>{number = 1, name = main}
???♀????♀? <CommonThread: 0x282ea3600>{number = 5, name = (null)} // 子線程
???? 0x281ffa940 <CFRunLoop 0x2807ff500 [0x20e729430]>{wakeup port = 0x9b03, stopped = false, ignoreWakeUps = true, 
current mode = (none),
common modes = <CFBasicHash 0x2835b32d0 [0x20e729430]>{type = mutable set, count = 1,
entries =>
    2 : <CFString 0x20e75fc78 [0x20e729430]>{contents = "kCFRunLoopDefaultMode"}
}
,
common mode items = (null),
modes = <CFBasicHash 0x2835b3360 [0x20e729430]>{type = mutable set, count = 1,
entries =>
    2 : <CFRunLoopMode 0x2800fca90 [0x20e729430]>{name = kCFRunLoopDefaultMode, port set = 0x9a03, queue = 0x2815f2880, source = 0x2815f3080 (not fired), timer port = 0x9803, 
    sources0 = (null), // ?? 空
    sources1 = (null), // ?? 空
    observers = (null), // ?? 空
    timers = (null), // ?? 空
    currently 629287011 (5987575088396) / soft deadline in: 7.68614087e+11 sec (@ -1) / hard deadline in: 7.68614087e+11 sec (@ -1)
},

}
}

?????? 0x2814eb360 NSRunLoop -[NSRunLoop(Common) dealloc] // commonRunLoop run loop 對(duì)象被銷毀(run loop 退出)
?????? <CommonThread: 0x2836ddc40>{number = 6, name = (null)} CommonThread -[CommonThread dealloc] // commonThread 線程對(duì)象被銷毀(線程退出)

運(yùn)行程序后,我們的 commonThread 線程還是退出了廊鸥,commonRunLoop 也退出了望浩。其實(shí)是這里涉及到一個(gè)知識(shí)點(diǎn),當(dāng) run loop 當(dāng)前運(yùn)行的 mode 中沒有任何需要處理的事件時(shí)惰说,run loop 會(huì)退出磨德。正如上面控制臺(tái)中的打印: sources0、sources1助被、observers剖张、timers 四者都是 (null),所以我們需要?jiǎng)?chuàng)建一個(gè)事件讓 run loop 來處理揩环,這樣 run loop 才不會(huì)退出搔弄。我們?cè)谏厦媸纠a中的 [commonRunLoop run]; 行上面添加如下兩行:

// 往 run loop 里面添加 Source\Timer\Observer
[commonRunLoop addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];

// 這里要添加如下完整的 NSTimer 對(duì)象,只是添加一個(gè) [[NSTimer alloc] init] 會(huì) crash
// NSTimer *time = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
//     NSLog(@"?? %@", timer);
// }];
// [[NSRunLoop currentRunLoop] addTimer:time forMode:NSDefaultRunLoopMode];

NSLog(@"?? %p %@", commonRunLoop, commonRunLoop);

// 控制臺(tái)部分打印:
...
sources1 = <CFBasicHash 0x60000251a4c0 [0x7fff8002e7f0]>{type = mutable set, count = 1,
entries =>
1 : <CFRunLoopSource 0x600001e700c0 [0x7fff8002e7f0]>{signalled = No, valid = Yes, order = 200, context = <CFMachPort 0x600001c7c370 [0x7fff8002e7f0]>{valid = Yes, por 
// ?? CFMachPort 0x600001c7c370 即為我們添加的 NSPort
...

運(yùn)行程序發(fā)現(xiàn)我們的 NSPort 實(shí)例被添加到 commonRunLoopsources1 中丰滑,并且 commonThreadcommonRunLoop 都沒有打印 dealloc顾犹,表示我們的線程和 run loop 都沒有退出,此時(shí) commonThread 線程對(duì)應(yīng)的 run loop 就被啟動(dòng)了褒墨,同時(shí)觀察控制臺(tái)的話看到 [commonRunLoop run]; 行下面的 NSLog(@"???? %p %@", commonRunLoop, commonRunLoop); 行沒有得到執(zhí)行炫刷,即使我們?cè)诖诵写蛞粋€(gè)斷點(diǎn),發(fā)現(xiàn)代碼也不會(huì)執(zhí)行到這里郁妈,這和我們上面 main 函數(shù)中由于 UIApplicationMain 函數(shù)開啟了 main run loop 使 UIApplicationMain 函數(shù)本身不再返回浑玛,所以最后的 return 0; 行得不到執(zhí)行的結(jié)果是一致的,這里則是由于 [commonRunLoop run];開啟了當(dāng)前線程的 run loop 使run 函數(shù)本身不再返回噩咪,自此 commonThread 線程不再退出并保持活性顾彰。[commonRunLoop run];行可以被看作一個(gè)界限极阅,它下面的代碼在 commonRunLoop 啟動(dòng)期間不會(huì)再執(zhí)行了,只有當(dāng)commonRunLoop退出時(shí)才會(huì)執(zhí)行涨享。

下面我們首先通過開發(fā)文檔對(duì) NSRunLoop 的 run函數(shù)進(jìn)行學(xué)習(xí)筋搏,然后再對(duì) commonThread 線程的活性進(jìn)行驗(yàn)證,然后再使 commonRunLoop 失去活性讓線程和 run loop 在我們的控制下退出厕隧。

run
runNSRunLoop 類的一個(gè)實(shí)例方法奔脐,它的主要功能是:Puts the receiver(NSRunLoop 對(duì)象) into a permanent loop, during which time it processes data from all attached input sources.

@interface NSRunLoop (NSRunLoopConveniences)

- (void)run;

// ?? 下面還有兩個(gè)指定 mode 和 limitDate 的 run 函數(shù)
- (void)runUntilDate:(NSDate *)limitDate;
- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
...
@end

如果沒有 input sources 或 timers(NSTimer)附加到 run loop,此方法將立即退出吁讨。否則髓迎,它將通過重復(fù)調(diào)用 runMode:beforeDate:NSDefaultRunLoopMode 模式下運(yùn)行 receiver(NSRunLoop 對(duì)象)。換句話說挡爵,此方法有效地開始了一個(gè)無限 loop竖般,該 loop 處理來自 run loop 的 input sources 和 timers 的數(shù)據(jù)。

從 run loop 中手動(dòng)刪除所有已知的 input sources 和 timers 并不能保證 run loop 將退出茶鹃。macOS 可以根據(jù)需要安裝和刪除附加的 input sources涣雕,以處理針對(duì) receiver’s thread 的請(qǐng)求。因此闭翩,這些 sources 可以阻止 run loop 退出挣郭。

如果希望 run loop 終止,則不應(yīng)使用此方法疗韵。相反兑障,請(qǐng)使用其他 run 方法之一,并在循環(huán)中檢查自己的其他任意條件蕉汪。一個(gè)簡單的例子是:

BOOL shouldKeepRunning = YES; // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);

在程序中的其它位置應(yīng)將 shouldKeepRunning 設(shè)置為 NO流译。

看到 run 函數(shù)的注釋已經(jīng)明確告訴我們,run 內(nèi)部無限重復(fù)調(diào)用 runMode:beforeDate: 函數(shù)者疤,在默認(rèn)模式下運(yùn)行 run loop福澡,即開啟了一個(gè)無限 loop,如果我們打算讓 run loop 永久運(yùn)行且對(duì)應(yīng)的線程也永不退出的話我們可以使用 run 函數(shù)來啟動(dòng) run loop 對(duì)象驹马,如果想要根據(jù)開發(fā)場景需要來任意的啟動(dòng)或停止 run loop 的話革砸,則需要使用 run 函數(shù)下面兩個(gè)有 limitDate 參數(shù)的 run 函數(shù)并結(jié)合一個(gè) while 循環(huán)使用,如上面 Apple 給的示例代碼一樣糯累,等下面我們會(huì)對(duì)此情景進(jìn)行詳細(xì)的講解算利。

run 函數(shù)的偽代碼大概如下,CFRunLoopStop 函數(shù)(它是 run loop 的停止函數(shù)泳姐,下面會(huì)細(xì)講效拭,這里主要幫助我們理解 run 函數(shù)的內(nèi)部邏輯)對(duì)調(diào)用 run 函數(shù)啟動(dòng)的 run loop 無效,使用 CFRunLoopStop函數(shù)停止的可能只是某一次循環(huán)中的 runMode:beforeDate:,下次循環(huán)進(jìn)來時(shí) run loop 對(duì)象又一次調(diào)用了 runMode:beforeDate: 函數(shù)允耿。

NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while(1) {
    Bool resul = [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}

[NSDate distantFuture]
[NSDate distantFuture] 是一個(gè) NSDate 對(duì)象借笙,表示遙遠(yuǎn)的將來的一個(gè)日期(以世紀(jì)為單位)。當(dāng)需要 NSDate 對(duì)象以實(shí)質(zhì)上忽略 date 參數(shù)時(shí)较锡,可以傳遞此值〉裂鳎可以使用 distantFuture 返回的對(duì)象作為 date 參數(shù)來無限期地等待事件發(fā)生蚂蕴。

@property (class, readonly, copy) NSDate *distantFuture;

當(dāng)前打印 [NSDate distantFuture] 是: 4001-01-01 08:00:00,當(dāng)前實(shí)際時(shí)間是 2020 12 12俯邓。

等下我們?cè)龠M(jìn)行手動(dòng)退出 run loop 的功能點(diǎn)骡楼,暫時(shí)先驗(yàn)證下在已經(jīng)啟動(dòng) run loop 的線程中我們是否可以動(dòng)態(tài)的給該線程添加任務(wù)。

在已啟動(dòng) run loop 的線程中添加任務(wù)

我們需要對(duì)上面的測試代碼進(jìn)行修改稽鞭。首先我們把上面的 commonThread 局部變量修改為 ViewController 的一個(gè)屬性鸟整。

@property (nonatomic, strong) CommonThread *commonThread;

然后把之前 commonThread 局部變量的創(chuàng)建賦值給 self.commonThread,然后添加如下一個(gè)自定義函數(shù) rocket:ViewControllertouchesBegan:withEvent: 方法朦蕴。

- (void)rocket:(NSObject *)param {
    sleep(1);
    NSLog(@"???? %@ param: %p", [NSThread currentThread], param);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"???? START...");
    
    // 末尾的 wait 參數(shù)表示 performSelector:onThread:withObject: 函數(shù)是否等 @selector(rocket:) 執(zhí)行完成后才返回篮条,還是直接返回,
    // 類似 dispatch_async 和 dispatch_sync吩抓,表示在 self.commonThread 線程中是異步執(zhí)行 @selector(rocket:) 還是同步執(zhí)行 @selector(rocket:)涉茧。
    [self performSelector:@selector(rocket:) onThread:self.commonThread withObject:nil waitUntilDone:YES];
    
    NSLog(@"???? END...");
}

上面代碼編輯好后,觸摸 ViewController 的空白區(qū)域疹娶,看到 rocket 函數(shù)正常執(zhí)行伴栓。

???? START...
???? <CommonThread: 0x281f8ce80>{number = 5, name = (null)} param: 0x0
???? END...

(這里發(fā)現(xiàn)一個(gè)點(diǎn),連續(xù)點(diǎn)擊屏幕雨饺,點(diǎn)擊幾次 rocket函數(shù)就能執(zhí)行幾次钳垮,即使在 performSelector:onThread:withObject:waitUntilDone:函數(shù)的最后參數(shù)傳遞YES時(shí),touchesBegan:withEvent:函數(shù)本次沒有執(zhí)行完成的時(shí)候额港,我們就點(diǎn)擊屏幕饺窿,系統(tǒng)依然會(huì)記錄我們點(diǎn)擊過屏幕的次數(shù),然后rocket 函數(shù)就會(huì)執(zhí)行對(duì)應(yīng)的次數(shù)锹安。把 thread 參數(shù)使用主線程 [NSThread mainThread]短荐,依然會(huì)執(zhí)行對(duì)應(yīng)的點(diǎn)擊次數(shù),不過子線程和主線程還是有些許區(qū)別的叹哭,感興趣的話可以自行測試一下忍宋。(其實(shí)是我真的不知道怎么描述這個(gè)區(qū)別))

然后我們?cè)龠M(jìn)行一個(gè)測試,把 self.commonThread 線程任務(wù)中的 run loop 代碼注釋的話风罩,則觸摸屏幕是不會(huì)執(zhí)行 rocket 函數(shù)的糠排,如果把 performSelector:onThread:withObject:waitUntilDone: 函數(shù)最后一個(gè)參數(shù)傳YES的話,則會(huì)直接 crash超升,之前 commonThread線程是一個(gè)局部變量的時(shí)候我們能看到它會(huì)退出并且被銷毀了入宦,此時(shí)雖然我們修改為了 ViewController的一個(gè)屬性被強(qiáng)引用哺徊,但是當(dāng)不主動(dòng)啟動(dòng) self.commonThread線程的 run loop 的話,它依然是沒有活性的乾闰。

停止已啟動(dòng) run loop 線程的 run loop

下面學(xué)習(xí)如何停止 run loop落追,首先我們?cè)?ViewController 上添加一個(gè)停止按鈕并添加點(diǎn)擊事件,添加如下代碼:

// 停止按鈕的點(diǎn)擊事件
- (IBAction)stopAction:(UIButton *)sender {
    NSLog(@"?? stop loop START(ACTION)...");
    [self performSelector:@selector(stopRunLoop:) onThread:self.commonThread withObject:nil waitUntilDone:NO];
    NSLog(@"?? stop loop END(ACTION)...");
}

// 停止 run loop
- (void)stopRunLoop:(NSObject *)param {
    NSLog(@"?? stop loop START...");
    CFRunLoopStop(CFRunLoopGetCurrent());
    NSLog(@"?? stop loop END...");
}

點(diǎn)擊停止按鈕后涯肩,可看到兩個(gè)函數(shù)都正常的執(zhí)行了轿钠。但是我們點(diǎn)擊屏幕的空白區(qū)域,發(fā)現(xiàn)rocket函數(shù)依然能正常調(diào)用病苗。

 ?? stop loop START(ACTION)...
 ?? stop loop END(ACTION)...
 ?? stop loop START...
 ?? stop loop END...
 ???? START...
 ???? <CommonThread: 0x2807c2a80>{number = 5, name = (null)} param: 0x0
 ???? END...

那么我們把 [commonRunLoop run]; 修改為 [commonRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];疗垛,然后運(yùn)行程序后,我們直接點(diǎn)擊停止按鈕硫朦,看到控制臺(tái)有如下打印:

 ?? stop loop START(ACTION)...
 ?? stop loop END(ACTION)...
 ?? stop loop START...
 ?? stop loop END...
 ???? 0x2819d6700 <CFRunLoop 0x2801d7000 [0x20e729430]>{wakeup port = 0x9b03, stopped = false, ignoreWakeUps = true, 
current mode = (none),
...
 ?????? 0x2819d6700 NSRunLoop -[NSRunLoop(Common) dealloc]

此邏輯大概是 commonRunLoop 執(zhí)行完 CFRunLoopStop 函數(shù)后律秃,[commonRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];函數(shù)返回了亲配,然后下面的 NSLog(@"???? %p %@", commonRunLoop, commonRunLoop); 得到了執(zhí)行规婆,然后 self.commonThread 創(chuàng)建時(shí)添加的 block 函數(shù)就完整執(zhí)行完畢了隘谣,完整執(zhí)行完此邏輯后,commonRunLoop 便退出并且銷毀了挚赊。

下面我們?cè)倏匆环N情況诡壁。再次運(yùn)行程序,我們不點(diǎn)擊停止按鈕荠割,直接點(diǎn)擊屏幕空白區(qū)域妹卿,看到控制臺(tái)有如下打印:

 ???? START...
 ???? END...
  ???? <CommonThread: 0x280fddb00>{number = 5, name = (null)} param: 0x0
 ???? 0x283e86b80 <CFRunLoop 0x282687900 [0x20e729430]>{wakeup port = 0x9b03, stopped = false, ignoreWakeUps = true, 
current mode = (none),
...
 ?????? 0x283e86b80 NSRunLoop -[NSRunLoop(Common) dealloc]

本次我們沒有執(zhí)行 CFRunLoopStop 函數(shù),僅在 self.commonThread 線程執(zhí)行了一個(gè)事件蔑鹦,執(zhí)行完 rocket函數(shù)以后夺克,[commonRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; 函數(shù)返回了,然后下面的 NSLog(@"???? %p %@", commonRunLoop, commonRunLoop); 得到了執(zhí)行嚎朽,然后 self.commonThread 創(chuàng)建時(shí)添加的 block 函數(shù)就完整執(zhí)行完畢了铺纽,完整執(zhí)行完此邏輯后,commonRunLoop便退出并且銷毀了哟忍。

那么我們根據(jù) run 函數(shù)中的注釋來把代碼修改為 Apple 示例代碼的樣子狡门。首先添加一個(gè)布爾類型的 shouldKeepRunning屬性,并初始為 YES锅很,然后把 [commonRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; 修改為 while (self.shouldKeepRunning && [commonRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);其馏,然后進(jìn)行各項(xiàng)測試,可發(fā)現(xiàn)打印結(jié)果和 [commonRunLoop run]; 使用時(shí)完全一致爆安。

下面我們優(yōu)化一下代碼叛复,添加 __weak 修飾的 self 防止循環(huán)引用:

NSLog(@"?? START: %@", [NSThread currentThread]);
{
    __weak typeof(self) _self = self;
    self.commonThread = [[CommonThread alloc] initWithBlock:^{
        NSLog(@"???♀????♀? %@", [NSThread currentThread]);
        
        NSRunLoop *commonRunLoop = [NSRunLoop currentRunLoop];
        
        // 往 run loop 里面添加 Source\Timer\Observer
        [commonRunLoop addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        
        // NSTimer *time = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        //     NSLog(@"?? %@", timer);
        // }];
        // [[NSRunLoop currentRunLoop] addTimer:time forMode:NSDefaultRunLoopMode];
        
        NSLog(@"?? %p %@", commonRunLoop, commonRunLoop);
        __strong typeof(_self) self = _self;
        while (self && self.shouldKeepRunning && [commonRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
        NSLog(@"???? %p %@", commonRunLoop, commonRunLoop);
    }];
    
    [self.commonThread start];
}

NSLog(@"?? END: %@", [NSThread currentThread]);

然后是 stopRunLoop 函數(shù),當(dāng)點(diǎn)擊停止按鈕時(shí)修改 self.shouldKeepRunningNO,保證 CFRunLoopStop 函數(shù)執(zhí)行后 commonRunLoop 停止褐奥,然后不再進(jìn)入 while 循環(huán)咖耘。

- (void)stopRunLoop:(NSObject *)param {
    NSLog(@"?? stop loop START...");
    self.shouldKeepRunning = NO;
    CFRunLoopStop(CFRunLoopGetCurrent());
    NSLog(@"?? stop loop END...");
}

然后我們?cè)贉y試一下,運(yùn)行程序撬码,首先點(diǎn)擊屏幕空白區(qū)域儿倒,rocket 函數(shù)正常執(zhí)行,然后點(diǎn)擊停止按鈕耍群,看到 self.commonThread 線程的 run loop 退出并且 run loop 對(duì)象銷毀义桂,然后由當(dāng)前 ViewControler 返回上一個(gè)控制器(跳轉(zhuǎn)邏輯可以自行添加),看到當(dāng)前控制器和 self.commonThread 線程都正常銷毀蹈垢。如下是控制臺(tái)打印:

 ?? START: <NSThread: 0x283cfda80>{number = 1, name = main}
 ?? END: <NSThread: 0x283cfda80>{number = 1, name = main}
 ???♀????♀? <CommonThread: 0x283c84180>{number = 6, name = (null)} // ?? self.commonThread 子線程開啟
 ?? 0x280dfdce0 <CFRunLoop 0x2815f9500 [0x20e729430]>{wakeup port = 0x1507, stopped = false, ignoreWakeUps = true, 
current mode = (none), // ?? 正常獲取到 self.commonThread 線程的 run loop 并且給它添加一個(gè)事件,防止無事件時(shí) run loop 退出
...

 ???? START...
 ???? END...
 ???? <CommonThread: 0x283c84180>{number = 6, name = (null)} param: 0x0 // ?? 觸摸屏幕向已保持活性的 self.commonThread 線程添加任務(wù)能正常執(zhí)行 
 ?? stop loop START(ACTION)... // ?? 點(diǎn)擊停止按鈕袖裕,停止 self.commonThread 線程的 run loop
 ?? stop loop END(ACTION)...
 ?? stop loop START...
 ?? stop loop END...
 ???? 0x280dfdce0 <CFRunLoop 0x2815f9500 [0x20e729430]>{wakeup port = 0x1507, stopped = false, ignoreWakeUps = true, 
current mode = (none), // self.commonThread 線程的 run loop 已停止曹抬,self.commonThread 線程創(chuàng)建時(shí)添加的 block 函數(shù)繼續(xù)往下執(zhí)行
...

 ?????? 0x280dfdce0 NSRunLoop -[NSRunLoop(Common) dealloc] // self.commonThread 線程的 run loop 已經(jīng)退出,run loop 對(duì)象正常銷毀
 ?????? <ViewController: 0x10151b630> ViewController -[ViewController dealloc] // pop 后當(dāng)前控制器正常銷毀
 ?????? <CommonThread: 0x283c84180>{number = 6, name = main} CommonThread -[CommonThread dealloc] // self.commonThread 線程對(duì)象也正常銷毀

以上是在我們?cè)谕耆謩?dòng)可控的情況下:開啟線程的 run loop急鳄、動(dòng)態(tài)的向已開啟 run loop 的線程中添加任務(wù)谤民、手動(dòng)停止已開啟 run loop 的線程,看到這里我們大概對(duì) run loop 保持線程的活性有一個(gè)整體的認(rèn)識(shí)了疾宏。根據(jù) Apple 提供的 NSThread 和 NSRunLoop 類以面向?qū)ο蟮乃枷雽W(xué)習(xí)線程和 run loop 確實(shí)更好的幫助我們理解一些概念性的東西张足。

下面我們根據(jù)一些重要的知識(shí)點(diǎn)對(duì)上面的全部代碼進(jìn)行整體優(yōu)化。

performSelector:onThread:withObject:waitUntilDone: 函數(shù)的最后一個(gè)參數(shù)waitYES 時(shí)必須保證
thread 線程參數(shù)存在并且該線程已開啟 run loop坎藐,否則會(huì)直接 crash为牍,這是因?yàn)榫€程不滿足以上條件時(shí)無法執(zhí)行 selector 參數(shù)傳遞的事件,wait傳遞 YES又非要等 selector 執(zhí)行完成岩馍,這固然是完全是不可能的碉咆。所以,我們?cè)谒械?performSelector:onThread:withObject:waitUntilDone:函數(shù)執(zhí)行前可以加一行 if (!self.commonThread) return;蛀恩,這里當(dāng)然在 self.commonThread 線程創(chuàng)建完成后疫铜,若 ViewControllerdealloc 函數(shù)執(zhí)行完成并徹底銷毀并釋放self.commonThread的引用之前,self.commonThread 是不會(huì)為 nil 的双谆,這里我們?cè)?self.commonThreadrun loop 執(zhí)行 CFRunLoopStop 后手動(dòng)把 self.commonThread 置為 nil壳咕,畢竟失去活性的線程和已經(jīng)為 nil沒什么兩樣。

因?yàn)槲覀冊(cè)趧?chuàng)建 self.commonThread 時(shí)就已經(jīng)開啟了該線程的 run loop顽馋,所以可以保證在向 self.commonThread線程添加事件時(shí)它已經(jīng)保持了活性谓厘。

還有一個(gè)極隱秘的點(diǎn)。當(dāng)我們使用 block 時(shí)會(huì)在 block 外面使用 __weak 修飾符取得一個(gè)的 self 的弱引用變量趣避,然后在 block 內(nèi)部又會(huì)使用 __strong修飾符取得一個(gè)的 self 弱引用變量的強(qiáng)引用庞呕,首先這里是在 block 內(nèi)部,當(dāng) block 執(zhí)行完畢后會(huì)進(jìn)行自動(dòng)釋放強(qiáng)引用的 self,這里的目的只是為了保證在 block 執(zhí)行期間 self 不會(huì)被釋放住练,這就默認(rèn)延長了 self 的生命周期到 block 執(zhí)行結(jié)束地啰,這在我們的日常開發(fā)中沒有任何問題,但是讲逛,但是亏吝,但是,放在 run loop 這里是不行的盏混,當(dāng)我們直接 push 進(jìn)入 ViewController 然后直接 pop 會(huì)上一個(gè)頁面時(shí)蔚鸥,我們要借用 ViewController 的 dealloc 函數(shù)來stop self.commonThread線程的 run loop 的,如果我們還用 __strong 修飾符取得 self 強(qiáng)引用的話许赃,那么由于 self.commonThread 線程創(chuàng)建時(shí)的 block 內(nèi)部的 run loop 的 runMode:beforeDate: 啟動(dòng)函數(shù)是沒有返回的止喷,它會(huì)一直潛在的延長 self 的生命周期,會(huì)直接導(dǎo)致 ViewController 無法釋放混聊,dealloc 函數(shù)得不到調(diào)用(描述的不夠清晰弹谁,看下面的實(shí)例代碼應(yīng)該會(huì)一眼看明白的)。

這里是 __weak__strong 配對(duì)使用的一些解釋句喜,如果對(duì) block 不清晰的話可以參考前面的文章進(jìn)行學(xué)習(xí)预愤。

// 下面在并行隊(duì)列里面要執(zhí)行的 block 沒有 retain self
__weak typeof(self) _self = self;
dispatch_async(globalQueue_DEFAULT, ^{
    // 保證在下面的執(zhí)行過程中 self 不會(huì)被釋放,執(zhí)行結(jié)束后 self 會(huì)執(zhí)行一次 release咳胃。
    
    // 在 ARC 下植康,這里看似前面的 __wek 和這里的 __strong 相互抵消了,
    // 這里 __strong 的 self展懈,在出了下面的右邊花括號(hào)時(shí)销睁,會(huì)執(zhí)行一次 release 操作。 
    // 且只有此 block 執(zhí)行的時(shí)候 _self 有值那么此處的 __strong self 才會(huì)有值标沪,
    // 否則下面的 if 判斷就直接 return 了榄攀。
    
    __strong typeof(_self) self = _self;
    if (!self) return;
    
    // do something
    // ...
    
    dispatch_async(dispatch_get_main_queue(), ^{
        // 此時(shí)如果能進(jìn)來,表示此時(shí) self 是存在的
        self.view.backgroundColor = [UIColor redColor];
    });
});

下面是對(duì)應(yīng)上面的解釋結(jié)果的所有代碼金句。

#import "ViewController.h"
#import "CommonThread.h"

@interface ViewController ()

@property (nonatomic, strong) CommonThread *commonThread;
@property (nonatomic, assign) BOOL shouldKeepRunning;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.shouldKeepRunning = YES;
    
    NSLog(@"?? START: %@", [NSThread currentThread]);
    
    // ?? 下面的 block 內(nèi)部要使用 self 的弱引用檩赢,否則會(huì)導(dǎo)致 vc 無法銷毀
    __weak typeof(self) _self = self;
    
    self.commonThread = [[CommonThread alloc] initWithBlock:^{
        NSLog(@"???♀????♀? %@", [NSThread currentThread]);
        NSRunLoop *commonRunLoop = [NSRunLoop currentRunLoop];
        
        // 往 run loop 里面添加 Source/Timer/Observer
        [commonRunLoop addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        NSLog(@"?? %p %@", commonRunLoop, commonRunLoop);
        
        // ?? 上面的最后一段描述即針對(duì)這里,這里不能再使用 __strong 強(qiáng)引用外部的 _self违寞,會(huì)直接導(dǎo)致 vc 無法銷毀
        // __strong typeof(_self) self = _self;
        
        while (_self && _self.shouldKeepRunning) {
            [commonRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
        NSLog(@"???? %p %@", commonRunLoop, commonRunLoop);
    }];
    [self.commonThread start];
    NSLog(@"?? END: %@", [NSThread currentThread]);
}

- (void)rocket:(NSObject *)param {
    // sleep(3);
    NSLog(@"???? %@ param: %p", [NSThread currentThread], param);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"???? START...");
    if (!self.commonThread) return; // 首先判斷 self.commonThread 不為 nil贞瞒,下面的 wait 參數(shù)是 YES
    [self performSelector:@selector(rocket:) onThread:self.commonThread withObject:nil waitUntilDone:YES];
    NSLog(@"???? END...");
}

- (IBAction)stopAction:(UIButton *)sender {
    NSLog(@"?? stop loop START(ACTION)...");
    if (!self.commonThread) return; // 首先判斷 self.commonThread 不為 nil,下面的 wait 參數(shù)是 YES
    [self performSelector:@selector(stopRunLoop:) onThread:self.commonThread withObject:nil waitUntilDone:YES];
    NSLog(@"?? stop loop END(ACTION)...");
}

- (void)stopRunLoop:(NSObject *)param {
    NSLog(@"?? stop loop START...");
    self.shouldKeepRunning = NO;
    CFRunLoopStop(CFRunLoopGetCurrent()); // 停止當(dāng)前線程的 run loop
    self.commonThread = nil; // run loop 停止后在這里把 self.commonThread 置為 nil
    NSLog(@"?? stop loop END...");
}

- (void)dealloc {
    [self stopAction:nil]; // 這里隨著 vc 的銷毀停止 self.commonThread 的 run loop
    NSLog(@"?????? %@ ViewController %s", self, __func__);
}

@end

看到這里我們應(yīng)該對(duì) run loop 和線程的關(guān)系有個(gè)大概的認(rèn)知了趁曼,當(dāng)然 run loop 的作用絕不僅僅是線程本活,還有許多其他方面的應(yīng)用

文章來源于網(wǎng)絡(luò)
原文地址:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末挡闰,一起剝皮案震驚了整個(gè)濱河市乒融,隨后出現(xiàn)的幾起案子掰盘,更是在濱河造成了極大的恐慌,老刑警劉巖赞季,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件愧捕,死亡現(xiàn)場離奇詭異,居然都是意外死亡申钩,警方通過查閱死者的電腦和手機(jī)次绘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來撒遣,“玉大人邮偎,你說我怎么就攤上這事∫謇瑁” “怎么了禾进?”我有些...
    開封第一講書人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長廉涕。 經(jīng)常有香客問我命迈,道長,這世上最難降的妖魔是什么火的? 我笑而不...
    開封第一講書人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮淑倾,結(jié)果婚禮上馏鹤,老公的妹妹穿的比我還像新娘。我一直安慰自己娇哆,他們只是感情好湃累,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著碍讨,像睡著了一般治力。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上勃黍,一...
    開封第一講書人閱讀 49,046評(píng)論 1 285
  • 那天宵统,我揣著相機(jī)與錄音,去河邊找鬼覆获。 笑死马澈,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的弄息。 我是一名探鬼主播痊班,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼摹量!你這毒婦竟也來了涤伐?” 一聲冷哼從身側(cè)響起馒胆,我...
    開封第一講書人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎凝果,沒想到半個(gè)月后祝迂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡豆村,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年液兽,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片掌动。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡四啰,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出粗恢,到底是詐尸還是另有隱情柑晒,我是刑警寧澤,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布眷射,位于F島的核電站匙赞,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏妖碉。R本人自食惡果不足惜涌庭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望欧宜。 院中可真熱鬧坐榆,春花似錦、人聲如沸冗茸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽夏漱。三九已至豪诲,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間挂绰,已是汗流浹背屎篱。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留扮授,地道東北人芳室。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像刹勃,于是被迫代替她去往敵國和親堪侯。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345

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

  • 一個(gè)應(yīng)用開始運(yùn)行以后放在那里荔仁,如果不對(duì)它進(jìn)行任何操作伍宦,這個(gè)應(yīng)用就像靜止了一樣芽死,不會(huì)自發(fā)的有任何動(dòng)作發(fā)生,但是如果我...
    我是啊梁閱讀 523評(píng)論 0 0
  • 這篇文章圍繞Core Foundation框架中關(guān)于run loop的源碼做一個(gè)深入理解次洼。Core Fundati...
    nemie閱讀 331評(píng)論 0 3
  • 從事iOS編程1年关贵,一直沒搞懂RunLoop原理,不知道大家有沒有想過這個(gè)問題卖毁,一個(gè)應(yīng)用開始運(yùn)行以后放在那...
    DeerRun閱讀 867評(píng)論 0 8
  • 寫在前面 由于文章比較長揖曾,簡書沒有目錄,讀起來不方便亥啦。建議看有目錄版RunLoop從源碼到應(yīng)用全面解析——帶目錄版...
    紙簡書生閱讀 3,606評(píng)論 1 16
  • 久違的晴天炭剪,家長會(huì)。 家長大會(huì)開好到教室時(shí)翔脱,離放學(xué)已經(jīng)沒多少時(shí)間了奴拦。班主任說已經(jīng)安排了三個(gè)家長分享經(jīng)驗(yàn)。 放學(xué)鈴聲...
    飄雪兒5閱讀 7,494評(píng)論 16 22