深入理解RunLoop

當你試圖解決一個你不理解的問題時,復(fù)雜化就產(chǎn)生了。—— AndyBoothe

**RunLoop: **顧名思義也就是循環(huán)運行的意思袖肥。做iOS 的同學都會接觸到這個概念咪辱,但是真正用上的卻不是很多振劳。在這里,我將結(jié)合以往的一些經(jīng)驗及實踐來談?wù)勎覍unLoop的理解油狂。

一历恐、 為什么會存在RunLoop

官方RunLoop模型圖

我們都知道,oc是一種面向?qū)ο蟮恼Z言专筷,但是代碼的執(zhí)行終究還是面向過程的弱贼,也就是說會有始有終。而線程也是一樣的磷蛹,我們的線程從創(chuàng)建到運行再到銷毀也是會存在一個生命周期的吮旅。在項目開發(fā)中,有時候會存在對持續(xù)異步任務(wù)的需求,那么我們就需要來維護特定線程的生命周期味咳,這時就該輪到RunLoop上場了庇勃。說白了,RunLoop就是來保證你的線程以一種環(huán)形的結(jié)構(gòu)運行下去槽驶,在需要的時候喚醒责嚷,不需要的時候讓線程進入休眠狀態(tài),從而來減少對CPU的開銷掂铐。

二罕拂、RunLoop與線程的關(guān)系

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

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

當我們的程序啟動后,上面的代碼就會被調(diào)用全陨,主線程也就開始執(zhí)行爆班。大家一定注意到了,我們的主線程是一直存在的辱姨,所有的視圖柿菩、控件的操作以及事件鏈的監(jiān)聽都是在主線程下進行的,直到APP退出炮叶。所以可以推測出碗旅,當主線程被創(chuàng)建時渡处,必然存在一個RunLoop來維護它的生命周期,保證后面程序的運行祟辟。線程與RunLoop可以說是一種線性的關(guān)系(一對一)医瘫,除主線程的RunLoop會被自動創(chuàng)建,并運行在默認模式外旧困,子線程的RunLoop是需要我們手動來創(chuàng)建的醇份。

三、認識RunLoop

NSRunLoop是Cocoa框架中的類吼具,與之對應(yīng)僚纷,在Core Fundation中是CFRunLoopRef類。這兩者的區(qū)別是前者不是線程安全的拗盒,而后者是線程安全的怖竭。
這里我們先從CFRunLoopRef中來剖析一下RunLoop的結(jié)構(gòu)。在CoreFoundation里面有關(guān)于RunLoop的5個類:

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

剛才也提高過線程與RunLoop是一一對應(yīng)的關(guān)系陡蝇,而在RunLoop里會存在若干個Mode痊臭,每個Mode下又會存在若干個Source、Timer登夫、Observer(觀察者)广匙。

RunLoop的相關(guān)類關(guān)系圖

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

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

NSConnectionReplyMode: 使用這個Mode去監(jiān)聽NSConnection對象的狀態(tài)恼策,我們很少需要自己使用這個Mode鸦致。

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

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

GSEventReceiveRunLoopMode: 用來接受系統(tǒng)事件分唾,內(nèi)部的Run Loop Mode。

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

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

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

Input source用來投遞異步消息,通常消息來自另外的線程或者程序专酗。在接收到消息并調(diào)用程序指定方法時睹逃,線程中對應(yīng)的NSRunLoop對象會通過執(zhí)行runUntilDate:方法來退出。

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

Input source有兩個不同的種類: Port-Based Sources 和 Custom Input Sources:Port-Based Sources由內(nèi)核自動發(fā)送斑鼻,Custom Input Sources需要從其他線程手動發(fā)送。

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

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

performSelectorOnMainThread:withObject:waitUntilDone:

performSelectorOnMainThread:withObject:waitUntilDone:modes:

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

performSelector:onThread:withObject:waitUntilDone:

performSelector:onThread:withObject:waitUntilDone:modes:

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

performSelector:withObject:afterDelay:

performSelector:withObject:afterDelay:inModes:

4.取消當前線程的調(diào)用

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:有則獲取,無則創(chuàng)建

+ (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的應(yīng)用

CFRunLoopRef的作用主要還是用在對于消息的監(jiān)聽上面些楣,所以這里主要講的是關(guān)于NSRunLoop的應(yīng)用場景。

1.創(chuàng)建一個與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]];
    }
}
**注意:在這里如果輸入源不存在可能會造成線程的循環(huán)空轉(zhuǎn)戈毒,造成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”);代碼是不會執(zhí)行的横堡,我們就可以在handleRunLoopThreadTask方法里執(zhí)行我們想要的操作了

while (!self.runLoopThreadDidFinishFlag) {

NSLog(@"Begin RunLoop");

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

NSLog(@"End RunLoop");

}

NSLog(@"Exit handleRunLoopThreadButtonTouchUpInside");

}

4.在一定時間內(nèi)監(jiān)聽某種事件,或執(zhí)行某種任務(wù)的線程

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時葬项,定時器的方法是不執(zhí)行的。這是因為蘋果公司為了增加用戶的體驗感迹蛤,在用戶進行滑動操作時民珍,會將主線程的RunLoop模式切換到UITrackingRunLoopMode下,UITrackingRunLoopMode的優(yōu)先級高于NSDefaultRunLoopMode盗飒,所以定時器方法會延緩執(zhí)行嚷量。為了避免這種錯誤的發(fā)生,在我們初始化NSTimer時逆趣,可以選擇將其放入UITrackingRunLoopMode或NSRunLoopCommonModes模式下蝶溶。

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

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的源碼地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末摊唇,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子涯鲁,更是在濱河造成了極大的恐慌巷查,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件抹腿,死亡現(xiàn)場離奇詭異岛请,居然都是意外死亡,警方通過查閱死者的電腦和手機警绩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門崇败,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人肩祥,你說我怎么就攤上這事后室。” “怎么了混狠?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵岸霹,是天一觀的道長。 經(jīng)常有香客問我将饺,道長贡避,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任予弧,我火速辦了婚禮刮吧,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘掖蛤。我一直安慰自己杀捻,他們只是感情好,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布蚓庭。 她就那樣靜靜地躺著致讥,像睡著了一般。 火紅的嫁衣襯著肌膚如雪彪置。 梳的紋絲不亂的頭發(fā)上拄踪,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機與錄音拳魁,去河邊找鬼惶桐。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的姚糊。 我是一名探鬼主播贿衍,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼救恨!你這毒婦竟也來了贸辈?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤肠槽,失蹤者是張志新(化名)和其女友劉穎擎淤,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體秸仙,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡嘴拢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了寂纪。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片席吴。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖捞蛋,靈堂內(nèi)的尸體忽然破棺而出孝冒,到底是詐尸還是另有隱情,我是刑警寧澤拟杉,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布庄涡,位于F島的核電站,受9級特大地震影響捣域,放射性物質(zhì)發(fā)生泄漏啼染。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一焕梅、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧卦洽,春花似錦贞言、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蚤霞,卻和暖如春酗失,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背昧绣。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工规肴, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓拖刃,卻偏偏與公主長得像删壮,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子兑牡,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容

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