什么是RunLoop
- 运行循环
- 内部其实就是do-while循环,在这个循环内部不断地处理各种任务(比如Source、Timer、Observer)
- 线程和RunLoop是一一对应的关系.一个线程对应一个RunLoop,主线程的RunLoop默认在底层启动,子线程的RunLoop必须得创建,还得调用run方法来进行启动
-RunLoop一直循环不退出,必须得有运行循环模式,并且在这个模式中得存在Source(Source0,Source1)、Timer中的任意一个
自动释放池什么时候释放
- 1)runloop启动的时候,会创建一个自动释放池
- 2)runloop退出和即将休眠的时候,会销毁自动释放池
在开发中RunLoop的应用场景
- 应用场景1:滑动与图片刷新
- 当tableview的cell上有需要从网络获取的图片的时候,滚动tableView,异步线程会去加载图片,加载完成后主线程就会设置cell的图片,但是会造成卡顿。可以让设置图片的任务在CFRunLoopDefaultMode下进行,当滚动tableView的时候,RunLoop是在 UITrackingRunLoopMode 下进行,并不会执行CFRunLoopDefaultMode模式下的任务,所以不会去设置图片,而是当停止滚动tableView的时候,才设置图片。
- 默认情况下,不滚动tableview的cell是CFRunLoopDefaultMode模式,滚动tableview的cell是 UITrackingRunLoopMode模式,这两个模式执行的任务是互不影响的。但是如果让不滚动tableview的cell变为UITrackingRunLoopMode模式,那么当我们滚动tableview的cell时,程序也会开启异步线程去加载图片,造成卡顿。
- (void)viewDidLoad {
[super viewDidLoad];
// 只在NSDefaultRunLoopMode下执行(刷新图片)
[self.myImageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@""] afterDelay:ti inModes:@[NSDefaultRunLoopMode]];
}
- 应用场景2:开启一个常驻子线程,让子线程不断处理事件
- 在子线程中创建runloop并开启runloop,而且线程中的runloop中至少添加一个source或者一个timer
- 在子线程中处理网络请求操作,图片下载操作
GCD中的定时器和OC中的定时器是否受Runloop影响
- OC中的NSTimer定时器是否工作是受Runloop的影响的.
- 例如: 定时器简单的添加到RunLoop中时,设置了默认的运行模式。当在界面上滚动textView,ScrollView,TableView时,定时器不会工作。
- 解决办法:把定时器添加到runloop中,并把runloop的运行模式为kCFRunLoopCommonModes.
- GCD中的定时器不受Runloop影响
RunLoop与线程
每条线程都有唯一的一个与之对应的RunLoop对象
主线程的RunLoop系统底层已经自动创建好了,子线程的RunLoop需要主动创建
RunLoop在第一次获取时创建,在线程结束时销毁
- Foundation框架(都是方法) 需要加*号,oc的框架
- [NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
- [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
- 例如:NSRunLoop * runloop1 = [NSRunLoop currentRunLoop];
- Core Foundation框架(都是函数) 不需要加* ,c语言的框架
- CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
- CFRunLoopGetMain(); // 获得主线程的RunLoop对象
- 例如:CFRunLoopRef runloop2 = CFRunLoopGetCurrent();
- kCFRunLoopDefaultMode等价NSDefaultRunLoopMode
和RunLoop相关的五个类
a.CFRunloopRef
b.CFRunloopModeRef【Runloop的运行模式】
c.CFRunloopSourceRef【Runloop要处理的事件源】
d.CFRunloopTimerRef【Timer事件】
e.CFRunloopObserverRef【Runloop的观察者(监听者)】
核心知识点:
b中的CFRunloopModeRef中,
Runloop要想跑起来,它的内部必须要有一个mode,这个mode里面必须有source\observer\timer,至少要有其中的一个。
01.CFRunloopModeRef代表着Runloop的运行模式
02.一个Runloop中可以有多个mode,一个mode里面又可以有多个source\observer\timer等等
03.每次runloop启动的时候,只能指定一个mode,这个mode被称为该Runloop的当前mode
04.如果需要切换mode,只能先退出当前Runloop,再重新指定一个mode进入
05.这样做主要是为了分割不同组的定时器等,让他们相互之间不受影响
06.系统默认注册了5个mode
a.kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
b.UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
c.UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
d.GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
e.kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode
OC中的定时器
(一)定时器在主线程工作
(二)定时器在子线程工作
(三)定时器在主线程工作+Runloop
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self timer2];
}
//第二种获得定时器的方法(基本)
-(void)timer2{
//1.创建定时器
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(task) userInfo:nil repeats:YES];
//2.添加到运行循环中
/*
第一个参数:定时器对象
第二个参数:runloop的运行模式
*/
//一个Runloop(运行循环)中可以有多个模式,例如:运行循环模式(NSDefaultRunLoopMode) 追踪循环模式UITrackingRunLoopMode
//kCFRunLoopDefaultMode等价于NSDefaultRunLoopMode。只不过前者是C语言,后者是OC。
//因为下面的NSRunLoop是OC语言,所以传递的参数也应为OC的参数
//2.把定时器添加到运行循环中. 运行循环:[NSRunLoop currentRunLoop].运行循环模式:NSDefaultRunLoopMode
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];//A 第一个参数:定时器对象 第二个参数:runloop的运行循环模式
//出现的问题:当手上下拖动手机上的textView时候,默认运行循环模式切换为了追踪运行循环模式.因为之前的定时器所处的模式是默认运行循环模式,所以并不会在追踪运行循环模式模式调用task方法.
//所以就造成了当程序开始运行的时候,定时器每隔两秒钟调用task方法,当手上下拖动textView的时候,定时器不再工作,即不会调用task方法。当手指停止在textView上拖动时,定时器又保持每隔两秒钟调用task方法
//3.把定时器添加到运行循环中. 运行循环:[NSRunLoop currentRunLoop].运行循环模式:UITrackingRunLoopMode.
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];//B A+B可以解决出现的问题,相当于设置了两个运行循环模式
}
-(void)task
{
NSLog(@"task---%@--%@",[NSThread currentThread],[NSRunLoop currentRunLoop].currentMode);
}
演示
-
遇到的问题:
- 解决办法:
(四)定时器在主线程工作+Runloop
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self timer3];
}
//第二种获得定时器的方法(拓展)
-(void)timer3{
//1.创建定时器
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(task) userInfo:nil repeats:YES];
/*
在touchesgegan中打印NSLog(@"%@",[NSRunLoop mainRunLoop]);结果如下:
-----------------------------------------------------------------
common modes = <CFBasicHash 0x7fcf995011e0 [0x103fb77b0]>{type = mutable set, count = 2,
entries =>
0 : <CFString 0x104eb3270 [0x103fb77b0]>{contents = "UITrackingRunLoopMode"}
2 : <CFString 0x103fd7b60 [0x103fb77b0]>{contents = "kCFRunLoopDefaultMode"}
}
-----------------------------------------------------------------
从打印结果可知:
被打印成common modes标签的有两个模式,一个是kCFRunLoopDefaultMode,另一个是UITrackingRunLoopMode.
所以NSRunLoopCommonModes等价于UITrackingRunLoopMode加上kCFRunLoopDefaultMode
可以把定时器添加到NSRunLoopCommonModes中,就可以解决手上下拖动textView时,定时器不工作的问题,并且仅需一行代码.
*///一个Runloop(运行循环)中可以有多个模式,例如:运行循环模式(NSDefaultRunLoopMode) 追踪循环模式UITrackingRunLoopMode
//2.把定时器添加到运行循环中. 运行循环:[NSRunLoop currentRunLoop].运行循环模式:NSRunLoopCommonModes
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
-(void)task
{
NSLog(@"task---%@--%@",[NSThread currentThread],[NSRunLoop currentRunLoop].currentMode);
}
演示
(五)定时器在子线程工作+Runloop
GCD中的定时器
- GCD的定时器不会受到runloop的影响,即,程序运行时,你执行别的操作也行
- 队列决定了定时器在哪个线程工作(前提条件:在没有同步函数和异步函数参与的情况下)
- 参数为主队列,就在主线程中执行
- 参数为其他的任何队列(全局队列,全局并发队列,串行队列),都在子线程执行
CFRunLoopObserverRef
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self timer3];
[self observer];
}
-(void)observer
{
//1.创建监听者
/*
第一个参数:分配存储空间
第二个参数:要监听runloop的什么状态
第三个参数:持续监听 == YES
第四个参数:传0
*/
CFRunLoopObserverRef observer =CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(),kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
//当runloop的状态发生改变时候,就执行这个block块里面的内容
/*
kCFRunLoopEntry = (1UL << 0), runloop启动
kCFRunLoopBeforeTimers = (1UL << 1), runloop即将处理定时器事件
kCFRunLoopBeforeSources = (1UL << 2), runloop即将处理source事件
kCFRunLoopBeforeWaiting = (1UL << 5), runloop即将休眠
kCFRunLoopAfterWaiting = (1UL << 6), runloop被唤醒
kCFRunLoopExit = (1UL << 7), 退出
kCFRunLoopAllActivities = 0x0FFFFFFFU
*/
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"runloop启动");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"runloop即将处理定时器事件");
break;
case kCFRunLoopBeforeSources:
NSLog(@"runloop即将处理source事件");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"runloop即将休眠");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"runloop被唤醒");
break;
case kCFRunLoopExit:
NSLog(@"runloop退出");
break;
default:
break;
}
});
//2.设置监听的runloop和运行模式
/*
第一个参数:runloop对象
第二个参数:监听者
第三个参数:runloop的运行模式
第四个参数:
*/
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
//3.释放监听者
//因为监听者是通过Core Foundation框架创建出来的,必须释放监听者
CFRelease(observer);
}
-(void)timer3{
//1.创建定时器
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(task) userInfo:nil repeats:YES];
//2.把定时器添加到运行循环中. 运行循环:[NSRunLoop currentRunLoop].运行循环模式:NSRunLoopCommonModes
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
-(void)task
{
NSLog(@"task---%@--%@",[NSThread currentThread],[NSRunLoop currentRunLoop].currentMode);
}
@end
runloop被唤醒的时候就是要做一些事
2016-03-18 01:05:39.733 08-掌握-NSRunLoop相关类[9957:450017] runloop被唤醒
2016-03-18 01:05:39.734 08-掌握-NSRunLoop相关类[9957:450017] task---<NSThread: 0x7fa739f026c0>{number = 1, name = main}--kCFRunLoopDefaultMode
2016-03-18 01:05:39.734 08-掌握-NSRunLoop相关类[9957:450017] runloop即将处理定时器事件
2016-03-18 01:05:39.734 08-掌握-NSRunLoop相关类[9957:450017] runloop即将处理source事件
2016-03-18 01:05:39.734 08-掌握-NSRunLoop相关类[9957:450017] runloop即将休眠
2016-03-18 01:05:41.733 08-掌握-NSRunLoop相关类[9957:450017] runloop被唤醒
2016-03-18 01:05:41.734 08-掌握-NSRunLoop相关类[9957:450017] task---<NSThread: 0x7fa739f026c0>{number = 1, name = main}--kCFRunLoopDefaultMode
2016-03-18 01:05:41.734 08-掌握-NSRunLoop相关类[9957:450017] runloop即将处理定时器事件
2016-03-18 01:05:41.734 08-掌握-NSRunLoop相关类[9957:450017] runloop即将处理source事件
2016-03-18 01:05:41.734 08-掌握-NSRunLoop相关类[9957:450017] runloop即将休眠
2016-03-18 01:05:43.734 08-掌握-NSRunLoop相关类[9957:450017] runloop被唤醒
2016-03-18 01:05:43.734 08-掌握-NSRunLoop相关类[9957:450017] task---<NSThread: 0x7fa739f026c0>{number = 1, name = main}--kCFRunLoopDefaultMode
2016-03-18 01:05:43.734 08-掌握-NSRunLoop相关类[9957:450017] runloop即将处理定时器事件
2016-03-18 01:05:43.735 08-掌握-NSRunLoop相关类[9957:450017] runloop即将处理source事件
2016-03-18 01:05:43.735 08-掌握-NSRunLoop相关类[9957:450017] runloop即将休眠
代码模拟runloop的死循环(超级经典)
- source1 是基于端口的事件(系统触发的事件, source0是基于非端口的事件(用户主动调用的事件(就是方法,例如[self timer3]))
- RunLoop先通知Observer观察者,RunLoop先处理Timer,再处理Source0,再处理Source1,最后休眠,如果在休眠时,外界有事件发生(7左边三个),那么RunLoop将被唤醒,进而处理外界的事件,最后再跳回(2),如此往复执行。
RunLoop处理逻辑-网友整理版
RunLoop处理逻辑-官方版
RunLoop处理逻辑-官方版
- 输入源和定时器源中的事件都是通过RunLoop(运行循环,黄色的环)传递到线程中,让线程中的4个方法处理对应的事件
- 输入源(Input source):Port、perforSelector:onThread: 、Custom
- Port让线程中的handlePort:处理
- perforSelector:onThread:让线程中mySelector:处理
- Custom让线程中customSrc:处理
- 定时器源:Timer source
- 让线程中timerFired:处理
注意
让子线程不死,变成常驻线程 ,那么必须创建子线程的runloop并开启runloop,而且线程中的runloop中至少添加一个source或者一个timer,才不会让线程不死,才不会让runloop退出。以下列出两种做法:
-
做法1:
- 1.创建子线程对应的runloop.
- 2.添加source
- 3.子线程对应的runloop还需要手动开启
-(void)task1{ NSLog(@"%s-start--%@",__func__,[NSThread currentThread]); //1.创建子线程对应的runloop NSRunLoop *runloop = [NSRunLoop currentRunLoop]; //2.添加timer NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(timerRun) userInfo:nil repeats:YES]; [runloop addTimer:timer forMode:NSDefaultRunLoopMode]; //3.子线程对应的runloop还需要手动开启 [runloop run];
}
```
-
做法2:
- 1.创建子线程对应的runloop.
- 2.添加timer
- 3.子线程对应的runloop还需要手动开启
-(void)task1{
NSLog(@"%s-start--%@",func,[NSThread currentThread]);
//1.创建子线程对应的runloop
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
//2.添加source
[runloop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
//3.子线程对应的runloop还需要手动开启
[runloop run];
}
```
具体代码:
#import "ViewController.h"
@interface ViewController ()
/** 线程对象*/
@property (nonatomic ,strong) NSThread *thread;
@end
@implementation ViewController
//子线程(a中的thread)只要执行完任务它的task1,thread就会进入死亡状态,即使 用强指针引用着局部变量thread也不能阻止
//不让thread进入死亡状态的唯一方法就是对task1方法做手脚,不让task1任务执行完,thread就不会进入死亡状态
//具体做法:在task1方法中创建子线程(thread)的运行循环(runloop),并启用runloop。但是runloop要跑起来,必须得有一个运行循环模式(默认是NSDefaultRunLoopMode),而运行循环模式中还必须得有source\observer\timer其中一个,如果没有source\observer\timer其中一个,那么runloop就会退出,并不会循环下去。所以1,2,3缺一不可.
- (IBAction)createBtnClick:(id)sender{//主线程中的方法
//创建子线程
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(task1) object:nil];//a
//开始执行
[thread start];
//NSThread类型的强指针强引用着thread,不让thread销毁
self.thread = thread;
}
-(void)task1//子线程中的方法
{
NSLog(@"--------------");//x
//1.创建子线程对应的runloop
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
//2.添加source或者是timer
//方法1:添加timer(2行代码)
//点击createBtnClick按钮,每隔两秒就执行timerRun方法,从打印可知,形成了死循环,所以y中的内容一直不会打印.所以task1方法永远不会执行完。达到了子线程thread不会进入死亡状态。这样goOnBtnClick按钮就能拿到强指针引用着的,并且是没有死亡的thread,近而就能执行task2方法,如果thread死亡了,那么执行task2就会崩掉。
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(timerRun) userInfo:nil repeats:YES];
[runloop addTimer:timer forMode:NSDefaultRunLoopMode];
//方法2:添加source(1行代码)
//点击createBtnClick按钮,只会打印x,不会打印y,说明在x和y之间肯定有一个死循环。并且点击goOnBtnClick程序会打印task2的内容,更加验证了在x和y之间肯定有一个死循环,促使task1永远不会执行完,这样task1的线程thread就不会被销毁
//[runloop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
//3.子线程对应的runloop还需要手动开启
[runloop run];
NSLog(@"%s---%@",__func__,[NSThread currentThread]);//y
}
- (IBAction)goOnBtnClick:(id)sender//主线程中的方法
{
//只要是performSelector: onThread的这种形式一定涉及到了线程间通信。详细看线程间通信的两个方法Day26
NSLog(@"--goOn---");
//让强指针引用者的self.thread这个子线程调用task2方法
//调用前提:self.thread必须保证他里面的task1方法没有执行完,这样子线程thread才不会被销毁。如果被销毁了,程序就会崩掉
[self performSelector:@selector(task2) onThread:self.thread withObject:nil waitUntilDone:YES];//子线程
}
-(void)task2
{
NSLog(@"%s---%@",__func__,[NSThread currentThread]);
}
-(void)timerRun
{
NSLog(@"%s---%@",__func__,[NSThread currentThread]);
}
@end
runloop的自动释放池
/*
打印NSLog(@"%@",[NSRunLoop mainRunLoop]);可知:
runloop的自动释放池有两种状态: activities = 0xa0 -->(160=128+32),activities = 0x1 -->(1)
一个肯定是创建时的状态,另一个肯定是销毁时的状态。
runloop本身是有肯多状态的。选中CFRunLoopActivity进入文档,如下
kCFRunLoopEntry = (1UL << 0), 1(枚举值) runloop启动
kCFRunLoopBeforeTimers = (1UL << 1), 2 runloop即将处理定时器事件
kCFRunLoopBeforeSources = (1UL << 2), 4 runloop即将处理source事件
kCFRunLoopBeforeWaiting = (1UL << 5), 32 runloop即将休眠
kCFRunLoopAfterWaiting = (1UL << 6), 64 runloop被唤醒
kCFRunLoopExit = (1UL << 7), 128 退出
通过动释放池有两种状态的值和runloop本身的状态值比对,可以得到如下2个总结:
1)runloop启动的时候,会创建一个自动释放池【根据:自动动释放池二进制值为1】
2)runloop退出和即将休眠的时候,会销毁自动释放池【根据:自动释放池二进制值为160=128+32】
*/
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%@",[NSRunLoop mainRunLoop]);
//十六进制转换成十进制
NSLog(@"%d---%d",0x1,0xa0);
}
@end