RunLoop-基础与应用
RunLoop的概念
一种可以控制线程存在时间,有事的时候干活,没事的时候休息的机制。
OSX/iOS 系统中,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。
- CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API。
- NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API。
注意:NSRunLoop和CFRunLoopRef可以混用
image_1.jpgRunLoop与线程的关系
- 一个线程对应一个RunLoop
- RunLoop在主线程默认开启,在子线程默认是关闭
RunLoop的获取
RunLoop不能显示的创建,只能在当前线程下通过[NSRunLoop currentRunLoop]
或CFRunLoopGetCurrent()
进行获取。如果当前线程的RunLoop存在就直接返回,不存在就自动创建。
运行和终止
NSRunLoop的开启
//从[NSDate date]到limitDate一直运行,run默认时间为[NSDate distantFuture]
- (void)run;
- (void)runUntilDate:(NSDate *)limitDate;
//从[NSDate date]到limitDate一直运行,不过只要期间收到响应就结束
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
直接调用发现RunLoop是没有运行的,解决方案如下
方式一:调用之前添加一些port或者timer
- (void)addTimer:(NSTimer *)timer forMode:(NSString *)mode;
- (void)addPort:(NSPort *)aPort forMode:(NSString *)mode;
方式二:使用循环不断添加,保证RunLoop一直运行着
while(!finished)
{
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
}
原因:一个RunLoop包含多个mode,每个mode包含一个source集合、一个timer数组、一个observer数组;RunLoop一次只能运行一个mode,当mode中的source、timer、observer没有数据时,会在运行完马上结束,port属于source,当然往mode中添加一些observer也是可以的。方式二虽然是可以使RunLoop一直运行,不过他一直在空转。
注意:关于NSRunLoop的终止只能制定时间结束才能终止
CFRunLoopRef的开启
因为NSRunLoop是对CFRunLoopRef的封装,因此他们自然都需要遵守同样的约束。
//开启RunLoop
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
//往CFRunLoopRef中的mode添加source,observer,timer
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode)
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef mode)
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode)
//结束当前runloop
CFRunLoopStop(CFRunLoopRef runloop)
关于Mode
主要列举我们在iOS中常用的几种mode
Default(默认模式,在没有指定mode下,就会使用这种模式)
- NSDefaultRunLoopMode(Cocoa)
- kCFRunLoopDefaultMode(Core Foundation)
Event tracking(追踪 ScrollView 滑动时的状态)
- NSEventTrackingRunLoopMode(Cocoa)
Common modes(集合暂时可以理解为Default和Event tracking这两种模式)
- NSRunLoopCommonModes(Cocoa)
- kCFRunLoopCommonModes (Core Foundation)
理解common mode和普通mode的区别需要从CFRunLoopMode和CFRunLoop分析
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};
当我们把一些source/observer/timer添加到commonMode时,其实是添加到了_commonModeItems
中,然后把当前全部的source/observer/timer同步到_commonModes
中的普通mode上。把一个普通的mode添加到_commonModes
上通过使用CFRunLoopAddCommonMode
方法。
注意:主线程的RunLoop中的_commonModes
默认下有UITrackingRunLoopMode和kCFRunLoopDefaultMode,在子线程中默认只有kCFRunLoopDefaultMode。
关于Source/Observer/Timer
Source
Source分两种Source0和Source1
- Source0 需要我们主动触发
- Source1 通过内核的mach_port来自动触发
Observer
主要是用来观察RunLoop的状态,RunLoop的状态有
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};
使用
- (NSThread *)thread
{
if(!_thread)
{
_thread = [[NSThread alloc] initWithTarget:self selector:@selector(onThread) object:nil];
_thread.name = @"customThread";
}
return _thread;
}
- (void)onThread
{
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"runloop进入");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"runloop要去处理timer");
break;
case kCFRunLoopBeforeSources:
NSLog(@"runloop要去处理Sources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"runloop要睡觉了");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"runloop醒来啦");
break;
case kCFRunLoopExit:
NSLog(@"runloop退出");
break;
default:
break;
}
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
CFRelease(observer);
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
Timer
- scheduledTimerWithTimeInterval: 方法默认把计时器添加到主线程的RunLoop中
- timerWithTimeInterval: 方法创建的Timer需要自己添加到RunLoop中才可以
RunLoop的应用
让Timer在tableView滚动时继续计时
方案一:把计时器同时添加NSDefaultRunLoopMode和UITrackingRunLoopMode
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
方案二:把计时器添加到NSRunLoopCommonModes模式上
方案三:开一个子线程进行计时,更新UI时切换至主线程
控制一些线程存在时间
线程永久存在
- (NSThread *)thread
{
if(!_thread)
{
_thread = [[NSThread alloc] initWithTarget:self selector:@selector(onThread) object:nil];
_thread.name = @"customThread";
}
return _thread;
}
- (void)onThread
{
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
控制线程在合适的时候退出
- (NSThread *)thread
{
if(!_thread)
{
_thread = [[NSThread alloc] initWithTarget:self selector:@selector(onThread) object:nil];
_thread.name = @"customThread";
}
return _thread;
}
- (void)onThread
{
while(!isFinished)
{
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
}
}
总结
一个线程对应一个RunLoop,一个RunLoop只能运行一个mode,一个mode里面包含source/observer/timer。mode主要描述的是线程的状态,比如tableView滚动时,主线程把当前RunLoop的切换到UITrackingRunLoopMode下,当滚动停止时又换回至NSDefaultRunLoopMode。当RunLoop在休眠状态时,可以被source/timer这些方式激活。激活的方式其实就是在RunLoop内部实现一个do-while循环不断进行轮询
是否满足激活的条件。