Run loop接收輸入事件來自兩種不同的來源:輸入源(input source)和定時源(timer source)。
輸入源又分為基于端口、基于自定義、基于perform selector。
一.NSRunLoop
在Cocoa中,每個線程(NSThread)對象中內部都有一個run loop(NSRunLoop)對象用來循環處理輸入事件,處理的事件包括兩類,一是來自Input sources的異步事件,一是來自Timer sources的同步事件。run Loop在處理輸入事件時會產生通知,可以通過向線程中添加run-loop observers來監聽特定事件,以在監聽的事件發生時做附加的處理工作。
每個run loop可運行在不同的模式下,一個run loop mode是一個集合,其中包含其監聽的若干輸入事件源、定時器、以及在事件發生時需要通知的run loop observers。運行在一種mode下的run loop只會處理其run loop mode中包含的輸入源事件、定時器事件、以及通知run loop mode中包含的observers。
Cocoa中的預定義模式有:
Default模式
定義:NSDefaultRunLoopMode (Cocoa) kCFRunLoopDefaultMode (Core Foundation)
描述:默認模式中幾乎包含了所有輸入源(NSConnection除外),一般情況下應使用此模式。
Connection模式
定義:NSConnectionReplyMode(Cocoa)
描述:處理NSConnection對象相關事件,系統內部使用,用戶基本不會使用。
Modal模式
定義:NSModalPanelRunLoopMode(Cocoa)
描述:OS X的Modal面板事件。
Event tracking模式
定義:UITrackingRunLoopMode(cocoa)
描述:在拖動loop或其他user interface tracking loops時處于此種模式下,在此模式下會限制輸入事件的處理。例如,當手指按住UITableView拖動時就會處于此模式。
Common模式
定義:NSRunLoopCommonModes (Cocoa) kCFRunLoopCommonModes (Core Foundation)
描述:這是一個偽模式,其為一組run loop mode的集合,將輸入源加入此模式意味著在Common Modes中包含的所有模式下都可以處理。在Cocoa應用程序中,默認情況下Common Modes包含Default Modes、Modal Modes、Event Tracking Modes.可使用CFRunLoopAddCommonMode方法向Common Modes中添加自定義modes。
獲取當前線程的run loop mode
NSString* runLoopMode = [[NSRunLoop currentRunLoop] currentMode];
二.NSTimer、NSURLConnection與UITrackingRunLoopMode
NSTimer與NSURLConnection默認運行在default mode下,這樣當用戶在拖動UITableView處于UITrackingRunLoopMode模式時,NSTimer不能fire,NSURLConnection的數據也無法處理。NSTimer的例子:在一個UITableViewController中啟動一個0.2s的循環定時器,在定時器到期時更新一個計數器,并顯示在label上。
-(void)viewDidLoad{
label =[[UILabel alloc]initWithFrame:CGRectMake(10, 100, 100, 50)];
[self.view addSubview:label];
count = 0;
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval: 1 target: self selector: @selector(incrementCounter:) userInfo: nil repeats: YES];
}
- (void)incrementCounter:(NSTimer *)theTimer{
count++;
label.text = [NSString stringWithFormat:@"%d",count];
}
在正常情況下,可看到每隔0.2s,label上顯示的數字+1,但當你拖動或按住tableView時,label上的數字不再更新,當你手指離開時,label上的數字繼續更新。當你拖動UITableView時,當前線程run loop處于UIEventTrackingRunLoopMode模式,在這種模式下,不處理定時器事件,即定時器無法fire,label上的數字也就無法更新。
解決方法:
一種方法是在另外的線程中處理定時器事件,可把Timer加入到NSOperation中在另一個線程中調度;
還有一種方法是修改Timer運行的run loop模式,將其加入到UITrackingRunLoopMode模式或NSRunLoopCommonModes模式中。即[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];或[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
- (void)viewDidLoad{
[super viewDidLoad];
NSLog(@"主線程 %@", [NSThread currentThread]);
//創建并執行新的線程
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(newThread) object:nil];
[thread start];
}
- (void)newThread{
@autoreleasepool{
//在當前Run Loop中添加timer,模式是默認的NSDefaultRunLoopMode
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(timer_callback) userInfo:nil repeats:YES];
//開始執行新線程的Run Loop,如果不啟動run loop,timer的事件是不會響應的
[[NSRunLoop currentRunLoop] run];
}
}
- (void)timer_callback{
NSLog(@"Timer %@", [NSThread currentThread]);
}
NSURLConnection也是如此,見SDWebImage中的描述,以及SDWebImageDownloader.m代碼中的實現。修改NSURLConnection的運行模式可使用scheduleInRunLoop:forMode:方法。
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:15];
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[connection start];
我們一直在使用RunLoop,卻很少見到它。并且,我們在大多數情況下,都不需要顯式的創建或者啟動RunLoop,有兩種情況,我們卻必須手動設置它:
1、在分線程中使用定時器
定時器的實現便是基于runloop的,平時我們使用定時器你或許并沒有對runloop做什么操作,那是因為主線程的runloop默認是開啟運行的,如果我們在分線程中也需要重復執行某一動作,如下:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(time) userInfo:nil repeats:YES];
});
}
-(void)time{
NSLog(@"run");
}
你會發現,程序運行后并沒有打印任何信息,方法并沒有被調用,我們必須在線程中手動的執行如下代碼:
[[NSRunLoop currentRunLoop] run];
定時器才能正常工作。
2、當你在線程中使用如下方法時
某些延時函數和選擇器在分線程中的使用,我們也必須手動開啟runloop,這些方法如下:
@interface NSObject (NSDelayedPerforming)
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes;
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(id)anArgument;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget;
@end
@interface NSRunLoop (NSOrderedPerform)
- (void)performSelector:(SEL)aSelector target:(id)target argument:(id)arg order:(NSUInteger)order modes:(NSArray *)modes;
- (void)cancelPerformSelector:(SEL)aSelector target:(id)target argument:(id)arg;
- (void)cancelPerformSelectorsWithTarget:(id)target;
@end
Source有兩個版本:Source0 和 Source1。
Source0 只包含了一個回調(函數指針),它并不能主動觸發事件。使用時,你需要先調用 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理,然后手動調用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。
Source1 包含了一個 mach_port 和一個回調(函數指針),被用于通過內核和其他線程相互發送消息。這種 Source 能主動喚醒 RunLoop 的線程。