IOS---實例化講解RunLoop

實例化講解RunLoop

親,我的簡書已不再維護和更新了,所有文章都遷移到了我的個人博客:https://mikefighting.github.io/,歡迎交流。

之前看過很多有關RunLoop的文章,其中要么是主要介紹RunLoop的基本概念,要么是主要講解RunLoop的底層原理,很少用真正的實例來講解RunLoop的,這其中有大部分原因是由于大家在項目中很少能用到RunLoop吧。基于這種原因,本文中將用很少的篇幅來對基礎內容做以介紹,然后主要利用實例來加深大家對RunLoop的理解,本文中的代碼已經上傳GitHub,大家可以下載查看,有問題歡迎Issue我。本文主要分為如下幾個部分:

  • RunLoop的基礎知識
  • 初識RunLoop,如何讓RunLoop進駐線程
  • 深入理解Perform Selector
  • 一直"活著"的后臺線程
  • 深入理解NSTimer
  • 讓兩個后臺線程有依賴性的一種方式
  • NSURLConnetction的內部實現
  • AFNetWorking中是如何使用RunLoop的?
  • 其它:利用GCD實現定時器功能
  • 延伸閱讀

一、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以及observerRunLoop Mode包含以下幾種: NSDefaultRunLoopMode,NSEventTrackingRunLoopMode,UIInitializationRunLoopMode,NSRunLoopCommonModes,NSConnectionReplyMode,NSModalPanelRunLoopMode,至于這些mode各自的含義,讀者可自己查詢,網上不乏這類資源;

二、初識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,統稱為這個Modeitem,如果一個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

三、深入理解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。從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. 每個執行perform selector的請求都以隊列的形式被放到目標線程的run loop中。然后目標線程會根據進入run loop的順序來一一執行。

小結:當perform selector在后臺線程中執行的時候,這個線程必須有一個開啟的runLoop

四、一直"活著"的后臺線程

現在有這樣一個需求,每點擊一下屏幕,讓子線程做一個任務,然后大家一般會想到這樣的方式:

@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這樣來保證線程依舊還在

五、深入理解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沒有關聯,所以有時候使用它會更加的準確,這在最后會給予說明

六、讓兩個后臺線程有依賴性的一種方式

給兩個后臺線程添加依賴可能有很多的方式,這里說明一種利用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");
}

七、NSURLConnection的執行過程

在使用NSURLConnection時,我們會傳入一個Delegate,當我們調用了[connection start]之后,這個Delegate會不停的收到事件的回調。實際上,start這個函數的內部會獲取CurrentRunloop,然后在其中的DefaultMode中添加4個source。如下圖所示,CFMultiplexerSource是負責各種Delegate回調的,CFHTTPCookieStorage是處理各種Cookie的。如下圖所示:

NSURLConnection的執行過程

從中可以看出,當開始網絡傳輸是,我們可以看到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;
 
 } 

這里我們就可以解決下面這些疑問了:

  1. 為什么這個While循環不停的執行,還需要使用一個RunLoop? 程序執行一個while循環是不會耗費很大性能的,我們這里的目的是想讓子線程在有任務的時候處理任務,沒有任務的時候休眠,來節約CPU的開支。
  2. 如果沒有為RunLoop添加item,那么它就會立即退出,這里的item呢? 其實系統已經給我們默認添加了4個source了。
  3. 既然[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];讓線程在這里停下來,那么為什么這個循環會持續的執行呢?因為這個一直在處理任務,并且接受系統對這個Delegate的回調,也就是這個回調喚醒了這個線程,讓它在這里循環。

八、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);

}

九、延伸閱讀

  1. http://chun.tips/blog/2014/10/20/zou-jin-run-loopde-shi-jie-%5B%3F%5D-:shi-yao-shi-run-loop%3F/
  2. https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW1
  3. http://www.cocoachina.com/ios/20150601/11970.html
  4. http://www.lxweimin.com/p/de2716807570
  5. http://blog.csdn.net/enuola/article/details/9163051
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容