當你試圖解決一個你不理解的問題時,復雜化就產生了。—— AndyBoothe
**RunLoop: **顧名思義也就是循環運行的意思。做iOS 的同學都會接觸到這個概念,但是真正用上的卻不是很多。在這里,我將結合以往的一些經驗及實踐來談談我對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(觀察者)。
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的源碼地址,有興趣的同學可以自行下載。