1.RunLoop简介
当有持续的异步任务需求时,我们会创建一个独立的生命周期可控的线程。RunLoop就是控制线程生命周期并接收事件进行处理的机制。
RunLoop是iOS事件响应与任务处理最核心的机制,它贯穿iOS整个系统。
通过RunLoop机制实现省电,流畅,响应速度快,用户体验好
1.使程序一直运行并接受用户输入
2.决定程序在何时应该处理哪些Event
3.调用解耦
4.节省CPU时间(有工作时工作,没工作时闲着)
2.特性
a.主线程的RunLoop在应用启动的时候就会自动创建
主线程的run loop默认是启动的。
iOS的应用程序里面,程序启动后会有一个如下的main() 函数:
int main(int argc, char *argv[])
{
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([appDelegate class]));
}
}
b.其他线程则需要在该线程下自己启动,即需要手动去运行它
c.在任何一个Cocoa程序的线程中,都可以通过:
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
来获取到当前线程的run loop。
d.RunLoop并不是线程安全的,所以需要避免在其他线程上调用当前线程的RunLoop(我们不能再一个线程中去操作另外一个线程的run loop对象,那很可能会造成意想不到的后果。不过幸运的是CoreFundation中的不透明类CFRunLoopRef是线程安全的,而且两种类型的run loop完全可以混合使用。Cocoa中的NSRunLoop类可以通过实例方法:
- (CFRunLoopRef)getCFRunLoop;
获取对应的CFRunLoopRef类,来达到线程安全的目的)
e.RunLoop负责管理autorelease pools
f.RunLoop负责处理消息事件,即输入源事件和计时器事件
g.不能自己创建RunLoop
与CFRunloop相关的有GCD,mach kernel是苹果内核的东西,还有block,pthread等
与咱们平时敲代码比较近一层有以下这些
NSTimer 计时器完全依赖于runloop
UIEvent 事件的产生到分发给代码都是通过runloop
Autorelease 自动释放也是在runloop跑完一圈后
NSObject(NSDelayedPerforming) performSelector,cancel
NSObject(NSThreadPerformAddition) performSelectorOnMainThread,performSelectorOnBackgroundThread
CA层的CADisplayLink(每画一帧会有一个回调),CATransition,CAAnimation
dispatch_get_main_queue()
NSURLConnection
AFNetworking,它的delegate跟网络传输数据都是在它的runloop里面执行的
NSPort 描述通讯信道的抽象类
等等..
4.在runloop中定义了以下6种函数
static void CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION();
static void CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK();
static void CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE();
static void CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION();
static void CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION();
static void CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION();
5.RunLoop机制
RunLoop跟Thread是一一绑定的(也就是之前说的一个Thread里只有一个根runloop但是可以嵌套N个)
CFRunLoopMode:RunLoop必须在系统定义的几种模式下运行
下边几种是在RunLoopMode里面的
比较抽象,继续往下走
a.理解Perform Selector
我们先在主线程中使用下performselector:
-(void)tryPerformSelectorOnMianThread{
[self
performSelector:@selector(mainThreadMethod)
withObject:nil];
}
-(void)mainThreadMethod{
NSLog(@"execute%s",__func__);
// print: execute -[ViewController mainThreadMethod]
}
这样我们在ViewDidLoad中调用tryPerformSelectorOnMianThread,就会立即执行,并且输出:print: execute -[ViewController mainThreadMethod];
和上面的例子一样,我们使用GCD,让这个方法在后台线程中执行
-(void)tryPerformSelectorOnBackGroundThread{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
0),
^{
[self
performSelector:@selector(backGroundThread)
onThread:[NSThread currentThread] withObject:nil waitUntilDone:NO];
});
}
-(void)backGroundThread{
NSLog(@"%u",[NSThread isMainThread]);
NSLog(@"execute %s",__FUNCTION__);
}
同样的,我们调用tryPerformSelectorOnBackGroundThread这个方法,我们会发现,下面的backGroundThread不会被调用,这是什么原因呢?
这是因为,在调用performSelector:onThread: withObject: waitUntilDone的时候,系统会给我们创建一个Timer的source,加到对应的RunLoop上去,然而这个时候我们没有RunLoop,如果我们加上RunLoop:
-(void)tryPerformSelectorOnBackGroundThread{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
0),
^{
[self
performSelector:@selector(backGroundThread) onThread:[NSThread currentThread] withObject:nil waitUntilDone:NO];
NSRunLoop*runLoop = [NSRunLoop currentRunLoop];
[runLoop run];
});
}
这时就会发现我们的方法正常被调用了。那么为什么主线程中的perfom selector却能够正常调用呢?通过上面的例子相信你已经猜到了,主线程的RunLoop是一直存在的,所以我们在主线程中执行的时候,无需再添加RunLoop。
小结:当perform selector在后台线程中执行的时候,这个线程必须有一个开启的runLoop
b.理解NSTime
我们平时使用NSTimer,一般是在主线程中的,代码大多如下:
-(void)tryTimerOnMainThread{
NSTimer*myTimer = [NSTimer scheduledTimerWithTimeInterval:0.5
target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
[myTimer fire];
}
-(void)timerAction{
NSLog(@"timer
action");
}
这个时候代码按照我们预定的结果运行,如果我们把这个Tiemr放到后台线程中呢?
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
0),
^{
NSTimer*myTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
[myTimer fire];
});
这个时候我们会发现,这个timer只执行了一次,就停止了。这是为什么呢?通过上面的讲解,想必你已经知道了,NSTimer,只有注册到RunLoop之后才会生效,这个注册是由系统自动给我们完成的,既然需要注册到RunLoop,那么我们就需要有一个RunLoop,我们在后台线程中加入如下的代码:
NSRunLoop*runLoop = [NSRunLoop currentRunLoop];
[runLoop run];
这样我们就会发现程序正常运行了。在Timer注册到RunLoop之后,RunLoop会为其重复的时间点注册好事件,比如1:10,1:20,1:30这几个时间点。有时候我们会在这个线程中执行一个耗时操作,这个时候RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer,这就造成了误差(Timer有个冗余度属性叫做tolerance,它标明了当前点到后,容许有多少最大误差),可以在执行一段循环之后调用一个耗时操作,很容易看到timer会有很大的误差,这说明在线程很闲的时候使用NSTiemr是比较傲你准确的,当线程很忙碌时候会有较大的误差。系统还有一个CADisplayLink,也可以实现定时效果,它是一个和屏幕的刷新率一样的定时器。如果在两次屏幕刷新之间执行一个耗时的任务,那其中就会有一个帧被跳过去,造成界面卡顿。另外GCD也可以实现定时器的效果,由于其和RunLoop没有关联,所以有时候使用它会更加的准确,这在最后会给予说明。
②CFRunLoopSource
source是RunLoop的数据源(输入源)的抽象类(protocol)
RunLoop定义了两个version的Source:
i.source0:处理App内部事件,App自己负责管理(出发),如UIEvent、CFSocket
ii.source1:由RunLoop和内核管理,Mach Port(进程间通讯端口)驱动,如CFMachPort、CFMessagePort
CFRunLoopObserver:告知外界当前状态
kCFRunLoopEntry = (1UL << 0),// 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1),// 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2),// 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5),// 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6),// 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7),// 即将退出Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU//所有状态
RunLoopObserver与Autorelease Pool
大家面试的时候可以问问面试者这个问题,autorelease的对象到底在什么时候释放?
据测试,AutoreleasePool通常在RunLoop两次Sleep之间释放
③Run Loop Modes
RunLoop在同一时间只能且必须在一种特定的Mode下Run
更换Mode时,需要停止当前RunLoop,然后重启新的RunLoop
Mode是iOS App流畅滑动的关键(因为在滑动时的Mode跟平时运行的Mode是不一样,从而避免干扰)
也可以基于系统的Mode创建自己的Mode(也是基本不会发生的)
Cocoa定义了四中Mode
①Default:NSDefaultRunLoopMode,默认模式,也是空闲状态,在Run Loop没有指定Mode的时候,默认就跑在Default Mode下,主线程通常在这个 Mode 下运行的。
②Connection:NSConnectionReplyMode,用来监听处理网络请求NSConnection的事件
③Modal:NSModalPanelRunLoopMode,OS X的Modal面板事件
④Event tracking:UITrackingRunLoopMode,拖动事件(ScrollView滚动时候的模式)(主线程中该模式优先级最高)
⑤Common mode:NSRunLoopCommonModes,是一个模式集合,当绑定一个事件源到这个模式集合的时候就相当于绑定到了集合内的每一个模式。是一个数组,默认包括了第1和第2种模式,可以添加自己的Mode。
RunLoop可以通过[acceptInputForMode:beforeDate:]和[runMode:beforeDate:]来指定在一段时间内的运行模式。如果不指定的话,RunLoop默认会运行在Default下(不断重复调用runMode:NSDefaultRunLoopMode beforDate:)
在主线程启动一个计时器Timer,然后拖动UITableView或者UIScrollView,计时器不执行。这是因为,为了更好的用户体验,在主线程中Event tracking模式的优先级最高。在用户拖动控件时,主线程的Run Loop会将切换成NSEventTrackingRunLoopMode模式,在这个过程中,默认的NSDefaultRunLoopMode模式中注册的事件是不会被执行的,而创建的Timer是默认关联为Default Mode,因此系统不会立即执行Default Mode下接收的事件。解决方法:
NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerFireMethod:) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; //或 [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
[timer fire];
//NSRunLoopCommonModes 能够在多线程中起作用,这个模式等效于NSDefaultRunLoopMode和NSEventTrackingRunLoopMode的结合,这也是将modes换为NSRunLoopCommonModes便可解决的原因。
.Run Loop应用实践
Run Loop主要有以下三个应用场景:
①维护线程的生命周期,让线程不自动退出,isFinished为Yes时退出。
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
while (!self.isCancelled && !self.isFinished) { @autoreleasepool {
[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
}
}
②创建常驻线程,执行一些会一直存在的任务。该线程的生命周期跟App相同
@autoreleasepool {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
③在一定时间内监听某种事件,或执行某种任务的线程
如下代码,在30分钟内,每隔30s执行onTimerFired:。这种场景一般会出现在,如我需要在应用启动之后,在一定时间内持续更新某项数据。
@autoreleasepool {
NSRunLoop * runLoop = [NSRunLoop currentRunLoop]; NSTimer * udpateTimer = [NSTimer timerWithTimeInterval:30 target:self selector:@selector(onTimerFired:) userInfo:nil repeats:YES];//30秒执行一次
[runLoop addTimer:udpateTimer forMode:NSRunLoopCommonModes];
[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:60*30]];//30分钟内
}
另外实例:
①一个TableView延迟加载图片的新思维
这个问题是有的TableView有大量图片(比如头像)加载,在滑动的时候,请求网络,下载完图片之后设置的时候会卡,往常的解决方案一般是添加delegate之类的,检测什么时候滑动结束什么时候去设置图片
在知道RunLoop之后,可以采用下面的方案,在DefaultMode去做,这样滑动的时候就不会调用设置图片方法
UIImage *downLoadImage = ...;
[self.avatarImageView performSelector:@selector(setImage:)
withObject:downloadImage
afterDelay:0
inModes:@[NSDefaultRunLoopMode]];
②让Crash的App回光返照
App崩溃的发生分两种情况:
program received signal:SIGABRT SIGABRT 一般是过度release 或者 发送 unrecogized selector导致。
EXC_BAD_ACCESS 是访问已被释放的内存导致,野指针错误。
由 SIGABRT 引起的Crash 是系统发这个signal给App,程序收到这个signal后,就会把主线程的RunLoop杀死,程序就Crash了 该例只针对 SIGABRT引起的Crash有效
CFRunLoopRef runloop = CFRunLoopGetCurrent();
//获取所有Mode,因为可能有很多Mode,每个Mode都需要跑,此处可以选择提交下崩溃信息之类的
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"程序崩溃了" message:@"崩溃信息" delegate:nil cancelButtonTitle:@"取消" otherButtonTitles:nil];
[alertView show];
NSArray *allModes = CFBridgingRelease(CFRunLoopCopyAllModes(runloop));
while (1) {
//快速切换Mode
for (NSString *mode in allModes) {
CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
}
}
接到Crash的Signal后手动重启RunLoop
@autoreleasepool {
NSRunLoop * runLoop = [NSRunLoop currentRunLoop];
[runLoop addTimer:udpateTimer forMode:NSRunLoopCommonModes];
[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:60*30]];
}
6.那么一般在什么情况下用到呢
a.需要使用Port或者自定义Input Source与其他线程进行通讯。
b.需要在线程中使用Timer。
c.需要在线程上使用performSelector*****方法。
d.需要让线程执行周期性的工作。
举个例子 定义一个NSTimer来隔一会调用某个方法 但这时你在拖动textVIew不放手 主线程就被占用了。 timer的监听方法就不调用 直到你松手 ,,这时吧nstimer加到 runloop里 就相当于告诉主循环 腾出点时间来给timer ,再拖动textView就不会因主线程被占用而不调用了。
你看看AFNetWorking和SDWebImage的源码,都是子类化NSOperation,在子线程中发起NSURLConnetion,connetion的代理就是这个operation,然后CFRunloopRun(),开启Runloop,Fail或者Finish的时候CFRunloopStop(CFRunloopGetCurrent())关闭Runloop。如果不写CFRunloopRun(),根本不会执行NSURLConnection的代理方法的,因为该线程没开启Runloop,马上就完了。RunLoop相当于子线程的循环,可以灵活控制子线程的生命周期。