RunLoop 之初探
你好2019!一起努力呀!
1、什么是runloop
runloop是通过内部维护的事件循环对事件/消息进行管理的一个对象。
事件循环(Event loop):通俗的解释:没有消息处理的时候,休眠以避免资源占用;有消息需要处理时,立即被唤醒!书面的解释:没有需要处理的消息时,用户态切换为内核态;有消息需要处理时,内核态切换为用户态!
用户态:一般时开发者开发使用的、常见的api
内核态:系统调用底层的相关,例如:开关机、来电等!
用户态与内核态的理解!
需要注意的是:接收消息->处理消息->等待 ,此处等待≠死循环!在之后runloop的是用场景以及分析中会解释这个!
可参考下图理解runloop的处理流程
2、runloop的组成
NSRunLoop是基于Foundation框架对CFRunLoop(基于CoreFoundation)的封装,因为NSRunloop是没有开源的,但是CFRunLoop是开源的,其结构基本一致,所以分析CFRunLoop的组成即可。
主要涉及到CFRunLoop、CFRunLoopMode、Source/Timer/oberser
CFRunLoop的构成:
pthread(这个可以知道runloop和线程一一对应)、currentMode、modes<mode型的集合,说明一个runloop可以用多个mode,如下图>、commondModes<字符串型的集合>、commonModeItems<多个Observer、多个timer、多个source的集合>
runloop与mode 一对多
关于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组合。
mode是由source/timer/observer组成
Source就是输入源事件,分为:source0,诸如UIEvent(触摸,滑动等),performSelector这种需要手动触发的操作;source1,处理系统内核的mach_msg事件(系统内部的端口事件)。诸如唤醒RunLoop或者让RunLoop进入休眠节省资源等。我们需要对常驻线程进行操作的事件大多都是source0。一般来说日常开发中我们需要关注的是source0,source1只需要了解。
Timer即为定时源事件,常见到的就是NSTimer,NSTimer定时器的触发正是基于RunLoop运行的,所以使用NSTimer之前必须注册到RunLoop,但是RunLoop为了节省资源并不会在非常准确的时间点调用定时器,如果一个任务执行时间较长,那么当错过一个时间点后只能等到下一个时间点执行,并不会延后执行(NSTimer提供了一个tolerance属性用于设置宽容度,如果确实想要使用NSTimer并且希望尽可能的准确,则可以设置此属性)。
Oberver相当于消息循环中的一个监听器,随时通知外部当前RunLoop的运行状态。NSRunLoop没有相关方法,只能通过CFRunLoop相关方法创建
// 创建observer CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { NSLog(@"----监听到RunLoop状态发生改变---%zd", activity); }); // 添加观察者:监听RunLoop的状态 CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
runloop是如何创建的呢?
苹果是不允许直接创建runloop,它提供了四个函数获取runloop
[NSRunLoop currentRunLoop];
//获取当前线程的RunLoop
[NSRunLoop mainRunLoop];
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 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。
除了主线程,其他线程的runloop默认是没有开启的,且从上面的源代码来看,任意一个子线程的RunLoop都会保证主线程的RunLoop的存在。
RunLoop正常运行的条件是:1.有Mode。2.Mode有事件源。3.运行在有事件源的Mode下。
源码分析之小结论:
①.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的事件后退出。
3、runloop的是用场景
3.1:保持线程存活
自定义一个继承自NSThread的HFSThread:目的是重写其dealloc方法,看何时被释放掉的
3.1.1:Thread关联的方法中不开启runloop的情况下:
1 - (void)threadTest
2 {
3 HFSThread *testHead = [[HFSThread alloc]initWithTarget:self selector:@selector(threadAction) object:nil];
4 testHead.name = @"testThread";
5 // self.myThread = testHead;
6 [testHead start];
9 }
2019-01-13 15:09:38.664810+0800 HaiFeiTestProject[26482:874633] begin threadAction -[NSThread currentThread] = <HFSThread: 0x600002da9a40>{number = 3, name = testThread}
2019-01-13 15:09:38.667483+0800 HaiFeiTestProject[26482:874633] 121212
2019-01-13 15:09:38.667780+0800 HaiFeiTestProject[26482:874633] end threadAction -[NSThread currentThread] = <HFSThread: 0x600002da9a40>{number = 3, name = testThread}
2019-01-13 15:09:38.668259+0800 HaiFeiTestProject[26482:874633] HFSThread dealloc name = testThread
可以发现,此thread关联的方法执行完毕之后被释放掉!
可能我们会觉得此处创建的thread是临时变量,那将其设置为控制器的属性,再次执行一次相关操作
此时的运行结果中没有执行其dealloc操作,我们会认为此thread没有被释放
那么如果再次执行其start方法会如何?
程序崩溃,原因如下:[HFSThread start]: attempt to start the thread again 也就是说虽然thread没有被释放,但是它处于死亡状态(线程执行结束之后就会进入这个状态),苹果不允许,已死亡的线程再次开启!
以上的操作会发现线程执行完其关联的任务之后就会死亡,如何保持其存活呢?
也许我们可以使用while循环保持线程存活,但是实践证明如果我们在线程关联的方法中执行如下操作
确实会让线程保持存活,但是此方法将会一直如此执行下去,显然不符合我们的期望!
3.1.2:初步尝试使用runloop
我们再次创建一个临时变量thread 在其关联的方法中如下操作
运行结果可以看出,虽然是临时的thread,但是没有在其关联的方法结束之后没有执行dealloc操作,并且根据运行结果可以发现[runloop run]之后的代码没有在执行!
原因:RunLoop本质就是个Event Loop的do while循环,所以运行到这一行以后子线程就一直在进行接受消息->等待->处理的循环。所以不会运行[runLoop run];之后的代码(这点需要注意,在使用RunLoop的时候如果要进行一些数据处理之类的要放在这个函数之前否则写的代码不会被执行),也就不会因为任务结束导致线程死亡进而销毁。
关于runloop的使用方法在讲述其组成的时候会进一步讲述!
3.2:线程在我们需要的时候响应消息
我们实现可以保持线程存活之后,希望实现在我们需要的时候响应消息
我们知道系统提供了几个某个线程中执行任务的放
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
在主线程中响应指定Selector。这两个方法给你提供了选项来阻断当前线程(不是执行Selector的线程而是调用上述方法的线程)直到selector被执行完毕。
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
在某个子线程(NSThread对像)中响应指定Selector。这两个方法同样给你提供了选项来阻断当前线程直到Selector被执行完毕。
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
在当前线程中执行Selector,并附加了延迟选项。多个排队的Selector会按照顺序一个一个的执行。
这几个方法都是向线程中的RunLoop发送了消息,然后RunLoop接收到了消息就唤醒线程,去做对应的事情。所以想要正常使用这几个方法,响应selector的线程必须开启了RunLoop。
例如:
最后子线程任务结束然后被释放是因为之前提到的,runMode:(NSString *)mode beforeDate:(NSDate *)limitDate这种启动RunLoop的方式有一个特性,那就是这个接口在非Timer事件触发(此处是达成了这个条件)、显式的用CFRunLoopStop停止RunLoop或者到达limitDate后会退出。而例子当中也没有用while把RunLoop包围起来,所以RunLoop退出后子线程完成了任务最后退出了!如果使用的是 [runloop run];那相关的触发操作可以一直执行!
3.3:线程定时执行某个任务
我们最初使用NSTimer的时候 创建之后却并没有按照我们预期的那样执行,之后添加了[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];才可以按照预期执行,当时我们大约并没有仔细分析其中缘由,
因为timer还提供了一些方法(以scheduledTimerWithTimeInterval开头的方法),可以不必设置runloop,因为这些方法创建一个定时器并自动添加到当前线程RunLoop的NSDefaultRunLoopMode中所以不需要开发者为处理相关!
NSTimer常见的问题:
1、时好时不好
用Timer的时间长了总有一天突然发现,为啥我的Timer运行的好好的突然就时好时坏了?在进行Scrollview的滚动操作时Timer不进行响应,滑动结束后timer又恢复正常了。大约很多人都曾经遇到过吧!
我们知道每次runloop只能运行在一个mode上,当我们创建一个NSTimer的时候,默认都是讲定时器添加到了加到了主线程RunLoop的NSDefaultRunLoopMode中。一般情况下主线程RunLoop就运行在NSDefaultRunLoopMode下,所以定时器正常运行。
但是当Scrollview开始滑动时,主线程RunLoop自动切换了当前运行的Mode(currentMode),变成了UITrackingRunLoopMode。所以现在RunLoop要处理的就是UITrackingRunLoopMode中item。
而我们的timer是添加在NSDefaultRunLoopMode中的,并没有添加到UITrackingRunLoopMode中。即我们的timer不是UITrackingRunLoopMode中的item。因为不同mode的item相关没有影响,所以RunLoop也就不会处理非当前Mode的item,所以定时器就不会响应。
当Scrollview滑动结束,主线程RunLoop自动切换了当前运行的Mode(currentMode),变成了NSDefaultRunLoopMode。我们的Timer是NSDefaultRunLoopMode的item,所以RunLoop会处理它,所以又正常响应了。
想Timer在两种Mode中都得到响应怎么办?前面提到过,一个item可以被同时加入多个mode。让Timer同时成为两种Mode的item就可以了(分别添加或者直接加到commonMode中),这样不管RunLoop处于什么Mode,timer都是当前Mode的item,都会得到处理。
我们还可以使用commonMode讲timer同步添加到多个mode中
commonMode它不是实际的一种mode,是同步source、timer、observer到多个mode的一种技术方案!
2、导致的ViewController无法释放问题
创建NSTimer会因为设置target为self导致Timer对ViewController有一个强引用,最后结果就是ViewController无法释放。
关于这个问题,目前个人有两个处理方法:创建一个中间对象,处理timer和控制器 ;使用GCDTimer(关于这个的使用,在之后会进行详细说明!)
针对第一种方法的实现代码说明:
1 #import <Foundation/Foundation.h> 2 #import <UIKit/UIKit.h> 3 4 5 NS_ASSUME_NONNULL_BEGIN 6 7 @interface ManagerTimer : NSObject 8 9 @property (nonatomic, weak) NSTimer *myTimer; 10 11 @property (nonatomic, weak) UIViewController *myVC; 12 - (void)startTimer; 13 @end 14 15 NS_ASSUME_NONNULL_END
1 #import "ManagerTimer.h" 2 3 @implementation ManagerTimer 4 5 - (id)init{ 6 if (self = [super init]) { 7 8 } 9 10 return self; 11 } 12 - (void)startTimer 13 { 14 self.myTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES]; 15 } 16 17 - (void)dealloc 18 { 19 NSLog(@"ManagerTimer dealloc "); 20 NSLog(@"self.myTimer = %@",self.myTimer); 21 22 } 23 - (void)timerAction:(NSTimer *)timer 24 { 25 static NSInteger num = 0; 26 NSLog(@"ManagerTimer num = %ld",num++); 27 if (self.myVC == nil) { 28 [timer invalidate]; 29 30 timer = nil; 31 32 } 33 34 } 35 36 37 @end
在控制器中使用
ManagerTimer *managerTimer = [[ManagerTimer alloc]init]; managerTimer.myVC = self; [managerTimer startTimer];
1 2019-01-13 17:10:00.438225+0800 HaiFeiTestProject[27851:974282] ManagerTimer num = 0 2 2019-01-13 17:10:01.439114+0800 HaiFeiTestProject[27851:974282] ManagerTimer num = 1 3 2019-01-13 17:10:02.439077+0800 HaiFeiTestProject[27851:974282] ManagerTimer num = 2 4 2019-01-13 17:10:03.438536+0800 HaiFeiTestProject[27851:974282] ManagerTimer num = 3 5 2019-01-13 17:10:04.438006+0800 HaiFeiTestProject[27851:974282] ManagerTimer num = 4 6 2019-01-13 17:10:04.601969+0800 HaiFeiTestProject[27851:974282] FirstViewController dealloc 7 2019-01-13 17:10:04.602102+0800 HaiFeiTestProject[27851:974282] (null) 8 2019-01-13 17:10:05.439199+0800 HaiFeiTestProject[27851:974282] ManagerTimer num = 5 9 2019-01-13 17:10:05.439585+0800 HaiFeiTestProject[27851:974282] ManagerTimer dealloc 10 2019-01-13 17:10:05.440027+0800 HaiFeiTestProject[27851:974282] self.myTimer = <__NSCFTimer: 0x600003b78300>
按照我们的预期实现了!
如果不是用这个中间对象,在我们离开当前控制器的时候,定时器无法停止,控制器也无法释放!
文中若有不对之处,还请劳驾之处,谢谢!
部分参考链接:RunLoop入门小结 这个链接中相关的其他关于runloop的一些博客分析也很值得去看!