RunLoop学习
开篇几道面试题:
讲讲RunLoop,在项目中有用到吗?
runloop内部实现逻辑
runloop和线程的关系
timer与runloop的关系
程序中添加每3秒响应一次的NSTimer,当拖动tableview时timer可能无法响应,为什么?怎样可以解决?
runloop是怎样响应用户操作的,具体流程是什么样?
说说runloop的几种状态
runloop的model作用是什么?
RunLoop,顾名思义,就是运行循环,就是在程序运行过程中循环做一些事情。
RunLoop的应用范围:
定时器(Timer)、PerformSelector、GCD
事件响应、手势识别、界面刷新、网络请求、自动释放池
RunLoop做了一个类似do-while循环的事情,有事的时候去处理消息,没事的时候就睡眠并等待消息。
伪代码:
RunLoop的基本作用:
保持程序的持续运行
处理App中的各种事件(比如:触摸事件、定时器事件)
节省CPU资源,提高程序性能:该做事做事,没事去休息
RunLoop对象
iOS有两套API来访问和使用runloop
在Foundation框架下的NSRunLoop
在Core Foundation框架下的CFRunLoopRef
NSRunLoop和CFRunLoopRef都代表着RunLoop对象
NSRunLoop是对CFRunLoopRef的一层OC包装
CFRunLoopRef是开源的
NSRunLoop *runloop1 = [NSRunLoop currentRunLoop];
CFRunLoopRef runloop2 = CFRunLoopGetCurrent();
两种方法都可以拿到RunLoop对象。
RunLoop与线程关系
每条线程都有唯一的一个与之相对应的RunLoop对象
RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
RunLoop会在线程结束时销毁
主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop。
稍微翻下源码:
确实表明:
每条线程都有唯一的一个与之相对应的RunLoop对象
RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
RunLoop相关的类
Core Foundation中关于RunLoop的5个类
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
CFRunLoopRef的定义:
可以看出,里面有一个CFRunLoopModeRef类型的_currentModel,以及CFMutableSetRef类型的_modes。
需要说明的是,CFMutableSetRef是一个集合,集合里面是也是CFRunLoopModeRef类型,那么
CFRunLoopModeRef的结构又是怎样的呢?
可以看出,CFRunLoopModeRef里面主要有:
mode的名字,以及sources0、sources1、observers、timers。
sources0、sources1里面装着CFRunLoopSourceRef类型的对象
observers里面装着CFRunLoopObserverRef类型的对象
timers里面装着CFRunLoopTimerRef类型的对象
可以看出:
CFRunLoopRef里面有CFRunLoopModeRef类型的_currentModel和_modes,而CFRunLoopModeRef里面又有CFRunLoopSourceRef、CFRunLoopObserverRef和CFRunLoopTimerRef三种类型。
CFRunLoopRef包含CFRunLoopModeRef
CFRunLoopModeRef包含CFRunLoopSourceRef、CFRunLoopObserverRef和CFRunLoopTimerRef
RunLoop里面有很多mode(模式),但是在运行的时候,只会选择一种mode(模式)。
CFRunLoopModeRef
CFRunLoopModeRef代表RunLoop的运行模式
一个RunLoop包含若干个Mode,每个Mode又包含若干个source0、source1、observer、timer
RunLoop启动时只能选择其中一个Mode作为currentMode。
如果需要切换Mode,只能退出当前loop,重新选择一个Mode进入。
不同组的source0、source1、observer、timer能分隔开来,互不影响
如果Mode里没有任何source0、source1、observer、timer,RunLoop会立马退出
常见的两种Mode模式:
kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程在这个Mode下运行。
UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响。
NSRunLoop *runloop1 = [NSRunLoop currentRunLoop];
CFRunLoopRef runloop2 = CFRunLoopGetCurrent();
NSLog(@"%p, %p", runloop1, runloop2);
结果:0x600000f7cc60, 0x60000177c000
怎么打印的结果地址不一样呢?难道一个线程有两个runloop?
其实并不是,看下一个结果:
NSLog(@"111%@, 333%@", runloop1, runloop2);
111<CFRunLoop 0x60000177c000.....
333<CFRunLoop 0x60000177c000 [0x7fff80617cb0].....
可以看到,runloop1和runloop2的地址是一样的,都是runloop2的地址。
说明,NSRunLoop是对CFRunLoopRef的一层封装,实际runloop还是CFRunLoopRef的地址。
CFRunLoopModeRef里面的内容分别代表什么?
Source0
触摸事件处理
performSelector:onThread:
Source1
基于Port的线程间通信
系统事件捕捉
Timers
NSTimer
performSelector:withObject:afterDelay:
Observers
用于监听RunLoop的状态
UI刷新(BeforeWaiting)
Autorelease pool(BeforeWaiting)
lldb指令下,bt可以打印出函数调用栈
可以发现,触摸事件,是由Sourse0处理的。
CFRunLoopObserverRef
添加Observer监听RunLoop的所有状态
RunLoop的运行逻辑
RunLoop休眠的实现原理
休眠是调用的内核api
RunLoop在实际开中的应用
- 控制线程生命周期(线程保活)
- 解决NSTimer在滑动时停止工作的问题
- 监控应用卡顿
- 性能优化
有关NSTimer和runloop的关系,可以参考NSTimer学习笔记
如何将子线程一直存在?(面试基本会遇到)
当然,你不能用strong指针拥有thread就算完事,那样只是拥有这个thread,但是thread可能已经失效,不能使用了。
@interface YZThread : NSThread
@end
#import "YZThread.h"
@implementation YZThread
- (void)dealloc
{
NSLog(@"%s", __func__);
}
@end
----------------------------------
ViewController.m文件
#import "ViewController.h"
#import "YZThread.h"
@interface ViewController ()
@property (strong, nonatomic) YZThread *thread;
@property (assign, nonatomic, getter=isStopped) BOOL stopped;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.stopped = NO;
__weak typeof(self) weakSelf = self;
YZThread *thread = [[YZThread alloc] initWithBlock:^{
NSLog(@"begin - %s, %@", __func__, [NSThread currentThread]);
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
while (weakSelf && !weakSelf.isStopped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
/**
[[NSRunLoop currentRunLoop] run];
相当于在执行下面的代码
while (1) {
[[NSRunLoop currentRunLoop] runMode:<#(nonnull NSRunLoopMode)#> beforeDate:<#(nonnull NSDate *)#>]
}
有个while循环,一直在调用
*/
NSLog(@"end - %s, %@", __func__, [NSThread currentThread]);
}];
self.thread = thread;
[thread start];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
if (self.thread == nil) {
return;
}
//waitUntilDone:YES 等test方法执行完毕,再执行打印123。NO 不等test方法执行完毕,就执行打印123
[self performSelector:@selector(test) onThread:self.thread withObject:self waitUntilDone:YES];
NSLog(@"123");
}
- (IBAction)stop:(id)sender {
if (self.thread == nil) {
return;
}
[self performSelector:@selector(threadStop) onThread:self.thread withObject:self waitUntilDone:YES];
}
- (void)threadStop
{
self.stopped = YES;
CFRunLoopStop(CFRunLoopGetCurrent());//停止runloop
NSLog(@"%s, %@", __func__, [NSThread currentThread]);
self.thread = nil;
}
//子线程需要做的操作
- (void)test
{
NSLog(@"%s, %@", __func__, [NSThread currentThread]);
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self stop:nil];
}
@end
简单来说就是:
在子线程中加入runloop来保证子线程存活
上面的用法能满足需求,但,可以做的更好,做下封装:
#import <Foundation/Foundation.h>
@interface YZKeepAliveThread : NSObject
/**
开启线程
*/
- (void)run;
/**执行block*/
- (void)excuteTaskWithBlock:(void(^)(void))block;
/**
结束线程
*/
- (void)stop;
@end
#import "YZKeepAliveThread.h"
@interface YZKeepAliveThread ()
@property (strong, nonatomic) NSThread *thread;
@property (assign, nonatomic, getter=isStopped) BOOL stopped;
@end
@implementation YZKeepAliveThread
- (instancetype)init
{
if (self = [super init]) {
self.stopped = NO;
__weak typeof(self) weakSelf = self;
self.thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"begin - %s, %@", __func__, [NSThread currentThread]);
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
while (weakSelf && !weakSelf.isStopped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
/**
[[NSRunLoop currentRunLoop] run];
相当于在执行下面的代码
while (1) {
[[NSRunLoop currentRunLoop] runMode:<#(nonnull NSRunLoopMode)#> beforeDate:<#(nonnull NSDate *)#>]
}
有个while循环,一直在调用
*/
NSLog(@"end - %s, %@", __func__, [NSThread currentThread]);
}];
}
return self;
}
/**
开启线程
*/
- (void)run
{
[self.thread start];
}
/**执行block*/
- (void)excuteTaskWithBlock:(void(^)(void))block
{
if (self.thread == nil || block == nil) {
return;
}
//waitUntilDone:YES 等test方法执行完毕,再执行打印123。NO 不等test方法执行完毕,就执行打印123
[self performSelector:@selector(__excuteTaskWithBlock:) onThread:self.thread withObject:block waitUntilDone:NO];
}
- (void)__excuteTaskWithBlock:(void(^)(void))block
{
block();
}
/**
结束线程
*/
- (void)stop
{
if (self.thread == nil) {
return;
}
[self performSelector:@selector(threadStop) onThread:self.thread withObject:self waitUntilDone:YES];
}
- (void)threadStop
{
self.stopped = YES;
CFRunLoopStop(CFRunLoopGetCurrent());//停止runloop
self.thread = nil;
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self stop];
}
@end
ViewController.m文件
#import "ViewController.h"
#import "YZKeepAliveThread.h"
@interface ViewController ()
@property (strong, nonatomic) YZKeepAliveThread *thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[YZKeepAliveThread alloc] init];
[self.thread run];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
__weak typeof(self) weakSelf = self;
[self.thread excuteTaskWithBlock:^{
[weakSelf test];
}];
}
- (IBAction)stop:(id)sender {
[self.thread stop];
}
//子线程需要做的操作
- (void)test
{
NSLog(@"子线程需要做的操作--%s, %@", __func__, [NSThread currentThread]);
}
- (void)dealloc
{
NSLog(@"%s", __func__);
}
@end
更多学习有关RunLoop深入理解RunLoop