實例化講解RunLoop

轉自此處

實例化講解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 forMode: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 waitUntilDone: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 waitUntilDone: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(doBackGroundThreadWork) onThread:self.myThread withObject:nil waitUntilDone:NO];
}
- (void)doBackGroundThreadWork{

    NSLog(@"do some work %s",__FUNCTION__);

}
@end
複製代碼

這個方法中,咱們利用一個強引用來獲取了後臺線程中的thread,而後在點擊屏幕的時候,在這個線程上執行doBackGroundThreadWork這個方法,此時咱們能夠看到,在touchesBegin方法中,self.myThread是存在的,可是這是爲是什麼呢?這就要從線程的五大狀態來講明瞭:新建狀態、就緒狀態、運行狀態、阻塞狀態、死亡狀態,這個時候儘管內存中還有線程,可是這個線程在執行完任務以後已經死亡了,通過上面的論述,咱們應該怎樣處理呢?咱們能夠給這個線程的RunLoop添加一個source,那麼這個線程就會檢測這個source等待執行,而不至於死亡(有工做的強烈願望而不死亡):

- (void)myThreadRun{

 [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode: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 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;
// 這個時候並不能執行線程完成以後的任務,由於Run Loop所在的線程並不知道runLoopThreadDidFinishFlag被從新賦值。Run Loop這個時候沒有被任務事件源喚醒。
// 正確的作法是使用 "selector"方法喚醒Run Loop。 即以下:
#endif
NSLog(@"Exit Normal Thread");
[self performSelector:@selector(tryOnMyThread) onThread:self.myThread withObject:nil waitUntilDone: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] 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的話,調用者須要持有這個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 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定時器的實現

- (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);
}
複製代碼
相關文章
相關標籤/搜索