NSRunLoop
1.什么是RunLoop
- 运行循环
- 一个线程对应一个RunLoop,主线程的RunLoop默认已经启动,子线程的RunLoop得手动启动(调用run方法)
- RunLoop只能选择一个Mode启动,如果当前Mode中没有任何Source(Sources0、Sources1)、Timer,那么就直接退出RunLoop
- 线程退出,则RunLoop也退出;强制退出RunLoop,RunLoop也会退出
-
RunLoop作用
- 保持程序的持续运行
- 处理App中的各种事件(比如触摸事件、定时器事件、Selector事件)
- 节省CPU资源,提高程序性能:该做事时做事,该休息时休息 ......
-
模拟RunLoop内部实现
- 其实它内部就是do-while循环,在这个循环内部不断地处理各种任务(比如Source、Timer、Observer)
获得RunLoop对象
- RunLoop对象
- NSRunLoop
- CFRunLoopRef
iOS中有两套API可以创建获取RunLoop对象。分别是Foundation框架的NSRunLoop和C语言的CFRunLoopRef
NSRunLoop和CFRunLoopRef都代表着RunLoop对象
NSRunLoop是基于CFRunLoopRef的一层OC包装,所以要了解RunLoop内部结构,需要多研究CFRunLoopRef层面的API(Core Foundation层面)
模拟RunLoop内部实现
void message(int num) { printf("执行第%i个任务", num); } int main(int argc, const char * argv[]) { do { printf("有事做吗? 没事我就休息了"); int number; scanf("%i", &number); message(number); } while (1); return 0; }
RunLoop与线程的关系
一个线程对应一个RunLoop,主线程的RunLoop默认程序启动就已经创建好了。
子线程默认没有RunLoop,不过子线程可以有RunLoop,子线程的RunLoop得手动创建并且手动启动(调用run方法)
RunLoop在第一次获取时创建,在线程结束时销毁
可以理解为,子线程的RunLoop是懒加载的(主线程除外)。只有用到的时候才会创建( 调用currentRunLoop方法)。
如果是在子线程中调用currentRunLoop,那么系统会先查看当前子线程是否有与之对应的NSRunLoop,如果没有就创建一个RunLoop对象
注意:如果想给子线程添加一个与之对应的RunLoop,不能通过alloc、init方法,只能通过currentRunLoop,如果用alloc、init创建出来的RunLoop不能添加到子线程。
如何获取RunLoop对象
1.通过Foundation框架获取
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
2.通过Core Foundation框架获取
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
RunLoop底层实现
// should only be called by Foundation // t==0 is a synonym for "main thread" that always works CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) { if (pthread_equal(t, kNilPthreadT)) { t = pthread_main_thread_np(); } __CFLock(&loopsLock); if (!__CFRunLoops) { __CFUnlock(&loopsLock); // 创建字典 CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks); // 创建主线程 CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np()); // 保存主线程 CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop); if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) { CFRelease(dict); } CFRelease(mainLoop); __CFLock(&loopsLock); } // 从字典中获取子线程对应的loop CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); __CFUnlock(&loopsLock); if (!loop) { // 如果不存在子线程对应的loop就创建一个 CFRunLoopRef newLoop = __CFRunLoopCreate(t); __CFLock(&loopsLock); loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); // 把新创建的loop保存在字典中,线程作为key,loop作为value if (!loop) { CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop); loop = newLoop; }
以上是从CF-1151.16的CFRunLoop.c文件中拷贝的RunLoop的源码:
当我们通过[NSRunLoop currentRunLoop]调用NSRunLoop的currentRunLoop方法的时候,底层就会调用NSRunLoopRef的get方法。
1.程序启动,底层会先创建一个字典。
2.然后马上会创建一个主线程的RunLoop,并把主线程作为key,把主线程的RunLoop作为value添加到字典中。
注意:这也就是为什么一个线程对应一个RunLoop的原因,因为RunLoop是通过key-value的形式和线程以一一对应的方式保存在字典中的。
3.如果从子线程通过[NSRunLoop currentRunLoop]调用NSRunLoop的currentRunLoop方法的时候,系统会以子线程作为key,去字典中取对应的RunLoop对象。
4.如果取出来的RunLoop对象为空,则系统会创建一个RunLoop对象并以子线程作为key把该RunLoop对象存储到字典中去。
RunLoop相关类
Core Foundation中关于RunLoop的5个类:
CFRunLoopRef :RunLoop对象
CFRunLoopModeRef :RunLoop的模式,可以把RunLoop理解为空调,对应着许多模式,但是一个RunLoop同时只能执行一种模式
CFRunLoopSourceRef : 事件来源,用来处理RunLoop的事件
CFRunLoopTimerRef :定时器,处理和定时器相关的事情
CFRunLoopObserverRef :通过observer监听事件
RunLoop的结构
一个RunLoop有多个模式:每个模式都有各自的source、timer和observer。
注意:一个RunLoop有多个模式,但是在同一时刻只能执行一种模式。
CFRunLoopModeRef:
CFRunLoopModeRef代表RunLoop的运行模式
一个 RunLoop 包含若干个 Mode,每个Mode又包含若干个Source/Timer/Observer
每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode
如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入
这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响
系统默认注册了5个Mode:
NSDefaultRunLoopMode:App的默认Mode,通常主线程是在这个Mode下运行。程序启动,如果用户什么都没做,默认就在这个模式
UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。程序启动,如果用户滑动了scrollView,就会从默认模式切换到这个模式
UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
NSRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode
PS:前四种模式是真正的模式,最后一种模式不是真正的模式。主要学习前两种模式和最后一种模式。
runLoop默认是个死循环,源码如下:
// 用DefaultMode启动 void CFRunLoopRun(void) { /* DOES CALLOUT */ int32_t result; do { result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); CHECK_FOR_FORK(); } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result); }
CFRunLoopSourceRef:
CFRunLoopSourceRef是事件源(输入源)
按照官方文档,Source的分类
Port-Based Sources
Custom Input Sources
Cocoa Perform Selector Sources
按照函数调用栈,Source的分类
Source0:非基于Port的, 处理app内部事件,APP自己负责触发,如UIEvent、CFSocket都属于Source0
Source1:基于Port的,通过内核和其他线程相互发送消息,由runLoop和内核管理,Mach port驱动,如CFMachPort、CFMessagePort
CFRunLoopTimerRef:
CFRunLoopTimerRef是基于时间的触发器
CFRunLoopTimerRef基本上说的就是NSTimer,它受RunLoop的Mode影响
GCD的定时器不受RunLoop的Mode影响
创建出来NSTimer对象,我们需要把NSTimer对象添加到runLoop中
// 创建一个NSTimer之后, 必须将NSTimer添加到RunLoop中, 才能执行 NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(demo) userInfo:nil repeats:YES]; // 添加到runLoop中(下面这就话就是把timer添加到当前线程的默认模式下) [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
runLoop同一时间只能执行一个模式,所以如果把timer添加到默认模式,那么timer只在默认模式下生效。例如,切换到追踪模式,默认模式下的timer是无效的。
那么怎么让timer在默认模式和追踪模式下都有效呢?
/*
common modes =
{
0 : <CFString 0x105b56e50 [0x104e83180]>{contents = "UITrackingRunLoopMode"}
2 : <CFString 0x104e5f080 [0x104e83180]>{contents = "kCFRunLoopDefaultMode"}
}
*/
// 这是一个占位用的Mode,不是一种真正的Mode // 其实Common是一个标识, 它是将NSDefaultRunLoopMode和UITrackingRunLoopMode标记为了Common // 所以, 只要将timer添加到Common占位模式下,timer就可以在NSDefaultRunLoopMode和UITrackingRunLoopMode模式下都能运行
// 相当于timer添加到了这两个模式中,在这两个模式中都有效
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
GCD的timer不受NSRunLoop定时器的影响
// 1.创建tiemr // queue: 代表定时器将来回调的方法在哪个线程中执行 // dispatch_queue_t queue = dispatch_get_main_queue(); dispatch_queue_t queue = dispatch_get_global_queue(0, 0); dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); self.timer = timer; // 2.设置timer /* 第一个参数: 需要设置哪个timer 第二个参数: 指定定时器开始的时间 第三个参数: 指定间隔时间 第四个参数: 定时器的精准度, 如果传0代表要求非常精准(系统会让定时器执行的时间变得更加准确) 如果传入一个大于0的值, 就代表我们允许的误差 // 例如传入60, 就代表允许误差有60秒 */ // 定时器开始时间 // dispatch_time_t startTime = DISPATCH_TIME_NOW; // 调用这个函数,就可以指定两秒之后开始/而不是立即开始 dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)); // 定时器间隔的时间 uint64_t timerInterval = 2.0 * NSEC_PER_SEC; dispatch_source_set_timer(timer, startTime, timerInterval, 0 * NSEC_PER_SEC); // 3.设置timer的回调 dispatch_source_set_event_handler(timer, ^{ NSLog(@"我被调用了 %@", [NSThread currentThread]); }); // 4.开始执行定时器 dispatch_resume(timer); }
CFRunLoopObserverRef:
CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变
可以监听的时间点有以下几个:
自定义Observer来监听指定线程的状态的改变:
// 0.创建一个监听对象 /* 第一个参数: 告诉系统如何给Observer对象分配存储空间 第二个参数: 需要监听的类型 第三个参数: 是否需要重复监听 第四个参数: 优先级 第五个参数: 监听到对应的状态之后的回调 */ CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { // NSLog(@"%lu", activity); switch (activity) { case kCFRunLoopEntry: NSLog(@"进入RunLoop"); break; case kCFRunLoopBeforeTimers: NSLog(@"即将处理timer"); break; case kCFRunLoopBeforeSources: NSLog(@"即将处理source"); break; case kCFRunLoopBeforeWaiting: NSLog(@"即将进入睡眠"); break; case kCFRunLoopAfterWaiting: NSLog(@"刚刚从睡眠中醒来"); break; case kCFRunLoopExit: NSLog(@"退出RunLoop"); break; default: break; } }); // 1.给主线程的RunLoop添加监听 /* 第一个参数:需要监听的RunLoop对象 第二个参数:给指定的RunLoop对象添加的监听对象 第三个参数:在那种模式下监听 */ CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes); // 如果通过scheduled方法创建NSTimer, 系统会默认添加到当前线程的默认模式下 NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(demo) userInfo:nil repeats:YES];
总结:
一条线程对应一条RunLoop,程序一启动,主线程的RunLoop就已经创建并且和主线程绑定好。通过查看RunLoop源代码,系统内部是通过字典的形式把线程和RunLoop进行了绑定。
子线程的RunLoop默认是没有的,如果想使用子线程的RunLoop,只需要在子线程调用NSRunLoop的currentRunLoop方法即可。
我们可以把RunLoop理解为懒加载的,只有在用到的时候才会创建。ru如果子线程中调用了currentRunLoop方法,那么系统会先根据子线程去字典中取对应的RunLoop,如果没有,则系统会创建一个RunLoop并且和该子线程进行绑定并且保存到字典中。
每个RunLoop中又有很多的mode,每个mode中又可以有很多的source、timer和observer。需要注意的是,RunLoop在同一时刻只能执行一种模式,也就是同一时刻,只有一个模式中的source、timer和observer有效,其他模式的source、timer和observer无效。苹果这样做的目的是防止不同模式中的source、timer和observer相互影响,不好控制。
可以通过timer的形式来监听RunLoop的执行流程:
进入RunLoop,首先会处理一些系统的事件(也就是首先执行timer、source0、source1)当处理完后,RunLoop就会睡觉。当用户触发一些事件后,RunLoop就会从睡眠中醒来,处理timer、source0和source1.处理完事件后又继续睡觉。
RunLoop是有生命周期的,RunLoop挂掉有两种情况:
1.生命周期到了,默认RunLoop的生命周期是很大的,不过我们可以自己设置runLoop的生命周期
2.线程挂了,RunLoop也会挂掉
RunLoop的应用
runLoop主要有5个应用场景:NSTimer、ImageView显示图片、performSelecter、常驻线程、自动释放池
1.NSTimer
上面已经举过一个NSTimer的例子,这里不再多说。
2.ImageView显示
默认程序启动会进入runLoop的default模式。performSelecter: withObject:afterDelay:inMode:方法默认就是在default模式下有效。而在track追踪模式下无效,所以可以通过设置模式来控制imageView图片的显示。
// 只有在追踪模式下才会给imageView设置图片
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"avatar"] afterDelay:2.0 inModes:@[UITrackingRunLoopMode]];
开发中一般在默认情况下设置图片而在追踪模式下是不设置图片的,这样一来可以提高我们应用程序的流畅性,为什么呢?如果在track模式下,不仅处理屏幕的拖拽事件,还要给imageView设置图片,很容易出现程序卡顿的现象。
3.PerformSelector
4.常驻线程
常驻线程应用场景:
举个例子,某个应用需要频繁的下载或者上传大容量的音频或者视频,默认主线程就是一个常驻线程,但是这种耗时操作肯定要转移到子线程中取完成。比如说微信\陌陌,用户有时候需要一直发送语音,如果每发送一条语音就开启一个自子线程,那么频繁的开启、销毁线极大的消耗手机性能,所以常驻线程就应运而生。
如何创建常驻线程?
尝试一:再次调用[self.thread start];答案当然是否定的。原因如下:
注意点:默认情况下,只要一个线程的任务执行完毕,那么这个线程就不能使用了。所以不能通过start方法来重新启动一个已经执行完任务的线程。否则会报以下错误: -[WSThread start]: attempt to start the thread again'
尝试二:给这个子线程一个执行不完的任务while(1);答案依然是否定的,原因如下:
把while(1)添加到子线程执行,而子线程的任务中有一个while死循环,那么其他任务永远也执行不到。
所以,通过死循环虽然保证了子线程永远不死,但是不能让子线程处理任务,因为子线程一直在处理while死循环的任务。
尝试三:联想主线程为什么不死,因为主线程默认一启动就会绑定一个runLoop,所以尝试给子线程绑定一个runLoop
[NSRunLoop currentRunLoop];
[runLoop run];
但是仅仅创建一个runLoop然后run依然无效。原因如下:
注意:
(1). currentRunLoop仅仅代表创建了一个NSRunLoop对象, 并没有运行RunLoop
(2). 一个NSRunLoop中, 如果没有source或者timer, 那么NSRunLoop就会退出死循环(面试很可能问到)。因为如果runLoop没有source和timer,那么这个runLoop就没有source和timer事件处理,这个runLoop也就变得没有意义,所以runLoop会自动退出。(runLoop是否退出和observer没有关系,只和source和timer有关系)
所以,给runLoop添加一个source或者timer
最终的解决方案:
NSRunLoop *runLoop =[NSRunLoop currentRunLoop]; // 以下代码的目的是为了保证runloop不死
/*
// 给runLoop添加一个timer
// NSTimer *timer = [NSTimer timerWithTimeInterval:99999 target:self selector:@selector(demo) userInfo:nil repeats:NO];
// [runLoop addTimer:timer forMode:NSDefaultRunLoopMode];
*/
// 或者给runLoop添加一个source,一般都是添加source,不添加timer,写三方框架的大牛都这么写 [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run];
至此,一个常驻子线程就已经创建好了,并且可以接受并处理事件。并且只要是在这个常驻子线程中执行的任务,都是在同一个线程中。
如下是创建常驻子线程的代码:
#import "ViewController.h" #import "WSThread.h" @interface ViewController () @property (weak, nonatomic) IBOutlet UIImageView *imageView; @property (nonatomic, strong) WSThread *thread; /**< 子线程 */ @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.thread = [[WSThread alloc] initWithTarget:self selector:@selector(demo) object:nil]; [self.thread start]; } - (void)demo { // 在子线程执行 NSLog(@"%s", __func__); // 注意点: 默认情况下只要一个线程的任务执行完毕, 那么这个线程就不能使用了 // 在self.thread线程中执行test方法 // [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:YES]; // while(1); // 给子线程添加一个RunLoop // 注意: // 1. currentRunLoop仅仅代表创建了一个NSRunLoop对象, 并没有运行RunLoop // 2. 一个NSRunLoop中, 如果没有source或者timer, 那么NSRunLoop就会退出死循环 NSRunLoop *runLoop =[NSRunLoop currentRunLoop]; // 以下代码的目的是为了保证runloop不死 [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run]; NSLog(@"-----------"); } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { // 主线程 NSLog(@"%s", __func__); // [self.thread start]; [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:YES]; } - (void)test { NSLog(@"%s %@", __func__, [NSThread currentThread]); } @end
打印结果:
2015-10-31 17:46:29.780 08-RunLoop应用场景[3855:303176] -[ViewController touchesBegan:withEvent:] 2015-10-31 17:46:29.780 08-RunLoop应用场景[3855:303300] -[ViewController test] <WSThread: 0x7fe44a53a290>{number = 2, name = (null)} 2015-10-31 17:46:29.959 08-RunLoop应用场景[3855:303176] -[ViewController touchesBegan:withEvent:] 2015-10-31 17:46:29.959 08-RunLoop应用场景[3855:303300] -[ViewController test] <WSThread: 0x7fe44a53a290>{number = 2, name = (null)} 2015-10-31 17:46:30.121 08-RunLoop应用场景[3855:303176] -[ViewController touchesBegan:withEvent:] 2015-10-31 17:46:30.122 08-RunLoop应用场景[3855:303300] -[ViewController test] <WSThread: 0x7fe44a53a290>{number = 2, name = (null)} 2015-10-31 17:46:30.266 08-RunLoop应用场景[3855:303176] -[ViewController touchesBegan:withEvent:] 2015-10-31 17:46:30.267 08-RunLoop应用场景[3855:303300] -[ViewController test] <WSThread: 0x7fe44a53a290>{number = 2, name = (null)} 2015-10-31 17:46:30.431 08-RunLoop应用场景[3855:303176] -[ViewController touchesBegan:withEvent:] 2015-10-31 17:46:30.432 08-RunLoop应用场景[3855:303300] -[ViewController test] <WSThread: 0x7fe44a53a290>{number = 2, name = (null)}
5.自动释放池
程序“即将进入runLoop”会创建自动释放池,“即将退出runLoop”会销毁自动释放池。即将进入休眠状态会销毁之前的自动释放池,再创建一个新的自动释放池。
所以,释放池中的对象不是立即销毁的,而是在即将进入休眠和退出runloop的时候销毁的。
/* _wrapRunLoopWithAutoreleasePoolHandler + activities = 0x1 = 1 = 即将进入RunLoop + 创建一个自动释放池 _wrapRunLoopWithAutoreleasePoolHandler + activities = 0xa0 = 160 = 128 + 32 + 32 即将进入休眠 1.销毁一个自动释放池 2.再创建一个新的自动释放池 + 128 即将退出RunLoop 销毁一个自动释放池 */ NSLog(@"%@", [NSRunLoop currentRunLoop]); NSLog(@"%d", 1 << 0); // 1 NSLog(@"%d", 1 << 1); // 2 NSLog(@"%d", 1 << 2); // 4 NSLog(@"%d", 1 << 5); // 32 NSLog(@"%d", 1 << 6); // 64 NSLog(@"%d", 1 << 7); // 128
学习RunLoop的资料
苹果官方文档
https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html
CFRunLoopRef是开源的
http://opensource.apple.com/source/CF/CF-1151.16/