Runloop:运行循环-死循环
主要目的:提高性能,有事情就干,没事情休眠。
参考https://blog.csdn.net/callauxiliary/article/details/107419854
主要应用
1,保证线程一直运行,处理事件,比如触摸事件,时钟事件,都是由runloop完成。
2,优化卡顿:将一次runloop执行完的任务,放到多次runloop中执行。
3,UI滑动时计时不准确的问题,设置定时器的Mode为:NSRunLoopCommonModes。
4,需要在线程上使用performSelector*****方法(运行时方法)。例如
让UITableView、UICollectionView等延迟加载图片
[imageView performSelector:@selector(setImage:) withObject:image afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
这算一个优化点,这里用的defaultMode,就是滚动的时候不加载图片,停止滚动加载图片
Runloop运行原理
当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。每次线程运行RunLoop都会自动处理之前未处理的消息,并且将消息发送给观察者,让事件得到执行。
Runloop的生命周期:在第一次获取时创建,在线程结束时销毁。
Runloop要想跑起来,它的内部必须要有一个mode,这个mode里面必须有source\observer\timer,至少要有其中的一个。
系统默认注册了5个mode
a.kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
b.UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
c.UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
d.GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
e.kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode
RunLoop运行时首先根据modeName找到对应mode,如果mode里没有source/timer/observer,直接返回。
主要分为三大步骤:
1、首先根据modeName找到对应的Mode
2、如果model里没有Source/Timer/Observer,直接返回
3、如果model有Source/Timer/Observer,就会即将进入runloop
详细步骤
简单来讲就是一个do-while
循环,有输入源就唤醒,没有就处于休眠状态,官方解释链接。具体过程如下:
- 通知观察者开始进入 Runloop
- 通知观察者开始处理 Timer 事件
- 通知观察者将要处理非基于 port 的事件
- 启动准备好的事件
- 如果基于 port 的事件已经准备好,立即启动。并进入步骤 9
- 通知观察者进入休眠状态
- 线程进入休眠直到以下事件出现
- 基于 port 的事件源出现
- 定时器启动
- 设置的时间已经超时
- RunLoop 被唤醒
- 通知观察者线程将被唤醒
- 处理已经进入的事件
- 如果用户自定义的计时器启动,处理事件并重启 Runloop。转到第二步
- 输入源启动,传递消息
- 如果 Runloop 被显示唤醒且没有超时,重启 Runloop。转到第二步
- 通知观察者 Runloop 已经退出
Runloop组成
两种对象:NSRunLoop 和 CFRunLoopRef。
CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
RunLoop共包含5个类,但公开的只有Source、Timer、Observer相关的三个类。
1,Timder,时钟
2,source,事件源:一切事件的来源,按照函数的调用栈分为source0:非系统内核事件,source1:系统内核事件。
3,observer,观察者,观察的runloop的循环周期。
苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。
APP启动过程
1,当点击APP时,操作系统开启一条线程执行程序的main函数。这个线程就是这个程序的主线程。这个线程是一个常驻线程,因为这个线程的Runloop被开启了,不会被线程池释放。
线程和runloop 的关系
线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)
Runloop的作用
1,保证线程不退出。
2,监听所有的事件,比如触摸事件,时钟事件,网络事件等。
子线程要监听事件,就必须开启子线程的runloop。
验证将时钟添加到runloop才可以实现监听
1,scheduledTimerWithTimeInterval初始化方法默认是添加到当前线程的runloop的,所以不用显示的添加。
这种方法添加是NSDefaultRunLoopMode模式,UI事件会中断响应。
-(void)test{
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeerFunC) userInfo:nil repeats:YES];
}
-(void)timeerFunC{
static int num;
NSLog(@"%d",num);
num++;
}
3,子线程结束runloop
-(void)test{
//要放在子线程,设置runloop时间,时间过了没有执行的任务,子线程就会结束,并释放
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timeerFunC) userInfo:nil repeats:YES];
//将时钟手动添加到当前的runloop,否者时钟方法不会执行
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
//当为yes时,取消RunLoop循环
while (!_finish) {
NSLog(@"来了11");
[[NSRunLoop currentRunLoop]runUntilDate:[NSDate dateWithTimeIntervalSinceReferenceDate:1]];
}
NSLog(@"来了");
});
}
-(void)timeerFunC{
static int num;
NSLog(@"%d",num);
num++;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"22222");
_finish = YES;
}
2,timerWithTimeInterval初始化方法需要手动将时钟添加到当前线程的runloop中,否则时钟不能被监听。
-(void)test{
NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timeerFunC) userInfo:nil repeats:YES];
//将时钟手动添加到当前的runloop,否者时钟方法不会执行
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
}
-(void)timeerFunC{
static int num;
NSLog(@"%d",num);
num++;
}
runloop的三种Mode
1,当对象使用NSDefaultRunLoopMode(默认模式,一般处理网络事件和Timer事件),在runloop执行UI事件的时候,会暂停响应该对象的事件。
2,当对象使用UITrackingRunLoopMode(UI模式,一般处理UI事件),只有在runloop执行UI事件的时候,才会会暂停响应该对象的事件。
3,当对象使用NSRunLoopCommonModes(占位模式,这个不是一个真的模式,只是同时添加了上面两种模式),在runloop执行UI事件的时候,也会响应该对象的事件(最理想的结果)。
开启当前线程的runloop
子线程处理需要监听的事件(需要子线程活着一直监听的情况),可以启动子线程的runloop,让子线程成为常驻线程,避免被释放,从而可以响应监听的事件。唯一让线程不被释放的方法就去启动他的runloop。
解决NSTimer在滑动时停止工作的问题的办法
1,设置定时器的Mode为:NSRunLoopCommonModes
2,将NSTimder放到子线程并且开启runloop,name就是算是NSDefaultRunLoopMode,滑动UI也不会影响Timer。
NSTimder处理的事件一般放在子线程,并开启子线程的runloop,避免影响主线程处理事件。
-(void)test{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timeerFunC) userInfo:nil repeats:YES];
//将时钟手动添加到当前的runloop,否者时钟方法不会执行
[[NSRunLoop currentRunLoop]addTimer:timer forMode: NSDefaultRunLoopMode ];
//手动开启当前线程的runloop,这是一个死循环
[[NSRunLoop currentRunLoop]run];
NSLog(@"这里不会走了");
});
}
-(void)timeerFunC{
static int num;
NSLog(@"%d",num);
num++;
}
GCD实现定时器
Dispatch Source创建定时器timer,优于NSTimer
必须要声明timer属性强应用,不然会被释放
@property (nonatomic, strong) dispatch_source_t timer;
-(void)test2{
NSLog(@"启动");
_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 * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(_timer, ^{
NSLog(@"间隔一秒执行%@",[NSThread currentThread]);
});
dispatch_resume(_timer);
NSLog(@"启动");
}
优化练习
tableview在快速滑动时,高清图片造成卡顿优化。
原因:tableview在快速滑动时,runloop必须在一次循环内,渲染所有的图片。
思路:runloop每次循环只加载一张图片,例如一个屏幕总的可以显示15张图片,那么就分15次加入到runloop中加载。相当于把一次做完的事情,分成了15次。例如监听kCFRunLoopBeforeWaiting事件,回调函数加载图片,加载完成后删除这个任务。每次加载图片后都会再次到达kCFRunLoopBeforeWaiting,还有图片任务就会继续执行。
通过CFRunLoopObserverRef监听runloop 的状态,函数指针回调,Ref是引用,指针的意思。
步骤
1,添加runloop观察者,观察runloop 的状态变化。
2,将加载图片的代码块放到数组中。
3,在观察者回调中,拿出数组中加载图片的代码执行。