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 是如何作用與線程使其保持活性的谨朝?
前面我們學(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ì) “一般來講爷绘,一個(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í)例被添加到 commonRunLoop
的 sources1
中丰滑,并且 commonThread
和 commonRunLoop
都沒有打印 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
是 NSRunLoop
類的一個(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]
是一個(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ù)。
我們需要對(duì)上面的測試代碼進(jìn)行修改稽鞭。首先我們把上面的 commonThread
局部變量修改為 ViewController
的一個(gè)屬性鸟整。
@property (nonatomic, strong) CommonThread *commonThread;
然后把之前 commonThread
局部變量的創(chuàng)建賦值給 self.commonThread
,然后添加如下一個(gè)自定義函數(shù) rocket:
和 ViewController
的 touchesBegan: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 的話,它依然是沒有活性的乾闰。
下面學(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.shouldKeepRunning
為 NO
,保證 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ù)wait
傳YES
時(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)建完成后疫铜,若 ViewController
不 dealloc
函數(shù)執(zhí)行完成并徹底銷毀并釋放self.commonThread
的引用之前,self.commonThread
是不會(huì)為 nil
的双谆,這里我們?cè)?self.commonThread
的 run 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)用
原文地址: