實例化講解RunLoop
親,我的簡書已不再維護和更新了羊异,所有文章都遷移到了我的個人博客:https://mikefighting.github.io/怎顾,歡迎交流。
之前看過很多有關(guān)RunLoop的文章灾梦,其中要么是主要介紹RunLoop的基本概念峡钓,要么是主要講解RunLoop的底層原理,很少用真正的實例來講解RunLoop的若河,這其中有大部分原因是由于大家在項目中很少能用到RunLoop吧能岩。基于這種原因萧福,本文中將用很少的篇幅來對基礎(chǔ)內(nèi)容做以介紹拉鹃,然后主要利用實例來加深大家對RunLoop的理解,本文中的代碼已經(jīng)上傳GitHub,大家可以下載查看,有問題歡迎Issue我鲫忍。本文主要分為如下幾個部分:
- RunLoop的基礎(chǔ)知識
- 初識RunLoop膏燕,如何讓RunLoop進駐線程
- 深入理解Perform Selector
- 一直"活著"的后臺線程
- 深入理解NSTimer
- 讓兩個后臺線程有依賴性的一種方式
- NSURLConnetction的內(nèi)部實現(xiàn)
- AFNetWorking中是如何使用RunLoop的?
- 其它:利用GCD實現(xiàn)定時器功能
- 延伸閱讀
一、RunLoop的基本概念:
什么是RunLoop
悟民?提到RunLoop坝辫,我們一般都會提到線程,這是為什么呢射亏?先來看下官方對RunLoop
的定義:RunLoop
系統(tǒng)中和線程相關(guān)的基礎(chǔ)架構(gòu)的組成部分(和線程相關(guān))近忙,一個RunLoop是一個事件處理環(huán),系統(tǒng)利用這個事件處理環(huán)來安排事務(wù)智润,協(xié)調(diào)輸入的各種事件及舍。RunLoop
的目的是讓你的線程在有工作的時候忙碌,沒有工作的時候休眠(和線程相關(guān))窟绷【饴辏可能這樣說你還不是特別清楚RunLoop
究竟是用來做什么的,打個比方來說明:我們把線程比作一輛跑車钾麸,把這輛跑車的主人比作RunLoop
更振,那么在沒有'主人'的時候炕桨,這個跑車的生命是直線型的,其啟動肯腕,運行完之后就會廢棄(沒有人對其進行控制献宫,'撞壞'被收回),當(dāng)有了RunLoop
這個主人之后实撒,‘線程’這輛跑車的生命就有了保障姊途,這個時候,跑車的生命是環(huán)形的知态,并且在主人有比賽任務(wù)的時候就會被RunLoop
這個主人所喚醒,在沒有任務(wù)的時候可以休眠(在IOS中凰浮,開啟線程是很消耗性能的励烦,開啟主線程要消耗1M內(nèi)存夭禽,開啟一個后臺線程需要消耗512k內(nèi)存城菊,我們應(yīng)當(dāng)在線程沒有任務(wù)的時候休眠,來釋放所占用的資源其做,以便CPU進行更加高效的工作)顶考,這樣可以增加跑車的效率,也就是說RunLoop
是為線程所服務(wù)的。這個例子有點不是很貼切妖泄,線程和RunLoop之間是以鍵值對的形式一一對應(yīng)的驹沿,其中key是thread,value是runLoop(這點可以從蘋果公開的源碼中看出來)蹈胡,其實RunLoop是管理線程的一種機制渊季,這種機制不僅在IOS上有,在Node.js中的EventLoop罚渐,Android中的Looper却汉,都有類似的模式。剛才所說的比賽任務(wù)就是喚醒跑車這個線程的一個source
;RunLoop Mode
就是搅轿,一系列輸入的source
,timer
以及observer
病涨,RunLoop Mode
包含以下幾種: NSDefaultRunLoopMode
,NSEventTrackingRunLoopMode
,UIInitializationRunLoopMode
,NSRunLoopCommonModes
,NSConnectionReplyMode
,NSModalPanelRunLoopMode
,至于這些mode各自的含義,讀者可自己查詢璧坟,網(wǎng)上不乏這類資源;
二、初識RunLoop赎懦,如何讓RunLoop進駐線程
我們在主線程中添加如下代碼:
while (1) {
NSLog(@"while begin");
// the thread be blocked here
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
// this will not be executed
NSLog(@"while end");
}
這個時候我們可以看到主線程在執(zhí)行完[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
之后被阻塞而沒有執(zhí)行下面的NSLog(@"while end")
;同時雀鹃,我們利用GCD,將這段代碼放到一個后臺線程中:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
while (1) {
NSLog(@"while begin");
NSRunLoop *subRunLoop = [NSRunLoop currentRunLoop];
[subRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
NSLog(@"while end");
}
});
這個時候我們發(fā)現(xiàn)這個while循環(huán)會一直在執(zhí)行励两;這是為什么呢?我們先將這兩個RunLoop
分別打印出來:
由于這個日志比較長黎茎,我就只截取了上面的一部分。
我們再看我們新建的子線程中的
RunLoop
,打印出來之后:從中可以看出來:我們新建的線程中:
sources0 = (null),
sources1 = (null),
observers = (null),
timers = (null),
我們看到雖然有Mode当悔,但是我們沒有給它soures,observer,timer
傅瞻,其實Mode中的這些source,observer,timer
踢代,統(tǒng)稱為這個Mode
的item
,如果一個Mode
中一個item
都沒有嗅骄,則這個RunLoop會直接退出胳挎,不進入循環(huán)(其實線程之所以可以一直存在就是由于RunLoop將其帶入了這個循環(huán)中)。下面我們?yōu)檫@個RunLoop添加個source:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
while (1) {
NSPort *macPort = [NSPort port];
NSLog(@"while begin");
NSRunLoop *subRunLoop = [NSRunLoop currentRunLoop];
[subRunLoop addPort:macPort forMode:NSDefaultRunLoopMode];
[subRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
NSLog(@"while end");
NSLog(@"%@",subRunLoop);
}
});
這樣我們可以看到能夠?qū)崿F(xiàn)了和主線程中相同的效果溺森,線程在這個地方暫停了慕爬,為什么呢?我們明天讓RunLoop在distantFuture
之前都一直run的捌粱医窿?相信大家已經(jīng)猜出出來了。這個時候線程被RunLoop
帶到‘坑’里去了炊林,這個‘坑’就是一個循環(huán)姥卢,在循環(huán)中這個線程可以在沒有任務(wù)的時候休眠,在有任務(wù)的時候被喚醒渣聚;當(dāng)然我們只用一個while(1)
也可以讓這個線程一直存在隔显,但是這個線程會一直在喚醒狀態(tài),及時它沒有任務(wù)也一直處于運轉(zhuǎn)狀態(tài)饵逐,這對于CPU來說是非常不高效的括眠。
小結(jié):我們的RunLoop要想工作,必須要讓它存在一個Item(source,observer或者timer)倍权,主線程之所以能夠一直存在掷豺,并且隨時準(zhǔn)備被喚醒就是應(yīng)為系統(tǒng)為其添加了很多Item
三、深入理解Perform Selector
我們先在主線程中使用下performselector
:
- (void)tryPerformSelectorOnMianThread{
[self performSelector:@selector(mainThreadMethod) withObject:nil]; }
- (void)mainThreadMethod{
NSLog(@"execute %s",__func__);
// print: execute -[ViewController mainThreadMethod]
}
這樣我們在ViewDidLoad中調(diào)用tryPerformSelectorOnMianThread
,就會立即執(zhí)行薄声,并且輸出:print: execute -[ViewController mainThreadMethod];
和上面的例子一樣当船,我們使用GCD,讓這個方法在后臺線程中執(zhí)行
- (void)tryPerformSelectorOnBackGroundThread{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self performSelector:@selector(backGroundThread) onThread:[NSThread currentThread] withObject:nil waitUntilDone:NO];
});
}
- (void)backGroundThread{
NSLog(@"%u",[NSThread isMainThread]);
NSLog(@"execute %s",__FUNCTION__);
}
同樣的,我們調(diào)用tryPerformSelectorOnBackGroundThread
這個方法默辨,我們會發(fā)現(xiàn)德频,下面的backGroundThread
不會被調(diào)用,這是什么原因呢缩幸?
這是因為壹置,在調(diào)用performSelector:onThread: withObject: waitUntilDone
的時候,系統(tǒng)會給我們創(chuàng)建一個Timer的source表谊,加到對應(yīng)的RunLoop上去钞护,然而這個時候我們沒有RunLoop
,如果我們加上RunLoop:
- (void)tryPerformSelectorOnBackGroundThread{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self performSelector:@selector(backGroundThread) onThread:[NSThread currentThread] withObject:nil waitUntilDone:NO];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop run];
});
}
這時就會發(fā)現(xiàn)我們的方法正常被調(diào)用了。那么為什么主線程中的perfom selector
卻能夠正常調(diào)用呢爆办?通過上面的例子相信你已經(jīng)猜到了难咕,主線程的RunLoop是一直存在的,所以我們在主線程中執(zhí)行的時候,無需再添加RunLoop余佃。從Apple的文檔中我們也可以得到驗證:
Each request to perform a selector is queued on the target thread’s run loop and the requests are then processed sequentially in the order in which they were received. 每個執(zhí)行perform selector的請求都以隊列的形式被放到目標(biāo)線程的run loop中暮刃。然后目標(biāo)線程會根據(jù)進入run loop的順序來一一執(zhí)行。
小結(jié):當(dāng)perform selector在后臺線程中執(zhí)行的時候爆土,這個線程必須有一個開啟的runLoop
四椭懊、一直"活著"的后臺線程
現(xiàn)在有這樣一個需求,每點擊一下屏幕雾消,讓子線程做一個任務(wù),然后大家一般會想到這樣的方式:
@interface ViewController ()
@property(nonatomic,strong) NSThread *myThread;
@end
@implementation ViewController
- (void)alwaysLiveBackGoundThread{
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(myThreadRun) object:@"etund"];
self.myThread = thread;
[self.myThread start];
}
- (void)myThreadRun{
NSLog(@"my thread run");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%@",self.myThread);
[self performSelector:@selector(doBackGroundThreadWork) onThread:self.myThread withObject:nil waitUntilDone:NO];
}
- (void)doBackGroundThreadWork{
NSLog(@"do some work %s",__FUNCTION__);
}
@end
這個方法中灾搏,我們利用一個強引用來獲取了后臺線程中的thread,然后在點擊屏幕的時候,在這個線程上執(zhí)行doBackGroundThreadWork
這個方法立润,此時我們可以看到狂窑,在touchesBegin
方法中,self.myThread是存在的桑腮,但是這是為是什么呢泉哈?這就要從線程的五大狀態(tài)來說明了:新建狀態(tài)、就緒狀態(tài)破讨、運行狀態(tài)丛晦、阻塞狀態(tài)、死亡狀態(tài)提陶,這個時候盡管內(nèi)存中還有線程烫沙,但是這個線程在執(zhí)行完任務(wù)之后已經(jīng)死亡了,經(jīng)過上面的論述隙笆,我們應(yīng)該怎樣處理呢锌蓄?我們可以給這個線程的RunLoop添加一個source,那么這個線程就會檢測這個source等待執(zhí)行撑柔,而不至于死亡(有工作的強烈愿望而不死亡):
- (void)myThreadRun{
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run]
NSLog(@"my thread run");
}
這個時候再次點擊屏幕瘸爽,我們就會發(fā)現(xiàn),后臺線程中執(zhí)行的任務(wù)可以正常進行了铅忿。
小結(jié):正常情況下剪决,后臺線程執(zhí)行完任務(wù)之后就處于死亡狀態(tài),我們要避免這種情況的發(fā)生可以利用RunLoop檀训,并且給它一個Source這樣來保證線程依舊還在
五柑潦、深入理解NSTimer
我們平時使用NSTimer,一般是在主線程中的肢扯,代碼大多如下:
- (void)tryTimerOnMainThread{
NSTimer *myTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self
selector:@selector(timerAction) userInfo:nil repeats:YES];
[myTimer fire];
}
- (void)timerAction{
NSLog(@"timer action");
}
這個時候代碼按照我們預(yù)定的結(jié)果運行妒茬,如果我們把這個Tiemr放到后臺線程中呢?
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSTimer *myTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
[myTimer fire];
});
這個時候我們會發(fā)現(xiàn),這個timer只執(zhí)行了一次蔚晨,就停止了。這是為什么呢?通過上面的講解铭腕,想必你已經(jīng)知道了银择,NSTimer,只有注冊到RunLoop之后才會生效,這個注冊是由系統(tǒng)自動給我們完成的,既然需要注冊到RunLoop,那么我們就需要有一個RunLoop
,我們在后臺線程中加入如下的代碼:
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop run];
這樣我們就會發(fā)現(xiàn)程序正常運行了累舷。在Timer注冊到RunLoop之后浩考,RunLoop會為其重復(fù)的時間點注冊好事件,比如1:10被盈,1:20析孽,1:30這幾個時間點。有時候我們會在這個線程中執(zhí)行一個耗時操作只怎,這個時候RunLoop為了節(jié)省資源袜瞬,并不會在非常準(zhǔn)確的時間點回調(diào)這個Timer,這就造成了誤差(Timer有個冗余度屬性叫做tolerance
,它標(biāo)明了當(dāng)前點到后身堡,容許有多少最大誤差)邓尤,可以在執(zhí)行一段循環(huán)之后調(diào)用一個耗時操作,很容易看到timer會有很大的誤差贴谎,這說明在線程很閑的時候使用NSTiemr是比較傲你準(zhǔn)確的汞扎,當(dāng)線程很忙碌時候會有較大的誤差。系統(tǒng)還有一個CADisplayLink
,也可以實現(xiàn)定時效果擅这,它是一個和屏幕的刷新率一樣的定時器澈魄。如果在兩次屏幕刷新之間執(zhí)行一個耗時的任務(wù),那其中就會有一個幀被跳過去仲翎,造成界面卡頓痹扇。另外GCD也可以實現(xiàn)定時器的效果,由于其和RunLoop沒有關(guān)聯(lián)谭确,所以有時候使用它會更加的準(zhǔn)確帘营,這在最后會給予說明。
六逐哈、讓兩個后臺線程有依賴性的一種方式
給兩個后臺線程添加依賴可能有很多的方式芬迄,這里說明一種利用RunLoop
實現(xiàn)的方式。原理很簡單昂秃,我們先讓一個線程工作禀梳,當(dāng)工作完成之后喚醒另外的一線程,通過上面對RunLoop
的說明,相信大家很容易能夠理解這些代碼:
- (void)runLoopAddDependance{
self.runLoopThreadDidFinishFlag = NO;
NSLog(@"Start a New Run Loop Thread");
NSThread *runLoopThread = [[NSThread alloc] initWithTarget:self selector:@selector(handleRunLoopThreadTask) object:nil];
[runLoopThread start];
NSLog(@"Exit handleRunLoopThreadButtonTouchUpInside");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (!_runLoopThreadDidFinishFlag) {
self.myThread = [NSThread currentThread];
NSLog(@"Begin RunLoop");
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
NSPort *myPort = [NSPort port];
[runLoop addPort:myPort forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
NSLog(@"End RunLoop");
[self.myThread cancel];
self.myThread = nil;
}
});
}
- (void)handleRunLoopThreadTask
{
NSLog(@"Enter Run Loop Thread");
for (NSInteger i = 0; i < 5; i ++) {
NSLog(@"In Run Loop Thread, count = %ld", i);
sleep(1);
}
#if 0
// 錯誤示范
_runLoopThreadDidFinishFlag = YES;
// 這個時候并不能執(zhí)行線程完成之后的任務(wù)肠骆,因為Run Loop所在的線程并不知道runLoopThreadDidFinishFlag被重新賦值算途。Run Loop這個時候沒有被任務(wù)事件源喚醒。
// 正確的做法是使用 "selector"方法喚醒Run Loop蚀腿。 即如下:
#endif
NSLog(@"Exit Normal Thread");
[self performSelector:@selector(tryOnMyThread) onThread:self.myThread withObject:nil waitUntilDone:NO];
// NSLog(@"Exit Run Loop Thread");
}
七嘴瓤、NSURLConnection的執(zhí)行過程
在使用NSURLConnection時扫外,我們會傳入一個Delegate,當(dāng)我們調(diào)用了[connection start]
之后,這個Delegate會不停的收到事件的回調(diào)廓脆。實際上筛谚,start這個函數(shù)的內(nèi)部會獲取CurrentRunloop,然后在其中的DefaultMode中添加4個source停忿。如下圖所示驾讲,CFMultiplexerSource是負責(zé)各種Delegate回調(diào)的,CFHTTPCookieStorage是處理各種Cookie的席赂。如下圖所示:
從中可以看出吮铭,當(dāng)開始網(wǎng)絡(luò)傳輸是,我們可以看到NSURLConnection創(chuàng)建了兩個新的線程:com.apple.NSURLConnectionLoader和com.apple.CFSocket.private颅停。其中CFSocket是處理底層socket鏈接的谓晌。NSURLConnectionLoader這個線程內(nèi)部會使用RunLoop來接收底層socket的事件,并通過之前添加的source便监,來通知(
喚醒
)上層的Delegate扎谎。這樣我們就可以理解我們平時封裝網(wǎng)絡(luò)請求時候常見的下面邏輯了:
while (!_isEndRequest)
{
NSLog(@"entered run loop");
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
NSLog(@"main finished,task be removed");
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
_isEndRequest = YES;
}
這里我們就可以解決下面這些疑問了:
- 為什么這個While循環(huán)不停的執(zhí)行烧董,還需要使用一個RunLoop? 程序執(zhí)行一個while循環(huán)是不會耗費很大性能的毁靶,我們這里的目的是想讓子線程在有任務(wù)的時候處理任務(wù),沒有任務(wù)的時候休眠逊移,來節(jié)約CPU的開支预吆。
- 如果沒有為RunLoop添加item,那么它就會立即退出,這里的item呢? 其實系統(tǒng)已經(jīng)給我們默認添加了4個source了胳泉。
- 既然
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
讓線程在這里停下來拐叉,那么為什么這個循環(huán)會持續(xù)的執(zhí)行呢?因為這個一直在處理任務(wù)扇商,并且接受系統(tǒng)對這個Delegate的回調(diào)凤瘦,也就是這個回調(diào)喚醒了這個線程,讓它在這里循環(huán)案铺。
八蔬芥、AFNetWorking中是如何使用RunLoop的?
在AFN中AFURLConnectionOperation是基于NSURLConnection構(gòu)建的,其希望能夠在后臺線程來接收Delegate的回調(diào)控汉。
為此AFN創(chuàng)建了一個線程,然后在里面開啟了一個RunLoop笔诵,然后添加item
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
這里這個NSMachPort
的作用和上文中的一樣,就是讓線程不至于在很快死亡姑子,然后RunLoop不至于退出(如果要使用這個MachPort的話乎婿,調(diào)用者需要持有這個NSMachPort,然后在外部線程通過這個port發(fā)送信息到這個loop內(nèi)部,它這里沒有這么做)街佑。然后和上面的做法相似谢翎,在需要后臺執(zhí)行這個任務(wù)的時候捍靠,會通過調(diào)用:[NSObject performSelector:onThread:..]
來將這個任務(wù)扔給后臺線程的RunLoop中來執(zhí)行。
- (void)start {
[self.lock lock];
if ([self isCancelled]) {
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
} else if ([self isReady]) {
self.state = AFOperationExecutingState;
[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
[self.lock unlock];
}
GCD定時器的實現(xiàn)
- (void)gcdTimer{
// get the queue
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// creat timer
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// config the timer (starting time岳服,interval)
// set begining time
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
// set the interval
uint64_t interver = (uint64_t)(1.0 * NSEC_PER_SEC);
dispatch_source_set_timer(self.timer, start, interver, 0.0);
dispatch_source_set_event_handler(self.timer, ^{
// the tarsk needed to be processed async
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 100000; i++) {
NSLog(@"gcdTimer");
}
});
});
dispatch_resume(self.timer);
}
九剂公、延伸閱讀
- http://chun.tips/blog/2014/10/20/zou-jin-run-loopde-shi-jie-%5B%3F%5D-:shi-yao-shi-run-loop%3F/
- https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW1
- http://www.cocoachina.com/ios/20150601/11970.html
- http://www.reibang.com/p/de2716807570
- http://blog.csdn.net/enuola/article/details/9163051