實例化講解RunLoop
以前看過不少有關RunLoop的文章,其中要麼是主要介紹RunLoop的基本概念,要麼是主要講解RunLoop的底層原理,不多用真正的實例來說解RunLoop的,這其中有大部分緣由是因爲你們在項目中不多能用到RunLoop吧。基於這種緣由,本文中將用不多的篇幅來對基礎內容作以介紹,而後主要利用實例來加深你們對RunLoop的理解。本文主要分爲以下幾個部分:
RunLoop的基礎知識
初識RunLoop,如何讓RunLoop進駐線程
深刻理解Perform Selector
一直"活着"的後臺線程
深刻理解NSTimer
讓兩個後臺線程有依賴性的一種方式
NSURLConnetction的內部實現
AFNetWorking中是如何使用RunLoop的?
其它:利用GCD實現定時器功能
延伸閱讀
1、RunLoop的基本概念:
什麼是RunLoop?提到RunLoop,咱們通常都會提到線程,這是爲何呢?先來看下官方對RunLoop的定義:RunLoop系統中和線程相關的基礎架構的組成部分(和線程相關),一個RunLoop是一個事件處理環,系統利用這個事件處理環來安排事務,協調輸入的各類事件。RunLoop的目的是讓你的線程在有工做的時候忙碌,沒有工做的時候休眠(和線程相關)。可能這樣說你還不是特別清楚RunLoop到底是用來作什麼的,打個比方來講明:咱們把線程比做一輛跑車,把這輛跑車的主人比做RunLoop,那麼在沒有'主人'的時候,這個跑車的生命是直線型的,其啓動,運行完以後就會廢棄(沒有人對其進行控制,'撞壞'被收回),當有了RunLoop這個主人以後,‘線程’這輛跑車的生命就有了保障,這個時候,跑車的生命是環形的,而且在主人有比賽任務的時候就會被RunLoop這個主人所喚醒,在沒有任務的時候能夠休眠(在IOS中,開啓線程是很消耗性能的,開啓主線程要消耗1M內存,開啓一個後臺線程須要消耗512k內存,咱們應當在線程沒有任務的時候休眠,來釋放所佔用的資源,以便CPU進行更加高效的工做),這樣能夠增長跑車的效率,也就是說RunLoop是爲線程所服務的。這個例子有點不是很貼切,線程和RunLoop之間是以鍵值對的形式一一對應的,其中key是thread,value是runLoop(這點能夠從蘋果公開的源碼中看出來),其實RunLoop是管理線程的一種機制,這種機制不只在IOS上有,在Node.js中的EventLoop,Android中的Looper,都有相似的模式。剛纔所說的比賽任務就是喚醒跑車這個線程的一個source;RunLoop Mode就是,一系列輸入的source,timer以及observer,RunLoop Mode包含如下幾種:
NSDefaultRunLoopMode,
NSEventTrackingRunLoopMode,
UIInitializationRunLoopMode,
NSRunLoopCommonModes,
NSConnectionReplyMode,
NSModalPanelRunLoopMode
複製代碼
至於這些mode各自的含義,讀者可本身查詢,網上不乏這類資源;
2、初識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" );
}
複製代碼
這個時候咱們能夠看到主線程在執行完[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; 以後被阻塞而沒有執行下面的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" );
}
});
複製代碼
這個時候咱們發現這個while循環會一直在執行;這是爲何呢?咱們先將這兩個RunLoop分別打印出來:
主線程的RunLoop
因爲這個日誌比較長,我就只截取了上面的一部分。咱們再看咱們新建的子線程中的RunLoop,打印出來以後:
backGroundThreadRunLoop.png
從中能夠看出來:咱們新建的線程中:
sources0 = (null),
sources1 = (null),
observers = (null),
timers = (null)
複製代碼
咱們看到雖然有Mode,可是咱們沒有給它soures,observer,timer,其實Mode中的這些source,observer,timer,統稱爲這個Mode的item,若是一個Mode中一個item都沒有,則這個RunLoop會直接退出,不進入循環(其實線程之因此能夠一直存在就是因爲RunLoop將其帶入了這個循環中)。下面咱們爲這個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 for Mode:NSDefaultRunLoopMode];
[subRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
NSLog(@"while end" );
NSLog(@"%@" ,subRunLoop);
}
});
複製代碼
這樣咱們能夠看到可以實現了和主線程中相同的效果,線程在這個地方暫停了,爲何呢?咱們明天讓RunLoop在distantFuture以前都一直run的啊?相信你們已經猜出出來了。這個時候線程被RunLoop帶到‘坑’裏去了,這個‘坑’就是一個循環,在循環中這個線程能夠在沒有任務的時候休眠,在有任務的時候被喚醒;固然咱們只用一個while(1)也可讓這個線程一直存在,可是這個線程會一直在喚醒狀態,及時它沒有任務也一直處於運轉狀態,這對於CPU來講是很是不高效的。
小結:咱們的RunLoop要想工做,必需要讓它存在一個Item(source,observer或者timer),主線程之因此可以一直存在,而且隨時準備被喚醒就是應爲系統爲其添加了不少Item
3、深刻理解Perform Selector
咱們先在主線程中使用下performselector:
- (void)tryPerformSelectorOnMianThread{
[self performSelector:@selector(mainThreadMethod) withObject:nil]; }
- (void)mainThreadMethod{
NSLog(@"execute %s" ,__func__);
// print : execute -[ViewController mainThreadMethod]
}
複製代碼
這樣咱們在ViewDidLoad中調用tryPerformSelectorOnMianThread,就會當即執行,而且輸出:print: execute -[ViewController mainThreadMethod];
和上面的例子同樣,咱們使用GCD,讓這個方法在後臺線程中執行
- (void)tryPerformSelectorOnBackGroundThread{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self performSelector:@selector(backGroundThread) onThread:[NSThread currentThread] withObject:nil wait UntilDone:NO];
});
}
- (void)backGroundThread{
NSLog(@"%u" ,[NSThread isMainThread]);
NSLog(@"execute %s" ,__FUNCTION__);
}
複製代碼
一樣的,咱們調用tryPerformSelectorOnBackGroundThread這個方法,咱們會發現,下面的backGroundThread不會被調用,這是什麼緣由呢?
這是由於,在調用performSelector:onThread: withObject: waitUntilDone的時候,系統會給咱們建立一個Timer的source,加到對應的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 wait UntilDone:NO];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop run];
});
}
複製代碼
這時就會發現咱們的方法正常被調用了。那麼爲何主線程中的perfom selector卻可以正常調用呢?經過上面的例子相信你已經猜到了,主線程的RunLoop是一直存在的,因此咱們在主線程中執行的時候,無需再添加RunLoop。
小結:當perform selector在後臺線程中執行的時候,這個線程必須有一個開啓的runLoop
4、一直"活着"的後臺線程
如今有這樣一個需求,每點擊一下屏幕,讓子線程作一個任務,而後你們通常會想到這樣的方式:
@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(do BackGroundThreadWork) onThread:self.myThread withObject:nil wait UntilDone:NO];
}
- (void)do BackGroundThreadWork{
NSLog(@"do some work %s" ,__FUNCTION__);
}
@end
複製代碼
這個方法中,咱們利用一個強引用來獲取了後臺線程中的thread,而後在點擊屏幕的時候,在這個線程上執行doBackGroundThreadWork這個方法,此時咱們能夠看到,在touchesBegin方法中,self.myThread是存在的,可是這是爲是什麼呢?這就要從線程的五大狀態來講明瞭:新建狀態、就緒狀態、運行狀態、阻塞狀態、死亡狀態,這個時候儘管內存中還有線程,可是這個線程在執行完任務以後已經死亡了,通過上面的論述,咱們應該怎樣處理呢?咱們能夠給這個線程的RunLoop添加一個source,那麼這個線程就會檢測這個source等待執行,而不至於死亡(有工做的強烈願望而不死亡):
- (void)myThreadRun{
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] for Mode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run]
NSLog(@"my thread run" );
}
複製代碼
這個時候再次點擊屏幕,咱們就會發現,後臺線程中執行的任務能夠正常進行了。
小結:正常狀況下,後臺線程執行完任務以後就處於死亡狀態,咱們要避免這種狀況的發生能夠利用RunLoop,而且給它一個Source這樣來保證線程依舊還在
5、深刻理解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" );
}
複製代碼
這個時候代碼按照咱們預約的結果運行,若是咱們把這個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];
});
複製代碼
這個時候咱們會發現,這個timer只執行了一次,就中止了。這是爲何呢?經過上面的講解,想必你已經知道了,NSTimer,只有註冊到RunLoop以後纔會生效,這個註冊是由系統自動給咱們完成的,既然須要註冊到RunLoop,那麼咱們就須要有一個RunLoop,咱們在後臺線程中加入以下的代碼:
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop run];
複製代碼
這樣咱們就會發現程序正常運行了。在Timer註冊到RunLoop以後,RunLoop會爲其重複的時間點註冊好事件,好比1:10,1:20,1:30這幾個時間點。有時候咱們會在這個線程中執行一個耗時操做,這個時候RunLoop爲了節省資源,並不會在很是準確的時間點回調這個Timer,這就形成了偏差(Timer有個冗餘度屬性叫作tolerance,它標明瞭當前點到後,允許有多少最大偏差),能夠在執行一段循環以後調用一個耗時操做,很容易看到timer會有很大的偏差,這說明在線程很閒的時候使用NSTiemr是比較傲你準確的,當線程很忙碌時候會有較大的偏差。系統還有一個CADisplayLink,也能夠實現定時效果,它是一個和屏幕的刷新率同樣的定時器。若是在兩次屏幕刷新之間執行一個耗時的任務,那其中就會有一個幀被跳過去,形成界面卡頓。另外GCD也能夠實現定時器的效果,因爲其和RunLoop沒有關聯,因此有時候使用它會更加的準確,這在最後會給予說明。
6、讓兩個後臺線程有依賴性的一種方式
給兩個後臺線程添加依賴可能有不少的方式,這裏說明一種利用RunLoop實現的方式。原理很簡單,咱們先讓一個線程工做,當工做完成以後喚醒另外的一線程,經過上面對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 for Mode: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);
}
// 錯誤示範
_runLoopThreadDidFinishFlag = YES;
// 這個時候並不能執行線程完成以後的任務,由於Run Loop所在的線程並不知道runLoopThreadDidFinishFlag被從新賦值。Run Loop這個時候沒有被任務事件源喚醒。
// 正確的作法是使用 "selector" 方法喚醒Run Loop。 即以下:
NSLog(@"Exit Normal Thread" );
[self performSelector:@selector(tryOnMyThread) onThread:self.myThread withObject:nil wait UntilDone:NO];
// NSLog(@"Exit Run Loop Thread" );
}
複製代碼
7、NSURLConnection的執行過程
在使用NSURLConnection時,咱們會傳入一個Delegate,當咱們調用了[connection start]以後,這個Delegate會不停的收到事件的回調。實際上,start這個函數的內部會獲取CurrentRunloop,而後在其中的DefaultMode中添加4個source。以下圖所示,CFMultiplexerSource是負責各類Delegate回調的,CFHTTPCookieStorage是處理各類Cookie的。以下圖所示:
從中能夠看出,當開始網絡傳輸是,咱們能夠看到NSURLConnection建立了兩個新的線程:com.apple.NSURLConnectionLoader和com.apple.CFSocket.private。其中CFSocket是處理底層socket連接的。NSURLConnectionLoader這個線程內部會使用RunLoop來接收底層socket的事件,並經過以前添加的source,來通知(喚醒)上層的Delegate。這樣咱們就能夠理解咱們平時封裝網絡請求時候常見的下面邏輯了:
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循環不停的執行,還須要使用一個RunLoop? 程序執行一個while循環是不會耗費很大性能的,咱們這裏的目的是想讓子線程在有任務的時候處理任務,沒有任務的時候休眠,來節約CPU的開支。
若是沒有爲RunLoop添加item,那麼它就會當即退出,這裏的item呢? 其實系統已經給咱們默認添加了4個source了。
既然[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];讓線程在這裏停下來,那麼爲何這個循環會持續的執行呢?由於這個一直在處理任務,而且接受系統對這個Delegate的回調,也就是這個回調喚醒了這個線程,讓它在這裏循環。
8、AFNetWorking中是如何使用RunLoop的?
在AFN中AFURLConnectionOperation是基於NSURLConnection構建的,其但願可以在後臺線程來接收Delegate的回調。爲此AFN建立了一個線程,而後在裏面開啓了一個RunLoop,而後添加item
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] set Name:@"AFNetworking" ];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] for Mode: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的話,調用者須要持有這個NSMachPort,而後在外部線程經過這個port發送信息到這個loop內部,它這裏沒有這麼作)。而後和上面的作法類似,在須要後臺執行這個任務的時候,會經過調用:[NSObject performSelector:onThread:..]來將這個任務扔給後臺線程的RunLoop中來執行。
- (void)start {
[self.lock lock];
if ([self isCancelled]) {
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil wait UntilDone:NO modes:[self.runLoopModes allObjects]];
} else if ([self isReady]) {
self.state = AFOperationExecutingState;
[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil wait UntilDone:NO modes:[self.runLoopModes allObjects]];
}
[self.lock unlock];
}
複製代碼
GCD定時器的實現
- (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);
}
複製代碼