深入理解RunLoop

當你試圖解決一個你不理解的問題時,復雜化就產生了。—— AndyBoothe

**RunLoop: **顧名思義也就是循環運行的意思。做iOS 的同學都會接觸到這個概念,但是真正用上的卻不是很多。在這里,我將結合以往的一些經驗及實踐來談談我對RunLoop的理解。

一、 為什么會存在RunLoop

官方RunLoop模型圖

我們都知道,oc是一種面向對象的語言,但是代碼的執行終究還是面向過程的,也就是說會有始有終。而線程也是一樣的,我們的線程從創建到運行再到銷毀也是會存在一個生命周期的。在項目開發中,有時候會存在對持續異步任務的需求,那么我們就需要來維護特定線程的生命周期,這時就該輪到RunLoop上場了。說白了,RunLoop就是來保證你的線程以一種環形的結構運行下去,在需要的時候喚醒,不需要的時候讓線程進入休眠狀態,從而來減少對CPU的開銷。

二、RunLoop與線程的關系

在我們的main.m文件里會有這樣的一段代碼:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

當我們的程序啟動后,上面的代碼就會被調用,主線程也就開始執行。大家一定注意到了,我們的主線程是一直存在的,所有的視圖、控件的操作以及事件鏈的監聽都是在主線程下進行的,直到APP退出。所以可以推測出,當主線程被創建時,必然存在一個RunLoop來維護它的生命周期,保證后面程序的運行。線程與RunLoop可以說是一種線性的關系(一對一),除主線程的RunLoop會被自動創建,并運行在默認模式外,子線程的RunLoop是需要我們手動來創建的。

三、認識RunLoop

NSRunLoop是Cocoa框架中的類,與之對應,在Core Fundation中是CFRunLoopRef類。這兩者的區別是前者不是線程安全的,而后者是線程安全的。
這里我們先從CFRunLoopRef中來剖析一下RunLoop的結構。在CoreFoundation里面有關于RunLoop的5個類:

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

剛才也提高過線程與RunLoop是一一對應的關系,而在RunLoop里會存在若干個Mode,每個Mode下又會存在若干個Source、Timer、Observer(觀察者)。

RunLoop的相關類關系圖

Run Loop Mode主要定義有以下幾種:

NSDefaultRunLoopMode: 大多數工作中默認的運行方式。

NSConnectionReplyMode: 使用這個Mode去監聽NSConnection對象的狀態,我們很少需要自己使用這個Mode。

NSModalPanelRunLoopMode: 使用這個Mode在Model Panel情況下去區分事件(OS X開發中會遇到)。

UITrackingRunLoopMode: 使用這個Mode去跟蹤來自用戶交互的事件(比如UITableView上下滑動)。

GSEventReceiveRunLoopMode: 用來接受系統事件,內部的Run Loop Mode。

NSRunLoopCommonModes: 這是一個偽模式,其為一組run loop mode的集合。

每一次運行自己的Run Loop時,都需要顯示或者隱示的指定其運行于哪一種Mode。Run Loop運行時只能以一種固定的Mode運行,并監控這個Mode下添加的Timer source和Input source。如果這個Mode下沒有添加事件源,Run Loop會立刻返回。

Run Loop從兩個不同的事件源中接收消息:

Input source用來投遞異步消息,通常消息來自另外的線程或者程序。在接收到消息并調用程序指定方法時,線程中對應的NSRunLoop對象會通過執行runUntilDate:方法來退出。

Timer source用來投遞timer事件(Schedule或者Repeat)中的同步消息。在處理消息時,并不會退出Run Loop。Run Loop還有一個觀察者Observer的概念,可以往Run Loop中加入自己的觀察者以便監控Run Loop的運行過程。

Input source有兩個不同的種類: Port-Based Sources 和 Custom Input Sources:Port-Based Sources由內核自動發送,Custom Input Sources需要從其他線程手動發送。

Cocoa框架為我們定義了一些Custom Input Sources,允許我們在線程中執行一系列selector方法:

1.在主線程的Run Loop下執行指定的 @selector 方法

performSelectorOnMainThread:withObject:waitUntilDone:

performSelectorOnMainThread:withObject:waitUntilDone:modes:

2.在當前線程的Run Loop下執行指定的 @selector 方法

performSelector:onThread:withObject:waitUntilDone:

performSelector:onThread:withObject:waitUntilDone:modes:

3.在當前線程的Run Loop下延遲加載指定的 @selector 方法

performSelector:withObject:afterDelay:

performSelector:withObject:afterDelay:inModes:

4.取消當前線程的調用

cancelPreviousPerformRequestsWithTarget:

cancelPreviousPerformRequestsWithTarget:selector:object:

以下是在CFRunLoopRef下添加Sources和Observer的方法:

- (void)runDefaultLoop {
    
    CFRunLoopSourceContext context = {0, (__bridge void *)(URLConnection), NULL, NULL, NULL, NULL, NULL, ScheduleCallBack, CancelCallBack, PerformCallBack};
    CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    
    while (KRunAlways) {
        @autoreleasepool {
            CFRunLoopRun();
        }
    }
}

void ScheduleCallBack(void *info, CFRunLoopRef rl, CFRunLoopMode mode)
{  
}
void CancelCallBack(void *info, CFRunLoopRef rl, CFRunLoopMode mode)
{  
}
void PerformCallBack(void *info)
{
}

四、RunLoop的使用

1.獲取當前線程的RunLoop:有則獲取,無則創建

+ (NSRunLoop *)currentRunLoop;

2.獲取主線程的RunLoop

