RunLoop做用html
模拟RunLoop内部实现面试
iOS中有两套API能够建立获取RunLoop对象。分别是Foundation框架的NSRunLoop和C语言的CFRunLoopRef微信
NSRunLoop和CFRunLoopRef都表明着RunLoop对象
NSRunLoop是基于CFRunLoopRef的一层OC包装,因此要了解RunLoop内部结构,须要多研究CFRunLoopRef层面的API(Core Foundation层面)app
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得手动建立而且手动启动(调用run方法)函数
RunLoop在第一次获取时建立,在线程结束时销毁oop
能够理解为,子线程的RunLoop是懒加载的(主线程除外)。只有用到的时候才会建立( 调用currentRunLoop方法)。性能
若是是在子线程中调用currentRunLoop,那么系统会先查看当前子线程是否有与之对应的NSRunLoop,若是没有就建立一个RunLoop对象学习
注意:若是想给子线程添加一个与之对应的RunLoop,不能经过alloc、init方法,只能经过currentRunLoop,若是用alloc、init建立出来的RunLoop不能添加到子线程。ui
[NSRunLoop currentRunLoop]; // 得到当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 得到主线程的RunLoop对象
CFRunLoopGetCurrent(); // 得到当前线程的RunLoop对象
CFRunLoopGetMain(); // 得到主线程的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); } // 从字典中获取子线程 CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); __CFUnlock(&loopsLock); if (!loop) { // 若是不存在建立一个新的子线程 CFRunLoopRef newLoop = __CFRunLoopCreate(t); __CFLock(&loopsLock); loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); // 保存子线程 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对象存储到字典中去。
Core Foundation中关于RunLoop的5个类:
CFRunLoopRef :RunLoop对象
CFRunLoopModeRef :RunLoop的模式,能够把RunLoop理解为空调,对应着许多模式,可是一个RunLoop同时只能执行一种模式
CFRunLoopSourceRef : 事件来源,用来处理RunLoop的事件
CFRunLoopTimerRef :定时器,处理和定时器相关的事情
CFRunLoopObserverRef :经过observer监听事件
一个RunLoop有多个模式:每一个模式都有各自的source、timer和observer。
注意:一个RunLoop有多个模式,可是在同一时刻只能执行一种模式。
CFRunLoopModeRef:
CFRunLoopModeRef表明RunLoop的运行模式
一个 RunLoop 包含若干个 Mode,每一个Mode又包含若干个Source/Timer/Observer
每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称做 CurrentMode
若是须要切换Mode,只能退出Loop,再从新指定一个Mode进入
这样作主要是为了分隔开不一样组的Source/Timer/Observer,让其互不影响
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的, 用于用户主动触发事件
Source1:基于Port的,经过内核和其余线程相互发送消息
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主要有5个应用场景:NSTimer、ImageView显示图片、performSelecter、常驻线程、自动释放池
默认程序启动会进入runLoop的default模式。performSelecter: withObject:afterDelay:inMode:方法默认就是在default模式下有效。而在track追踪模式下无效,因此能够经过设置模式来控制imageView图片的显示。
// 只有在追踪模式下才会给imageView设置图片
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"def"] afterDelay:2.0 inModes:@[UITrackingRunLoopMode]];
开发中通常在默认状况下设置图片而在追踪模式下是不设置图片的,这样一来能够提升咱们应用程序的流畅性,为何呢?若是在track模式下,不只处理屏幕的拖拽事件,还要给imageView设置图片,很容易出现程序卡顿的现象。
常驻线程应用场景:
举个例子,某个应用须要频繁的下载或者上传大容量的音频或者视频,默认主线程就是一个常驻线程,可是这种耗时操做确定要转移到子线程中取完成。好比说微信\陌陌,用户有时候须要一直发送语音,若是每发送一条语音就开启一个自子线程,那么频繁的开启、销毁线极大的消耗手机性能,因此常驻线程就应运而生。
如何建立常驻线程?
尝试一:再次调用[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] <XMGThread: 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] <XMGThread: 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] <XMGThread: 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] <XMGThread: 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] <XMGThread: 0x7fe44a53a290>{number = 2, name = (null)}
5.自动释放池
程序“即将进入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
苹果官方文档https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.htmlCFRunLoopRef是开源的http://opensource.apple.com/source/CF/CF-1151.16/