随着oc语言不断迭代,苹果的API也是逐步完善,RunLoop在实际开发中应用的越来越少。但是在面试中,假如面试官问你RunLoop的相关知识了解,那就相当于面试官在问你从事iOS开发工作的真实年限问题。那么下面我们就详细了解一下RunLoop的相关知识。
RunLoop是一种运行循环机制,其实就是死循环。它有两个作用:1.保证程序不退出 2.负责监听事件。我们可以理解为Runloop在不断的循环中,一直在询问线程队列中是否有待办任务。如果有任务就去处理,没有任务就休息。
Runloop模式:在官方文档介绍中,Runloop有五种应用模式,但是实际开发中,提供给我们开发者使用的就是常见的三种。
Runloop由三种元素组成:Source(事件源) Timer(定时器) Observer(观察),Runloop底层调用的是CoreFoundation框架下的CFRunloop,有兴趣的可以研究下。
Source(事件源):按照函数调用栈Source的分类
Source0:非Source1就是Source0
Source1:系统内核事件
- (void)viewDidLoad {
[super viewDidLoad];
// GCD
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0);
//设置回调
dispatch_source_set_event_handler(_timer, ^{
NSLog(@"@-----%@",[NSThread currentThread]);
});
//启动定时器 timer本质上也是一个source
dispatch_resume(_timer);
}
- (void)viewDidLoad {
[super viewDidLoad];
//获取RunLoop
CFRunLoopRef runloop = CFRunLoopGetCurrent();
//runloop是不是指针?是指针
//需要释放嘛?不需要
//创建观察者
CFRunLoopObserverRef runloopObserver = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, 0, &callBack, NULL);
//添加观察者
CFRunLoopAddObserver(runloop, runloopObserver, kCFRunLoopDefaultMode);
CFRelease(runloopObserver);
// free(runloopObserver);
// free和CFRelease(CoreFoundation下的CFRelease)的区别?
/*
CFRunLoopObserverCreate开辟堆空间,是个结构体。这个结构体内部有可能仍然有一个指针指向另一个堆空间。如果直接free,其内部的指针指向的堆空间就会内存泄露!
CFRelease作为CoreFoundation提供给开发者的方法,它会处理上面的问题。
*/
}
- (void)viewDidLoad {
[super viewDidLoad];
NSTimer * timer= [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
//currentRunLoop:获取当前线程的Runloop mainRunLoop:主线程的Runloop
/*
1.默认模式:NSDefaultRunLoopMode
在默认模式下,RunLoop会一直询问当前队列中是否存在待解决任务,有任务就去执行。但是当主线程中用户触发UI事件,应用此模式的RunLoop就会停止监听,从而不会触发回调事件
*/
//[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
/*
2.UI模式:UITrackingRunLoopMode
在UI模式下,UI模式是优先级最高的,当用户触发UI事件,会即刻触发RunLoop。但是只能被UI事件所唤醒
*/
//[[NSRunLoop currentRunLoop]addTimer:timer forMode:UITrackingRunLoopMode];
/*
3.占位模式:NSRunLoopCommonModes
占位模式下,上面两种模式都会被触发
*/
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
}
-(void)timerMethod{
NSLog(@"%@--",[NSThread currentThread]);
}
RunLoop与多线程
如果RunLoop的timer回调里面有耗时操作,设置到主线程的RunLoop会使界面造成卡顿感,所以我们要把RunLoop放在子线程中。
- (void)viewDidLoad {
[super viewDidLoad];
NSThread * thread =[[NSThread alloc]initWithBlock:^{
NSTimer * timer= [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
NSLog(@"来了");
}];
[thread start];
}
-(void)timerMethod{
NSLog(@"come here");
[NSThread sleepForTimeInterval:1.0];
NSLog(@"%@--",[NSThread currentThread]);
}
那么问题来了,假如我们使用上面的方式,那么log输出应该是怎样的呢?
2018-04-25 11:17:59.551463+0800 多线程[3867:89870] [MC] Lazy loading NSBundle MobileCoreServices.framework
2018-04-25 11:17:59.568394+0800 多线程[3867:89870] [MC] Loaded MobileCoreServices.framework
2018-04-25 11:17:59.665077+0800 多线程[3867:89994] 来了
没错,只是输出了“来了”,timer的timerMethod回调方法并没有被触发,原因是我们创建的thread子线程是个局部变量,在viewDidLoad方法执行完毕后就被释放了。
我们可以通过这样的方式进行验证。创建一个继承自NSThread的WTThread子类,在WTThread的dealloc方法中,设置log输出一下。
#import "WTThread.h"
@implementation WTThread
-(void)dealloc{
NSLog(@"线程走了");
}
@end
//修改viewDidLoad方法
- (void)viewDidLoad {
[super viewDidLoad];
NSThread * thread =[[WTThread alloc]initWithBlock:^{
NSTimer * timer= [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
NSLog(@"来了");
}];
[thread start];
}
-(void)timerMethod{
NSLog(@"come here");
[NSThread sleepForTimeInterval:1.0];
NSLog(@"%@--",[NSThread currentThread]);
}
log输出
2018-04-25 11:17:59.665077+0800 多线程[3867:89994] 来了
2018-04-25 11:17:59.665709+0800 多线程[3867:89994] 线程走了
我们会很明显的看到WTThread的dealloc方法执行了。我们可能会猜测会不会是我们在创建thread子线程的时候创建的是局部变量,才导致的这个问题呢?我们继续验证一下。
@property(nonatomic,strong)WTThread* thread;
- (void)viewDidLoad {
[super viewDidLoad];
_thread =[[WTThread alloc]initWithBlock:^{
NSTimer * timer= [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
NSLog(@"来了");
}];
[_thread start];
}
-(void)timerMethod{
NSLog(@"come here");
[NSThread sleepForTimeInterval:1.0];
NSLog(@"%@--",[NSThread currentThread]);
}
那我们这样解决的话,可以触发timerMethod回调方法嘛?答案是依旧不会。
2018-04-25 11:30:00.750498+0800 多线程[4116:97855] [MC] Lazy loading NSBundle MobileCoreServices.framework
2018-04-25 11:30:00.779425+0800 多线程[4116:97855] [MC] Loaded MobileCoreServices.framework
2018-04-25 11:30:00.830370+0800 多线程[4116:98055] 来了
我们就会明白,我们创建的WTThread是走了的,它并没有被释放(因为没走dealloc方法),但是timerMethod回调方法为什么依旧不执行呢?
重点:我们使用strong修饰创建的thread(@property(nonatomic,strong)WTThread* thread;)它实际是属于OC对象,这个OC对象和我们所说的线程是有区别的。我们创建的WTThread* thread它代表着一条线程,但是!这不代表着这个对象就是线程本身!
线程的生命:只会由CPU决定!只和线程的任务有关系,任务没有了,线程就走了。任务如果在,线程就依旧会在!我们接下来继续验证,回到最初的代码。假如我们给创建的子线程一个永远执行不完的任务,那么这条线程就会永远被保存!
- (void)viewDidLoad {
[super viewDidLoad];
NSThread * thread =[[WTThread alloc]initWithBlock:^{
NSTimer * timer= [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
NSLog(@"来了");
while (true) {
//啥都不干
}
}];
[thread start];
}
-(void)timerMethod{
NSLog(@"come here");
[NSThread sleepForTimeInterval:1.0];
NSLog(@"%@--",[NSThread currentThread]);
}
我们这样操作,NSThread * thread这个OC对象不会被释放,因为它代表的线程没有被释放!我们保住了thread子线程,但是!timerMethod回调依旧没有被触发?原因是啥?原因就是([[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];)timer对象被添加进RunLoop当中去了,但是我们写的while循环和RunLoop没有关系啊!那我们该咋办呢?
重点:线程中的RunLoop是不会自己主动跑起来的,它需要我们让它跑起来!但是我们在想在主线程中,RunLoop为什么会自动跑呢?因为在主线程的main函数当中,UIApplication帮我们开启了RunLoop。
NSThread * thread =[[WTThread alloc]initWithBlock:^{
NSTimer * timer= [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
[[NSRunLoop currentRunLoop] run];//让RunLoop运行起来(死循环,本质上就是do while 让线程保活,只不过跟我们自己写的while (true)相比,它里面做了很多事:当前队列中有没有任务需要处理,要不要处理)
NSLog(@"来了");
while (true) {
//啥都不干
}
}];
[thread start];
这样,我们就可以触发timerMethod回调。那我们继续想,如何取消这个RunLoop呢?
假如我们直接 [[NSRunLoop currentRunLoop] run];那麽我们很难将RunLoop取消掉,我们可以这样做
#import "ViewController.h"
#import "WTThread.h"
@interface ViewController ()
@property(nonatomic,assign)BOOL isFinished;
//@property(nonatomic,strong)WTThread* thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_isFinished=NO;
NSThread * thread =[[WTThread alloc]initWithBlock:^{
NSTimer * timer= [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
// [[NSRunLoop currentRunLoop] run];//死循环
NSLog(@"来了");
while (!_isFinished) {
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.00001]];//每隔0.00001秒执行一次RunLoop
}
}];
[thread start];
}
//触摸开始后停止RunLoop(实际使用时结合业务逻辑使RunLoop停止)
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
_isFinished = YES;
}
-(void)timerMethod{
NSLog(@"come here");
[NSThread sleepForTimeInterval:1.0];
NSLog(@"%@--",[NSThread currentThread]);
}
@end
或者这样
#import "ViewController.h"
#import "WTThread.h"
@interface ViewController ()
@property(nonatomic,assign)BOOL isFinished;
//@property(nonatomic,strong)WTThread* thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_isFinished=NO;
NSThread * thread =[[WTThread alloc]initWithBlock:^{
NSTimer * timer= [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
[[NSRunLoop currentRunLoop] run];//死循环
NSLog(@"来了");
[thread start];
}
//触摸开始后停止RunLoop(实际使用时结合业务逻辑使RunLoop停止)
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
_isFinished = YES;
}
-(void)timerMethod{
NSLog(@"come here");
if(_isFinished){
[NSThread exit];
}
[NSThread sleepForTimeInterval:1.0];
NSLog(@"%@--",[NSThread currentThread]);
}
@end
拓展:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[NSThread exit];
}
touchesBegan这个方法所在的线程是哪个线程呢?当然是主线程,那么在主线程中执行exit,程序会如何运行呢?答案是程序正常运行,但是用户就操作不了UI了。这就涉及到主线程和子线程的区别。
主线程和子线程的区别:
主线程其实就是程序的一个子线程
面试题:UIKit是否是线程安全的?
答案:不是线程安全的。如果出现了多条线程访问,就会出现资源抢夺。因为UIKit没有加锁
所以苹果和开发者就约定用主线程访问UI,主线程就是更新UI的作用。所以我们也叫主线程为UI线程。