+ (NSRunLoop *)mainRunLoop ;

3.獲取RunLoop的CFRunLoopRef對象

- (CFRunLoopRef)getCFRunLoop;

4.將定時器添加到runloop中

- (void)addTimer:(NSTimer *)timer forMode:(NSString *)mode;

5.添加輸入源端口到runloop中,NSPort對象可以理解為詳細的載體,會傳遞消息與其代理。

- (void)addPort:(NSPort *)aPort forMode:(NSString *)mode;

6.將某個輸入源端口移除

- (void)removePort:(NSPort *)aPort forMode:(NSString *)mode;

7.開始運行

- (void)run;

8.在某個期限前運行

- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

五、RunLoop的應用

CFRunLoopRef的作用主要還是用在對于消息的監聽上面,所以這里主要講的是關于NSRunLoop的應用場景。

1.創建一個與APP生命周期相同的子線程(不太推薦)

- (id)init{
    if (self = [super init]) {
        mdapThread_ = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
        mdapThread_.name = @"MdapThread";
        isThreadNeedRun = YES;
        conditionLock_ = [[NSConditionLock alloc] init];
        
        [mdapThread_ start];
    }
    
    return self;
}
- (void)run{
    // 為runloop 加入輸入源
    [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop]run];
}

2.維護線程的生命周期,讓線程不主動退出

- (id)init{
    if (self = [super init]) {
        mdapThread_ = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
        mdapThread_.name = @"MdapThread";
        isThreadNeedRun = YES;
        conditionLock_ = [[NSConditionLock alloc] init];
        
        [mdapThread_ start];
    }
    
    return self;
}
- (void)run{
    // 為runloop 加入輸入源
    [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    while (isThreadNeedRun) {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }
}
**注意:在這里如果輸入源不存在可能會造成線程的循環空轉,造成CPU的浪費**

3.阻塞線程

(void)handleRunLoopThreadButtonTouchUpInside
{

NSLog(@"Enter handleRunLoopThreadButtonTouchUpInside");

self.runLoopThreadDidFinishFlag = NO;

NSThread *runLoopThread = [[NSThread alloc] initWithTarget:self selector:@selector(handleRunLoopThreadTask) object:nil];

[runLoopThread start];

//在這里如果self.runLoopThreadDidFinishFlag不為YES,則  NSLog(@"Exit handleRunLoopThreadButtonTouchUpInside”);代碼是不會執行的,我們就可以在handleRunLoopThreadTask方法里執行我們想要的操作了

while (!self.runLoopThreadDidFinishFlag) {

NSLog(@"Begin RunLoop");

[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

NSLog(@"End RunLoop");

}

NSLog(@"Exit handleRunLoopThreadButtonTouchUpInside");

}

4.在一定時間內監聽某種事件,或執行某種任務的線程

NSTimer*udpateTimer=[NSTimer timerWithTimeInterval:30

target:self

selector:@selector(onTimerFired:)userInfo:nil

repeats:YES];

[NSRunLoopcurrentRunLoop] addTimer:udpateTimerforMode:NSRunLoopCommonModes];

注意:NSTimer的初始化有兩種scheduledTimerWithTimeInterval和timerWithTimeInterval。在使用scheduledTimerWithTimeInterval進行初始化時,它是會被自動的添加到NSDefaultRunLoopMode這種模式下的。而使用timerWithTimeInterval初始化時則需要我們來手動的添加Mode。那么為什么會有這兩種情況呢?不知道大家有沒有遇到過這樣的情況,就是當NSTimer運行在NSDefaultRunLoopMode模式下,如果我們在滑動頁面如UIScrollView或UITableView時,定時器的方法是不執行的。這是因為蘋果公司為了增加用戶的體驗感,在用戶進行滑動操作時,會將主線程的RunLoop模式切換到UITrackingRunLoopMode下,UITrackingRunLoopMode的優先級高于NSDefaultRunLoopMode,所以定時器方法會延緩執行。為了避免這種錯誤的發生,在我們初始化NSTimer時,可以選擇將其放入UITrackingRunLoopMode或NSRunLoopCommonModes模式下。

5.避免APP的崩潰
我們可以在自定義的錯誤捕捉方法里,添加這樣一段代碼來處理app崩潰事件,可以有效的阻止app奔潰。(關于具體的實現方法,有興趣的同學可以看看我在簡書里的另一篇關于崩潰捕獲的博客)

CFRunLoopRef runLoop = CFRunLoopGetCurrent();

CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);

while (!_isDismisssed) {

for (NSString *mode in (NSArray *)allModes) {

CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);

}

}

CFRelease(allModes);

另外附送上CFRunLoop的源碼地址,有興趣的同學可以自行下載。

CFRunLoop的源碼地址

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • RunLoop的定義與概念RunLoop的主要作用main函數中的RunLoopRunLoop與線程的關系RunL...
    __silhouette閱讀 1,021評論 0 6
  • 轉載:http://www.cocoachina.com/ios/20150601/11970.html RunL...
    Gatling閱讀 1,471評論 0 13
  • http://www.cocoachina.com/ios/20150601/11970.html RunLoop...
    紫色冰雨閱讀 867評論 0 3
  • 深入理解RunLoop 由ibireme| 2015-05-18 |iOS,技術 RunLoop 是 iOS 和 ...
    橙娃閱讀 883評論 1 2
  • 我們會有小小的家。城市的一方煙火,容納著溫暖的陽臺,晝夜的情話和蠢萌的貓狗。你從心上人,變成枕邊人。我稚嫩地牽著你...
    空蘭山閱讀 185評論 0 0