Runloop
- 多线程编程指南
- 资料:
- 1.开源网址中下载CF开头的包,CF是CoreFoundation的缩写,CFRnLoop.c是实现文件
- 2.官方文档
Runloop与线程
- 每条线程都有唯一的一个与之对应的Runloop对象
- CFRunLoopGet0(pthread_t t)
- 主线程的Runloop已经创建好了,子线程的Runloop需要主动创建
- Runloop在第一次获取时创建,在线程结束的时候销毁
获得Runloop对象
- Foundation(OC)
- [NSRunloop currentRunloop]
- [NSRunloop mainRunloop]
- Core Foundation(C)
- CFRunLoopRef
- CFRunLoopGetCurrent();
- CFRunLoopGetMain();
- 转换OC-C
- mainRunloop.getCFRunLoop
- RunLoop和线程的关系 一一对应
- [NSThread detachNewThreadSelector:toTarget:withObject:]
- 主线程对应的RunLoop默认已经创建了,子线程对应的runloop需要我们手动创建
- [NSRunloop currentRunloop]是懒加载的,只会初始化一次,会判断当前线程对应的runloop是否存在,如果存在,直接返回,如果不存在会创建一个,返回给我们,该方法就是用来创建Runloop的。
Runloop相关类
-
CoreFoundation中关于RunLoop的五个类:
- CFRunLoopRef
- CFRunLoopModeRef:运行模式
- CFRunLoopSourceRef:事件源、输入源
- CFRunLoopTimerRef:定时器事件
- CFRunLoopObserverRef:监听者、观察者
-
关系:
- Runloop要启动必须要选择一种运行模式,有多种运行模式可供选择,但只能选择一种
- 运行模式里面有source/observer/timer,runloop启动之前会检查运行模式是否为空(怎么判断是否为空?检查有没有source和timer,如果一个source和timer都没有就为空,如果为空,runloop启动之后就退出了)
- 开启runloop运行循环-死循环
-
运行模式:CFRunloopModeRef
- CFRunLoopRef代表RunLoop的运行模式
- 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source/Timer/Observer
- 每一次Runloop启动时,只能指定其中一个Mode,这个Mode被称作currentMode
- 如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入
- 这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响
CFRunLoopModeRef
- 系统默认注册了5个Mode
- kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
- UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响
- UIInitializationRunLoopMode:在刚启动App时进入第一个Mode,启动完成后就不再使用
- GSEventReceiveRunLoopMode:接受系统事件的内部Mode,通常用不到
- KCFRunLoopCommonModes:这是一个占位用的Mode,不是一种真正的Mode
CFRunLoopModeRef和NSTimer
- 定时器使用
- 方法一:[NSTimer scheduledTimerWithTimerInterval:target:selector:userInfo:repeats:]
- 该方法内部会自动将创建的定时器对象添加到当前的runloop当中,并且指定runloop的运行模式为默认
- 开一个子线程调用这个方法,这个定时器能工作吗?
- 不能工作!!!子线程对应的runloop没有创建
- 手动创建runloop,手动添加定时器,还是不可以!!
- 方法一:[NSTimer scheduledTimerWithTimerInterval:target:selector:userInfo:repeats:]
-
注意
:- 子线程对应的runloop需要手动的创建
- 子线程创建的runloop还需要手动的开启 [currentRunLoop run]
- 方法二:[NSTimer timerWithTimerInterval:target:selector:userInfo:repeats:]
- 使用这个方法需要把定时器添加到runloop中
- [NSRunloop currentRunloop] addTimer:forMode:
- 问题:拖拽textField的时候,定时器不工作的原因?
- 滚动textField,runloop运行模式改变了,切换到了UITrackingMode
- 问题:怎么让定时器,在滚动textField的时候,也能工作?
- 方法一:把定时器对象添加到runloop中,并且制定runloop的运行模式为默认,只有runloop的运行模式为NSDefaultRunloopMode的时候定时器才工作,要求在滚动textField的时候定时器也能工作,并且指定runloop的运行模式为tracking的时候,定时器才工作
- 方法二:占位模式(等价于上面的两行代码)NSRunLoopCommonModes = Default & Tracking
- NSRunLoopCommonModes表示把定时器对象添加到runloop中并且指定运行模式为Default或者是Tracking
GCD中的定时器(掌握)
- 第一个函数:创建定时器对象
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE__TIMER,0,0,dispatchQueue)
- 第一个参数:要创建的是一个定时器
- 第二个参数:默认总是传0,描述信息
- 第三个参数:默认总是传0,
- 第四个参数:队列,并发队列(全局),决定代码块(dispatch_source_set_event_handler)在哪个线程中调用(主队列+主线程)
- 第二个函数:设置定时器
dispatch_source_set_timer(timer,DISPATCH_TIME_NOW,intervalInSeconds *NSEC_PER_SEC,leewayInSeconds *NSEC_PER_SEC)
- 第一个参数:定时器对象
- 第二个参数:定时器开始计时的时间(开始时间)
- 怎么修改开始时间?
- dispatch_time_t t = dispatch_time(DISPATCH_TIME_NOW,延迟时间2.0*NSEC_PER_SEC)
- 第三个参数:设置间隔时间 GCD的时间单位:纳秒
- 第四个参数:精准的,0表示绝对精准
- 第三个函数:在block块里面要执行的任务,GCD定时器时间到了之后要执行的任务
dispatch_source_set_event_handler(timer,^{})
-
第四个函数:恢复执行dispatch_resume(timer)
- 默认是暂停状态,需要手动开启
-
定时器不工作的原因:
- GCD是不会受到runloop的影响
- 原因是timer被释放了
- 定一个属性,让timer不释放
RunloopSourceRef
- CFRunloopSourceRef:是事件源(输入源)
- 以前的分类:
- Port-Based Source 基于端口
- Custom Input Sources 自定义
- Cocoa Perform Selector Sources
- 现在的分类:
- Source0:非基于Port
- Source1:基于Port
- 根据函数调用栈进行分类的
RunloopObserverRef
- 01.创建一个监听者
-
CFRunloopObserverRef observer = CFRunLoopObserverCreateWithHandler()
- 第一个参数,分配存储空间
- CFAllocatorGetDefault()
- 第二个参数:要监听的状态 0
- 第三个参数:是否要持续监听YES
- 第四个参数:和优先级相关 0
- block块:当发现监听对象状态改变的时候调用该block
- KCFRunLoopEntry:runloop进入启动
- BeforeTimers:runloop即将处理timer事件
- BeforeSources: runloop即将处理source事件
- beforeWaiting:runloop即将进入睡眠状态
- afterWaiting: runloop被唤醒
- exit:runloop即将退出
- AllActivities:监听所有的活动状态
- 第一个参数,分配存储空间
只能使用C语言代码
-
- 02.设置监听
- CFRunLoopAddObserver()
- CFRunloopRef,要监听的runloop对象
- CFRunLoopObserverRef,监听者对象本身
- CFStringRef:runloop的运行模式
- NSDefaultRunLoopMode(OC)
- KCFRunLoopDefaultMode(C)✔️
- CFRunLoopAddObserver()
runloop的运行流程
- 处理逻辑(网友整理)
- 1.通知内部Observer:即将进入Loop
- 2.通知Observer:将要处理Timer
- 3.通知Observer:将要处理Source0
- 4.处理Source0
- 5.如果有Source1,跳到第九步
- 6.通知Observer:线程即将休眠
- 7.休眠等待唤醒
- 8.通知Observer:线程刚被唤醒
- 9.处理唤醒时收到的消息,之后跳回2
- 10.通知Observer:即将退出Runloop
- 处理逻辑(官方)
- Runloop的事件队列
- 每次运行runloop,线程的runloop会自动处理之前未处理的消息,并通知相关的观察者,具体的顺序如下:
- 1.通知观察者runloop已经启动
- 2.通知观察者任何即将要开始的定时器
- 3.通知观察者任何即将启动的非基于端口的源
- 4.启动任何准备好的的非基于端口的源
- 5.如果基于端口的源准备好并处于等待状态,立即启动,并进入步骤9
- 6.通知观察者线程进入休眠
- 7.将线程置于休眠直到下面的任意一个事件发生
- 某一时间到达基于端口的源
- 定时器启动
- Runloop设置的时间已经超时
- runloop被显示唤醒
- 8.通知观察者线程将被唤醒
- 9.处理未处理的事件
- 如果用户定义的定时器启动,处理定时器事件并重启runloop,进入步骤2
- 如果输入源启动,传递相应的消息
- 如果runloop被显示唤醒而且时间还没超时,重启runloop,进入步骤2
- 10.通知观察者runloop结束
代码模拟runloop死循环
void msg(int n)
{
NSLog(@"runloop被唤醒");
NSLog(@"runloop处理%zd事件",n);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"runloop启动了");
do {
NSLog(@"runloop即将处理timer事件");
NSLog(@"runloop即将处理source0事件");
NSLog(@"source1事件");
NSLog(@"runloop询问:还有事件需要我处理吗?");
NSLog(@"runloop计入到休眠状态");
int number = 0;
scanf("%zd",&number);
msg(number);
} while (1);
}
return 0;
}
runloop的应用
- NSTimer
- ImageView显示
- PerformSelector
- selecter事件和runloop的关系
- performSelector:withObject:afterDelay:inModes:
- @[NSDefaultRunLoopMode,UITrackingRunLoopMode]
- @[NSRunLoopCommonModes]
- 默认情况下,selector事件是被添加到当前的runloop中执行的,并且指定了运行模式为默认
- performSelector:withObject:afterDelay:inModes:
-(void)test1
{
[NSThread detachNewThreadSelector:@selector(task) toTarget:self withObject:nil];
}
-(void)task
{
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"Snip20160713_9"] afterDelay:3.0];
//运行模式启动之后,判断有一个selector事件,runloop才能够开启
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
[runloop run];
NSLog(@"+++++");
}
//需要进行线程间通信,否则会报错
- 常驻线程
- 开一条子线程,让子线程一直工作
- 获得当前线程对应的runloop对象
- 为runloop添加input source 或者是timer source
- 启动runloop
//创建线程,执行任务
- (IBAction)createNewThreadBtnClick:(id)sender {
//01 创建线程,执行任务
//因为要拿到线程对象,所以用NSThread创建线程
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(run1) object:nil];
//02 执行任务
[thread start];
self.thread = thread;
}
//继续执行任务
- (IBAction)goOnBtnClick:(id)sender {
/*
self.thread 任务执行完毕,已经进入到死亡状态|但是还没有被释放
程序崩溃报错:attempt to start the thread again
[self.thread start];❎不能尝再次开启线程
怎么让任务执行不完呢?在run1方法里搞一个死循环?这样做线程不会死,但是不会执行任务2
正确做法:在run1方法里面开启一个runloop
*/
//让之前创建的线程继续执行
[self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:YES];
}
-(void)run1
{
NSLog(@"run1---%@",[NSThread currentThread]);
/*
do {
NSLog(@"-%@",[NSThread currentThread]);
} while (1);
*/
//001 获得当前线程对应的runloop对象
NSRunLoop *currentRunloop = [NSRunLoop currentRunLoop];
//002 为runloop添加input soucre或者是timer souce ,为了让运行模式不为空,否则不能开启runloop
//基于端口的事件
//好处:不需要调用方法,开发中常用
[currentRunloop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
//Perform事件
//存在的问题:3.0s之后就退出了,开发中一般不用selector方法
//[self performSelector:@selector(run3) withObject:nil afterDelay:3.0];
//NSTimer事件
//[NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
//003 启动runloop
//runUntilDate |run 内部都指定了运行模式为默认
[currentRunloop run];
//[currentRunloop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10000000]];
NSLog(@"_______end____");//验证runloop是否开启了,打印(没有开启runloop)
}
-(void)run2
{
NSLog(@"run2---%@",[NSThread currentThread]);
}
-(void)run3
{
NSLog(@"run3---%@",[NSThread currentThread]);
}
-(void)timerTest
{
NSLog(@"timer---");
}
runloop常见面试题
-
什么是runloop?
- 从字面意思看:运行循环、跑圈
- 其实它内部就是do while循环,在这个循环内部不断地处理各种任务(比如Source/Timer/Observer)
- 一个线程对应一个Runloop,主线程的runloop默认已经启动,子线程的runloop需要手动启动(调用run方法)
- Runloop只能选择一个mode启动,如果当前mode中没有任何source(source0/source1)/timer,那么就直接退出runloop
-
自动释放池什么时候释放?
- 通过observer监听runloop的状态
- 自动释放池的创建和释放
- 创建:runloop启动的时候
- 销毁:runloop退出的时候
- 其他时候的创建和销毁:当runloop即将进入到休眠的时候会把之前的释放池销毁,重新创建一个新的自动释放池
-
在开发中如何使用runloop?应用场景?
- 开启一个常驻线程(让一个子线程不进入消亡状态,等待其他线程发来消息,处理其他事件)
- 在子线程中开启一个定时器
- 在子线程中进行一些长期监控
- 可以控制定时器在特定模式下执行
- 可以让某些事件(行为、任务)在特定模式下执行
- 可以添加Observer监听Runloop的状态,比如监听点击事件的处理(在所有点击事件之前做一些事情)
- 开启一个常驻线程(让一个子线程不进入消亡状态,等待其他线程发来消息,处理其他事件)
总结:
-
多图下载
- 缓存策略:内存缓存+磁盘缓存
- 开子线程下载
-
SDWebImage
- 基本使用
- 结构
- caches
- download
- manager
- 细节
-
Runloop
- 概念:运行循环(死循环)
- 作用【3】
- 保持程序的持续运行
- 处理app中的各种事件
- 提高性能
- 与线程关系:一一对应(字典)
- 线程与runloop是一一对应的关系(字典)
- 主线程对应的runloop已经创建并且启动mainRunloop
- 子线程对应的runloop需要手动创建并且开启
- 退出:线程销毁
- runloop的相关类
- runloopRef runloop本身
- runloopModeRef 运行模式[5]
- 默认Default
- 界面追踪Tracking
- commonModes
- runloopTimeRef
- observer 监听者
- sourceRef 事件源