一个应用开始运行之后放在那里,若是不对它进行任何操做,这个应用就像静止了同样,不会自发的有任何动做发生,可是若是咱们点击界面上的一个按钮,这个时候就会有对应的按钮响应事件发生。给咱们的感受就像应用一直处于随时待命的状态,在没人操做的时候它一直在休息,在让它干活的时候,它就能马上响应。其实,这就是run loop的功劳。安全
<1>线程任务的类型架构
线程的任务能够形象地分为:app
(1)直线型:执行一段任务以后,就被释放掉了。框架
(2)环型:不断循环,直到经过某种方式将它终止。函数
<2>线程与run loop的关系oop
Run loop,正如其名,loop表示某种循环,和run放在一块儿就表示一直在运行着的循环。实际上,run loop和线程是紧密相连的,能够这样说run loop是为了线程而生,没有线程,它就没有存在的必要。Run loops是线程的基础架构部分,Cocoa和CoreFundation都提供了run loop对象方便配置和管理线程的run loop(如下都已Cocoa为例)。每一个线程,包括程序的主线程(main thread)都有与之相应的run loop对象。学习
iOS 系统中,提供了两种RunLoop:NSRunLoop 和 CFRunLoopRef。测试
<1 CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,全部这些 API 都是线程安全的。优化
<2 NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,可是这些 API 不是线程安全的。spa
<3 CFRunLoopRef 的代码是开源的。
其中:主线程中的runloop是默认启动的。
int main(int argc, char *argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([appDelegate class])); } }
重点是UIApplicationMain() 函数,这个方法会为main thread 设置一个NSRunLoop 对象。这样就能解释了为何系统没有任务执行时进行死亡状态,有任务执行时又能进行响应。
1.保持线程的存活,而不是线性的执行完任务就退出了
<1>不开启RunLoop的线程
在遇到一些耗时操做时,为了不主线程阻塞致使界面卡顿,影响用户体验,每每咱们会把这些耗时操做放在一个临时开辟的子线程中。操做完成了,子线程线性的执行了代码也就退出了,就像下面同样。
-(void)notDidThread{ NSLog(@"%@ -------开辟子线程",[NSThread currentThread]); MyThread *subThread = [[MyThread alloc]initWithTarget:self selector:@selector(subThreaddo) object:nil]; subThread.name = @"subThread"; [subThread start]; } -(void)subThreaddo{ NSLog(@"%@----执行子线程任务",[NSThread currentThread]); }
其中MyThread是一个继承自NSThread的子类,并重写了dealloc方法。
-(void)dealloc { NSLog(@"%@线程被释放了", self.name); }
看一下打印结果:
<NSThread: 0x600001a22880>{number = 1, name = main} -------开辟子线程 <MyThread: 0x600001a42640>{number = 3, name = subThread}----执行子线程任务 subThread线程被释放了
能够看到子线程subThread在任务执行结束后,已经被释放掉了。
<1>开启RunLoop的线程
(1)实验用self来持有子线程
一样也是上个代码,让self对子线程进行持有,再看输出结果。
self.subThread = [[MyThread alloc]initWithTarget:self selector:@selector(subThreaddo) object:nil]; self.subThread.name = @"subThread"; [self.subThread start];
<NSThread: 0x600002f9e900>{number = 1, name = main} -------开辟子线程 <MyThread: 0x600002fc2c40>{number = 3, name = subThread}----执行子线程任务
在任务执行完成以后,子线程并无被释放掉。那既然没有被释放掉,若是再去从新开启能行吗?
self.subThread = [[MyThread alloc]initWithTarget:self selector:@selector(subThreaddo) object:nil]; self.subThread.name = @"subThread"; [self.subThread start]; [self.subThread start];//从新开启一次
<NSThread: 0x600002cb8000>{number = 1, name = main} -------开辟子线程 <MyThread: 0x600002cd5ac0>{number = 3, name = subThread}----执行子线程任务 *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[MyThread start]: attempt to start the thread again'
发现已经崩溃了。任务执行完毕后,thread虽然没有被释放掉,仍是处于内存中,可是它处于死亡状态(当线程执行完毕后,都会进如到这种状态),因此若是从新开启会出现崩溃。苹果在线程死亡后不容许从新开启。
<2>初步尝试使用RunLoop
如今咱们来初步了解下RunLoop如何使用,顺便作个小测试。
- (void)viewDidLoad { [super viewDidLoad]; 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 addPort:[NSMachPort port] forMode:NSRunLoopCommonModes]; //让RunLoop跑起来 [runLoop run]; NSLog(@"%@----执行子线程任务结束",[NSThread currentThread]);
查看输出结果:
<NSThread: 0x600002621400>{number = 1, name = main} -------开辟子线程 <MyThread: 0x600002677ec0>{number = 3, name = subThread}----执行子线程任务
这里没有对线程进行引用,也没有让线程内部的任务进行显式的循环。为何子线程的里面的任务没有执行到输出任务结束这一步,为何子线程没有销毁?就是由于[runLoop run];这一行的存在。
RunLoop本质就是个Event Loop的do while循环,因此运行到这一行之后子线程就一直在进行接受消息->等待->处理的循环。因此不会运行[runLoop run];以后的代码(这点须要注意,在使用RunLoop的时候若是要进行一些数据处理之类的要放在这个函数以前不然写的代码不会被执行),也就不会由于任务结束致使线程死亡进而销毁。
<3>如何建立RunLoop?
苹果不容许直接建立 RunLoop,它只提供了四个自动获取的函数
[NSRunLoop currentRunLoop];//获取当前线程的RunLoop [NSRunLoop mainRunLoop];//获取主线程的RunLoop CFRunLoopGetMain(); CFRunLoopGetCurrent();
函数内部的逻辑大概是下面这样:
/// 全局的Dictionary,key 是 线程, value 是 CFRunLoopRef static CFMutableDictionaryRef loopsDic; /// 访问 loopsDic 时的锁 static CFSpinLock_t loopsLock; /// 获取一个 pthread 对应的 RunLoop。 CFRunLoopRef _CFRunLoopGet(pthread_t thread) { OSSpinLockLock(&loopsLock); if (!loopsDic) { // 第一次进入时,初始化全局Dic,并先为主线程建立一个 RunLoop。 loopsDic = CFDictionaryCreateMutable(); CFRunLoopRef mainLoop = _CFRunLoopCreate(); CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop); } /// 直接从 Dictionary 里获取。 CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread)); if (!loop) { /// 取不到时,建立一个 loop = _CFRunLoopCreate(); CFDictionarySetValue(loopsDic, thread, loop); /// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。 _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop); } OSSpinLockUnLock(&loopsLock); return loop; } CFRunLoopRef CFRunLoopGetMain() { return _CFRunLoopGet(pthread_main_thread_np()); } CFRunLoopRef CFRunLoopGetCurrent() { return _CFRunLoopGet(pthread_self()); }
注:这并非源码,而是大神为了方便咱们理解,对源码进行了一些可读性优化后的结果。
一、线程默认不开启RunLoop,为何咱们的App或者说主线程却能够一直运行而不会结束?
主线程是惟一一个例外,当App启动之后主线程会自动开启一个RunLoop来保证主线程的存活并处理各类事件。并且从上面的源代码来看,任意一个子线程的RunLoop都会保证主线程的RunLoop的存在。
二、RunLoop能正常运行的条件是什么?
看到刚才代码中注释说暂时无论的代码,第一次接触确定会想[runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];这一句是什么意思?为何必须加这一句RunLoop才能正常运行?
- (void)viewDidLoad { [super viewDidLoad]; 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 addPort:[NSMachPort port] forMode:NSRunLoopCommonModes]; NSLog(@"RunLoop:%@",runLoop); //让RunLoop跑起来 [runLoop run]; NSLog(@"%@----执行子线程任务结束",[NSThread currentThread]); }
注释掉获得的结果
不注释获得的结果
注释掉之后咱们看似run了RunLoop可是最后线程仍是结束了任务,而后销毁了。与没注释获得的结果比较,形成这一切的缘由就在上面两张图片中标注部分的区别上。要解释这一部分就又要开始讲到让咱们抓耳挠腮的概念部分,咱们先来看一张眼熟到不行的RunLoop结构图。
一开始接触RunLoop我看到这张图的时候也是懵逼的,如今咱们结合刚才的打印结果来理解。
图中RunLoop蓝色部分就对应咱们打印结果中,整个RunLoop部分的打印结果
多个绿色部分共同被包含在RunLoop内就对应,打印结果中modes中同时包含多个Mode(这里但是看打印结果中标注出来的第一行往上再数两行。modes = ... count = 1。一个RunLoop能够包含多个Mode,每一个Mode的Name不同,只是在这个打印结果当中目前恰好Mode个数为1)
每个绿色部分Mode总体就对应,打印结果中被标注出来的总体。
黄色部分Source对应标注部分source0+source1
黄色部分Observer对应标注部分observer部分
黄色部分Timer对应标注部分timers部分
<1 Mode
我对Mode的理解就是”行为模式“,就像咱们说到上学这个行为模式,它就应该包含起床,出门,去学校,上课,午休等等。可是,若是上学这个行为模式什么都不包含,那么即便咱们进行上学这个行为,咱们也一直睡在床上什么都不会作。就像刚才注释掉addPort那一行代码获得的结果同样,RunLoop在kCFRunLoopDefaultMode下run了,可是由于该Mode下全部东西都为null(不包含任何内容),因此RunLoop什么都没作又退出来了,而后线程就结束任务最后销毁。之因此要有Mode的存在是为了让RunLoop在不一样的”行为模式“之下执行不一样的”动做“互不影响。好比执行上学这个行为模式就不能进行娱乐这个行为模式下的游戏这个动做。RunLoop同一时间只能运行在一种Mode下,当前运行的这个Mode叫currentMode。(这里也许比较抽象,在下面timer部分会有实例结合实例分析。)
通常咱们经常使用的Mode有三种
1.kCFRunLoopDefaultMode(CFRunLoop)/NSDefaultRunLoopMode(NSRunLoop) 默认模式,在RunLoop没有指定Mode的时候,默认就跑在DefaultMode下。通常状况下App都是运行在这个mode下的 2.(CFStringRef)UITrackingRunLoopMode(CFRunLoop)/UITrackingRunLoopMode(NSRunLoop) 通常做用于ScrollView滚动的时候的模式,保证滑动的时候不受其余事件影响。 3.kCFRunLoopCommonModes(CFRunLoop)/NSRunLoopCommonModes(NSRunLoop) 这个并非某种具体的Mode,而是一种模式组合,在主线程中默认包含了NSDefaultRunLoopMode和 UITrackingRunLoopMode。子线程中只包含NSDefaultRunLoopMode。 注意: ①在选择RunLoop的runMode时不能够填这种模式不然会致使RunLoop运行不成功。 ②在添加事件源的时候填写这个模式就至关于向组合中全部包含的Mode中注册了这个事件源。 ③你也能够经过调用CFRunLoopAddCommonMode()方法将自定义Mode放到 kCFRunLoopCommonModes组合。
<2 Source
source就是输入源事件,分为source0和source1这两种。
1.source0:诸如UIEvent(触摸,滑动等),performSelector这种须要手动触发的操做。 2.source1:处理系统内核的mach_msg事件(系统内部的端口事件)。诸如唤醒RunLoop或者让RunLoop进入休眠节省资源等。 通常来讲平常开发中咱们须要关注的是source0,source1只须要了解。 之因此说source0更重要是由于平常开发中,咱们须要对常驻线程进行操做的事件大多都是source0,稍后的实验会讲到。
<3 Timer
Timer即为定时源事件。通俗来说就是咱们很熟悉的NSTimer,其实NSTimer定时器的触发正是基于RunLoop运行的,因此使用NSTimer以前必须注册到RunLoop,可是RunLoop为了节省资源并不会在很是准确的时间点调用定时器,若是一个任务执行时间较长,那么当错过一个时间点后只能等到下一个时间点执行,并不会延后执行(NSTimer提供了一个tolerance属性用于设置宽容度,若是确实想要使用NSTimer而且但愿尽量的准确,则能够设置此属性)。
<4 Observer
它至关于消息循环中的一个监听器,随时通知外部当前RunLoop的运行状态。NSRunLoop没有相关方法,只能经过CFRunLoop相关方法建立
// 建立observer CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { NSLog(@"----监听到RunLoop状态发生改变---%zd", activity); }); // 添加观察者:监听RunLoop的状态 CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
因为它与这一问的关系并不大因此暂时不作过多阐述,但愿进一步了解Observer能够查看文末的文档或者RunLoop入门学习补充资料(3.Observer)。
重点:它不能做为让RunLoop正常运行的条件,只有Observer的RunLoop也是没法正常运行的。