當你試圖解決一個你不理解的問題時,復(fù)雜化就產(chǎn)生了。—— AndyBoothe
**RunLoop: **顧名思義也就是循環(huán)運行的意思袖肥。做iOS 的同學都會接觸到這個概念咪辱,但是真正用上的卻不是很多振劳。在這里,我將結(jié)合以往的一些經(jīng)驗及實踐來談?wù)勎覍unLoop的理解油狂。
一历恐、 為什么會存在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(觀察者)广匙。
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的源碼地址,有興趣的同學可以自行下載田轧。