RunLoop总结
RunLoop总览
RunLoop指的是NSRunloop(基于foundation框架)或者CFRunloopRef(基于core foundation框架),CFRunloopRef是纯C的函数,而NSRunloop仅仅是CFRunloopRef的OC封装,并未提供额外的其他功能。可以将runloop理解为一个处理消息循环的对象,有消息时就处理,没有消息的时候就休眠。两者是可以相互转化的,等价的。NSRunLoop是基于CFRunLoopRef的一层OC包装。RunLoop的作用就是保持程序的持续运行,处理app的各种事件,提高CPU的使用率,有事情就做,没事情做就休眠。
RunLoop和线程的关系
runloop的创建是不让开发人员手动创建的,runloop和线程是一一对应的,具有唯一性,并且区分是否为主线程。
一个线程对应一个runloop,是键值对的关系存在的。主线程RunLoop在线程启动的时候,系统自动创建,子线程的RunLoop需要手动调用currentRunLoop方法让系统创建。RunLoop都是在第一次获取的时候创建,在线程结束时销毁。runloop并不是在alloc init方法中创建的,而是通过调用currentRunLoop方法来创建的,可以看出非主线程只有在自己线程内才可以获得runloop。RunLoop的创建是懒加载的。
RunLoop的生命周期
RunLoop的生命周期创建->运行(开启,内部循环)->退出
RunLoop的相关方法
+ (NSRunLoop *)currentRunLoop
如果调用的线程中没有runloop,那么将会创建一个并返回
+ (NSRunLoop *)mainRunLoop
返回主线程的runloop
- (void)acceptInputForMode:(NSString *)mode beforeDate:(NSDate *)limitDate
运行loop一次或者直到limitDate。如果没有input sources加入到这个loop,那么马上返回;否则一直运行到limitDate,或者接口到一个input source然后返回。
- (void)addPort:(NSPort *)aPort forMode:(NSString *)mode
- (void)addTimer:(NSTimer *)aTimer forMode:(NSString *)mode
port和timer都可以添加到多个mode中
- (void)cancelPerformSelector:(SEL)aSelector target:(id)target argument:(id)anArgument
取消所有mode中的perform select,argument必须跟指定调用时候的一样
- (void)cancelPerformSelectorsWithTarget:(id)target
- (NSString *)currentMode
如果run loop没有运行,那么返回nil
- (CFRunLoopRef)getCFRunLoop
- (NSDate *)limitDateForMode:(NSString *)mode
下一次运行的时间,如果没有指定的mode上没有input source,返回nil
- (void)performSelector:(SEL)aSelector target:(id)target argument:(id)anArgument order:(NSUInteger)order modes:(NSArray *)modes
order值越低优先级越高
- (void)removePort:(NSPort *)aPort forMode:(NSString *)mode
- (void)run
在default mode下无限运行loop,但是如果没有任何input source,会立即返回。手动移除所有已知的inout source并不能保证run loop停止运行,因为系统可能会添加一些input source。
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate
运行input source一次,为指定mode的input阻塞直到date的时间。如过没有input source,立即返回并返回NO。
- (void)runUntilDate:(NSDate *)limitDate
如果没有input source,立即返回。否则在limitDate到来之前,不停的循环。
RunLoop的获得
获取runloop只有两个方法,currentRunLoop和mainRunLoop
NSRunLoop * runloop1 = [NSRunLoop mainRunLoop];
CFRunLoopRef runloop2 = CFRunLoopGetMain();
开启runloop的方法
NSRunLoop提供的方法:
- (void)run; //默认模式
- (void)runUntilDate:(NSDate *)limitDate;
- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
CFRunLoop提供的函数:
/// 默认模式
void CFRunLoopRun(void);
/// 在指定模式,指定时间,运行
CFRunLoopRunResult CFRunLoopRunInMode(CFRunLoopMode mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);
当执行了上面的运行方法后,如果runloop所在的模式没有对应的事件源,即上面图中提到的input sources、timer sources,会直接退出当前runloop(注意:是当前)。另外注意的是,input sources里面的Selector Sources,它有一些特殊情况,上面也提到了。这些情况下runloop还是会直接退出。
runloop退出的方式
1.最大的运行时间到期,推荐使用这个方式
2.modeItem(事件源)为空,不推荐使用,因为一些系统的Item并不知道。
3.调用CFRunLoopStop,退出runloop并将程序的控制权交给调用者(如果runloop有嵌套,则只退出最内层的runloop),一些情况下,CFRunLoopStop并不能真正的退出runloop,
比如使用
- (void)run; //默认模式
- (void)runUntilDate:(NSDate *)limitDate;
当执行NSRunLoop的run方法,一旦成功(默认模式下有事件源),那么run会不停的调用runMode:beforeDate:来运行runloop,那么即便CFRunLoopStop退出了一个runloop,很快会有另一个runloop执行。即:如果你想退出一个runloop,那么你就不该调用run方法来开启runloop
runUntilDate:与run一样不停的执行runMode:beforeDate:方法,CFRunLoopStop也是退不出来的,不同的是runUntilDate:自己有个期限,超过这个期限会自动退出
很明显,你会想到利用事件源为空来退出,这种方法上面已经说了,不推荐。。。
当runloop运行后,如果runloop所在的模式没有对应的事件源,就会直接退出当前runloop,runloop的退出和observer没有关系,observer只是起到了监听的作用。
runloop的内部循环图
每个runloop可以运行在不同的模式下,在一个时刻只能运行在一种模式下,一个run loop mode是一个集合,其中包含了其监听的若干事件源,定时器以及事件发生时需要通知的run loop observers。运行在一种mode下的run loop只会处理其run loop mode中包含的源事件,定时器事件,以及
通知run loop mode中包含的observers。
RunLoop五个相关的类
CFRunloopRef
CFRunloopModeRef【Runloop的运行模式】
CFRunloopSourceRef【Runloop要处理的事件源】
CFRunloopTimerRef【Timer事件】
CFRunloopObserverRef【Runloop的观察者(监听者)】
CFRunloopRef
(1) CFRunloopModeRef代表着Runloop的运行模式
(2)一个Runloop中可以有多个mode,一个mode里面又可以有多个source\observer\timer等等
(3)每次runloop启动的时候,只能指定一个mode,这个mode被称为该Runloop的当前mode
(4)如果需要切换mode,只能先退出当前Runloop,再重新指定一个mode进入
(5)这样做主要是为了分割不同组的定时器等,让他们相互之间不受影响
(6)系统默认注册了5个mode
模式一种五种。
Default模式
定义:NSDefaultRunLoopMode (Cocoa) kCFRunLoopDefaultMode (Core Foundation)
描述:默认模式中几乎包含了所有输入源(NSConnection除外),一般情况下应使用此模式,通常主线程是在这个Mode下运行。
Connection模式
定义:NSConnectionReplyMode(Cocoa)
描述:处理NSConnection对象相关事件,系统内部使用,用户基本不会使用。
Modal模式
定义:NSModalPanelRunLoopMode(Cocoa)
描述:处理modal panels事件。
Event tracking模式
定义:UITrackingRunLoopMode(iOS) NSEventTrackingRunLoopMode(cocoa)
描述:在拖动loop或其他user interface tracking loops时处于此种模式下,在此模式下会限制输入事件的处理。例如,当手指按住UITableView拖动时就会处于此模式,界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode影响 。
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];
RunLoop可以通过[acceptInputForMode:beforeDate:]和[runMode:beforeDate:]来指定在一段时间内的运行模式。如果不指定的话,RunLoop默认会运行在Default下(不断重复调用runMode:NSDefaultRunLoopMode beforDate:)
runloop模式的切换
对于非主线程,我们可以退出当前模式,然后再进入另一个模式,也可以直接进入另一个模式,即嵌套
对于主线程,我们当然也可以像上面一样操作,但是主线程有其特殊性,有很多系统的事件。系统会做一些切换,我们更关心的是系统是如何切换的?系统切换模式时,并没有使用嵌套
RunLoop的应用
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)]autorelease];
[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]autorelease];
[connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[connection start];