iOS 系统中,提供了两种RunLoop:NSRunLoop 和 CFRunLoopRef。
CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
Core Foundation框架下关于RunLoop的5个类:
- CFRunLoopRef:代表RunLoop的对象
- CFRunLoopModeRef:RunLoop的运行模式
- CFRunLoopSourceRef:就是RunLoop模型图中提到的输入源/事件源
- CFRunLoopTimerRef:就是RunLoop模型图中提到的定时源
- CFRunLoopObserverRef:观察者,能够监听RunLoop的状态改变
一个RunLoop对象(CFRunLoopRef)中包含若干个运行模式(CFRunLoopModeRef)。而每一个运行模式下又包含若干个输入源(CFRunLoopSourceRef)、定时源(CFRunLoopTimerRef)、观察者(CFRunLoopObserverRef)。每次RunLoop启动时,只能指定其中一个运行模式(CFRunLoopModeRef),这个运行模式(CFRunLoopModeRef)被称作CurrentMode。如果需要切换运行模式(CFRunLoopModeRef),只能退出Loop,再重新指定一个运行模式(CFRunLoopModeRef)进入。这样做主要是为了分隔开不同组的输入源(CFRunLoopSourceRef)、定时源(CFRunLoopTimerRef)、观察者(CFRunLoopObserverRef),让其互不影响 。
RunLoop运行逻辑图:
Source是什么?
source就是输入源事件,分为source0和source1这两种。
1、source0:诸如UIEvent(触摸,滑动等),performSelector这种需要手动触发的操作。
2、source1:处理系统内核的mach_msg事件(系统内部的端口事件)。诸如唤醒RunLoop或者让RunLoop进入休眠节省资源等。
一般来说日常开发中我们需要关注的是source0,source1只需要了解。
之所以说source0更重要是因为日常开发中,我们需要对常驻线程进行操作的事件大多都是source0。
Timer是什么?
Timer即为定时源事件。通俗来讲就是我们很熟悉的NSTimer,其实NSTimer定时器的触发正是基于RunLoop运行的,所以使用NSTimer之前必须注册到RunLoop,但是RunLoop为了节省资源并不会在非常准确的时间点调用定时器,如果一个任务执行时间较长,那么当错过一个时间点后只能等到下一个时间点执行,并不会延后执行(NSTimer提供了一个tolerance属性用于设置宽容度,如果确实想要使用NSTimer并且希望尽可能的准确,则可以设置此属性)。
Observer是什么?
它相当于消息循环中的一个监听器,随时通知外部当前RunLoop的运行状态。NSRunLoop没有相关方法,只能通过CFRunLoop相关方法创建
总结:
上面的 Source/Timer/Observer 被统称为 mode item,一个item可以被同时加入多个mode。但一个item被重复加入同一个mode时是不会有效果的。如果一个mode中一个item 都没有(只有Observer也不行),则 RunLoop 会直接退出,不进入循环。RunLoop能正常运行的条件就是,至少要包含一个Mode(RunLoop默认就包含DefaultMode),并且该Mode下需要有至少一个的事件源(Timer/Source)。事实上经过NSRunLoop封装后,只可以往mode中添加两类事件源:NSPort(对应的是source1)和NSTimer。
①.RunLoop是寄生于线程的消息循环机制,它能保证线程存活,而不是线性执行完任务就消亡。
②.RunLoop与线程是一一对应的,每个线程只有唯一与之对应的一个RunLoop。我们不能创建RunLoop,只能在当前线程当中获取线程对应的RunLoop(主线程RunLoop除外)。
③.子线程默认没有开启RunLoop,需要我们去主动开启,但是主线程是自动开启了RunLoop的。
④.RunLoop想要正常启用需要运行在添加了事件源的Mode下。
⑤.RunLoop有三种启动方式run、runUntilDate:(NSDate *)limitDate、runMode:(NSString *)mode beforeDate:(NSDate *)limitDate。第一种无条件永远运行RunLoop并且无法停止,线程永远存在。第二种会在时间到后退出RunLoop,同样无法主动停止RunLoop。前两种都是在NSDefaultRunLoopMode模式下运行。第三种可以选定运行模式,并且在时间到后或者触发了非Timer的事件后退出。
这几个方法都是向线程中的RunLoop发送了消息,然后RunLoop接收到了消息就唤醒线程,去做对应的事情。所以想要正常使用这几个方法,响应selector的线程必须开启了RunLoop。
- (void)kzRunLoop{ /****************** CFRunLoopRef **********************/ /* Core Foundation框架下: CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象 CFRunLoopGetMain(); // 获得主线程的RunLoop对象 Foundation框架下: [NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象 [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象 */ /****************** CFRunLoopModeRef **********************/ /* kCFRunLoopDefaultMode:App的默认运行模式,通常主线程是在这个运行模式下运行 UITrackingRunLoopMode:跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响) UIInitializationRunLoopMode:在刚启动App时第进入的第一个 Mode,启动完成后就不再使用 GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到 kCFRunLoopCommonModes:伪模式,不是一种真正的运行模式
kCFRunLoopCommonModes并不是某种具体的Mode,而是一种模式组合,在主线程中默认包含了NSDefaultRunLoopMode和 UITrackingRunLoopMode。
子线程中只包含NSDefaultRunLoopMode。
注意:
①在选择RunLoop的runMode时不可以填这种模式否则会导致RunLoop运行不成功。
②在添加事件源的时候填写这个模式就相当于向组合中所有包含的Mode中注册了这个事件源。
③你也可以通过调用CFRunLoopAddCommonMode()方法将自定义Mode放到 kCFRunLoopCommonModes组合
*/ /********************* 案例 ***********************/ /* 假如在一个界面中添加一个Text View,并且在控制器中添加一个定时器每隔2s执行打印方法,当我们拖动Text View滚动时会出现什么情况呢? */ NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; //NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES]; /* 此时会发现当拖动Text View滚动时定时器将不会执行打印操作,当我们松手时,定时器又恢复工作了 原因: 当我们不做任何操作的时候,RunLoop处于NSDefaultRunLoopMode下。 而当我们拖动Text View的时候,RunLoop就结束NSDefaultRunLoopMode,切换到了UITrackingRunLoopMode模式下,这个模式下没有添加NSTimer,所以我们的NSTimer就不工作了。 但当我们松开鼠标的时候,RunLoop就结束UITrackingRunLoopMode模式,又切换回NSDefaultRunLoopMode模式,所以NSTimer就又开始正常工作了。 此时,无论我们将NSTimer切换到何种模式,另一种模式总是不能完美的运行,这就用到了我们之前说过的伪模式(kCFRunLoopCommonModes),这其实不是一种真实的模式,而是一种标记模式,意思就是可以在打上Common Modes标记的模式下运行。 具体做法就是讲添加语句改为: [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; */ /*************** CFRunLoopObserverRef *****************/ /* CFRunLoopObserverRef是观察者,用来监听RunLoop的状态改变, typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), // 即将进入Loop:1 kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer:2 kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source:4 kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠:32 kCFRunLoopAfterWaiting = (1UL << 6), // 即将从休眠中唤醒:64 kCFRunLoopExit = (1UL << 7), // 即将从Loop中退出:128 kCFRunLoopAllActivities = 0x0FFFFFFFU // 监听全部状态改变 }; */ //创建观察者 CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { NSLog(@"监听到RunLoop发生改变---%zd", activity); }); //添加观察者到当前RunLoop中 CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode); //释放observer,最后添加完需要释放掉 CFRelease(observer); /******************** 后台常驻线程 *********************/ //在开发应用程序的过程中,如果后台操作特别频繁,经常会在子线程做一些耗时操作(下载文件、后台播放音乐等),我们最好能让这条线程永远常驻内存。那么怎么做呢?添加一条用于常驻内存的强引用的子线程,在该线程的RunLoop下添加一个Sources,开启RunLoop。 self.testThread = [[NSThread alloc]initWithTarget:self selector:@selector(run1) object:nil]; [self.testThread start]; } - (void)run{ NSLog(@"啦啦啦啦"); } - (void)run1{ //任务 NSLog(@"---run1---执行啦"); //添加下边两句代码,就可以开启RunLoop,之后self.thread就变成了常驻线程, //可随时添加任务,并交于RunLoop处理 [[NSRunLoop currentRunLoop]addPort:[NSPort port] forMode:NSDefaultRunLoopMode]; [[NSRunLoop currentRunLoop]run]; NSLog(@"测试是否开启了RunLoop,如果开启RunLoop,则来不了这里,因为RunLoop开启了循环"); } - (void)run2{ NSLog(@"---run2---运行啦"); } //btnAction中调用PerformSelector,从而实现在点击按钮的时候调用run2方法。 - (IBAction)btnAction:(id)sender { /* waitUntilDone: 当为yes的时候,先让主线程运行setEnd中的一些操作,之后再进行当前线程中的操作。 当为no的时候,先进行当前线程中的操作,之后让主线程运行setEnd中的一些操作。 */ [self performSelector:@selector(run2) onThread:self.testThread withObject:nil waitUntilDone:NO]; }
使用场景
1.保持线程的存活,而不是线性的执行完任务就退出了
<1>不开启RunLoop的线程
在遇到一些耗时操作时,为了避免主线程阻塞导致界面卡顿,影响用户体验,往往我们会把这些耗时操作放在一个临时开辟的子线程中。操作完成了,子线程线性的执行了代码也就退出了,就像下面一样。
#import "MyThread.h" @implementation MyThread -(void)dealloc{ NSLog(@"%@线程被释放了", self.name); } @end
{ NSLog(@"%@----开辟子线程",[NSThread currentThread]); self.subThread = [[MyThread alloc] initWithTarget:self selector:@selector(subThreadTodo) object:nil]; self.subThread.name = @"subThread"; [self.subThread start]; }
- (void)subThreadTodo { NSLog(@"%@----执行子线程任务",[NSThread currentThread]); }
就像一开始所说的一样,子线程执行完操作就自动退出了。
<2>开启RunLoop的线程
(1)实验用self来持有子线程
如果子线程的操作是偶尔或者干脆只需要执行一次的话,像上面那样就没什么问题。但是如果这个操作需要频繁执行,那么按照上面那样的逻辑,我们就需要频繁创建子线程,这是很消耗资源的。就像平时我们在设计类的时候会把需要频繁使用的对象保持起来,而不是频繁创建一样。我们试试把线程“保持”起来,让它在需要的时候执行任务,不需要的时候就啥都不干。
我们将刚才的代码稍作改动:
{ NSLog(@"%@----开辟子线程",[NSThread currentThread]); self.subThread = [[MyThread alloc] initWithTarget:self selector:@selector(subThreadTodo) object:nil]; self.subThread.name = @"subThread"; [self.subThread start]; }
子线程内部操作完成后并没有被释放,看样子我们成功持有了子线程。那么按照刚才的设想,我们就可以在任何需要的时候开启子线程完成线程里面的操作。
我们在[self.subThread start];后面再添加上一句[self.subThread start];再运行试试看结果。
我们发现它崩溃了。原因如下:
因为执行完任务后,虽然Thread没有被释放,还处于内存中,但是它处于死亡状态(当线程的任务结束后就会进入这种状态)。打个比方,人死不能复生,线程死了也不能复生(重新开启),苹果不允许在线程死亡后再次开启。所以会报错attempt to start the thread again(尝试重新开启线程)。
下面使用RunLoop做个小测试:
{ NSLog(@"%@----开辟子线程",[NSThread currentThread]); NSThread *subThread = [[MyThread alloc] initWithTarget:self selector:@selector(subThreadTodo) object:nil]; subThread.name = @"subThread"; [subThread start]; } } - (void)subThreadTodo { NSLog(@"%@----开始执行子线程任务",[NSThread currentThread]); //获取当前子线程的RunLoop NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
//下面这一行必须加,否则RunLoop无法正常启用。给RunLoop添加一个事件源,注意添加的Mode。
//关于这里的[NSMachPort port]我的理解是,给RunLoop添加了一个占位事件源,告诉RunLoop有事可做,
//让RunLoop运行起来。但是暂时这个事件源不会有具体的动作,而是要等RunLoop跑起来过后等有消息传递了才会有具体动作
[runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes]; //让RunLoop跑起来 [runLoop run]; NSLog(@"%@----执行子线程任务结束",[NSThread currentThread]); }
这里没有对线程进行引用,也没有让线程内部的任务进行显式的循环。为什么子线程的里面的任务没有执行到输出任务结束这一步,为什么子线程没有销毁?就是因为[runLoop run];这一行的存在。
RunLoop本质就是个Event Loop的do while循环,所以运行到这一行以后子线程就一直在进行接受消息->等待->处理的循环。之后的代码(这点需要注意,在使用RunLoop的时候如果要进行一些数据处理之类的要放在这个函数之前否则写的代码不会被执行),也就不会因为任务结束导致线程死亡进而销毁。这也就是我们最常使用RunLoop的场景之一,就如小节标题保持线程的存活,而不是线性的执行完任务就退出了。
为什么总是要把RunLoop和线程放在一起来讲?
总的来讲就是:RunLoop是保证线程不会退出,并且能在不处理消息的时候让线程休眠,节约资源,在接收到消息的时候唤醒线程做出对应处理的消息循环机制。它是寄生于线程的,所以提到RunLoop必然会涉及到线程。
线程默认不开启RunLoop,为什么我们的App或者说主线程却可以一直运行而不会结束?
主线程是唯一一个例外,当App启动以后主线程会自动开启一个RunLoop来保证主线程的存活并处理各种事件。而且从上面的源代码来看,任意一个子线程的RunLoop都会保证主线程的RunLoop的存在。
RunLoop能正常运行的条件是什么?
RunLoop正常运行的条件是:1.有Mode。2.Mode有事件源。3.运行在有事件源的Mode下。
注意:[runLoop addPort:[NSMachPort port] forMode:UITrackingRunLoopMode];使用这句话时,任务执行完,线程仍然会被释放。因为是在NSDefaultRunLoopMode下运行RunLoop。而我们添加的事件源是在另外一个Mode下,NSDefaultRunLoopMode仍然空空如也,所以RunLoop也就直接退出了。
除了[runLoop run]还有那些方法启动RunLoop?
NSRunLoop中总共包装了3个方法供我们使用
1.- (void)run;
除非希望子线程永远存在,否则不建议使用,因为这个接口会导致Run Loop永久性的运行NSDefaultRunLoopMode模式,即使使用 CFRunLoopStop(runloopRef);也无法停止RunLoop的运行,那么这个子线程也就无法停止,只能永久运行下去。
2.- (void)runUntilDate:(NSDate *)limitDate;
比上面的接口好点,有个超时时间,可以控制每次RunLoop的运行时间,也是运行在NSDefaultRunLoopMode模式。这个方法运行RunLoop一段时间会退出给你检查运行条件的机会,如果需要可以再次运行RunLoop。注意CFRunLoopStop(runloopRef);仍然无法停止RunLoop的运行,因此最好自己设置一个合理的RunLoop运行时间。比如
while (!Stop){
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];
}
3.- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
有一个超时时间限制,而且可以设置运行模式
这个接口在非Timer事件触发、显式的用CFRunLoopStop停止RunLoop或者到达limitDate后会退出返回。如果仅是Timer事件触发并不会让RunLoop退出返回,但是如果是PerfromSelector事件或者其他Input Source事件触发处理后,RunLoop会退出返回YES。同样可以像上面那样用while包起来使用。