本篇主要是介绍 RunLoop在Core Foundation中相关的5个类。
在此之前,先来回顾一下上一篇中介绍到的一些重要的内容:
- 线程与RunLoop之间的关系
每条线程都有唯一的一个与之对应的RunLoop对象
主线程的RunLoop已经自动创建好了,子线程的RunLoop需要手动创建>>
RunLoop在第一次获取是创建,在线程结束是销毁,该做事的时候做事,该休息就休息
- RunLoop底层的创建和保存方式
RunLoop在C层面上其实是以一个线程为参数来的创建,
pthread_t
- 使用
CFMutableDictionaryRef
来创建一个可变字典- 创建
CFRunLoopCreate(pthread_t)
来创建一个RunLoop- 使用字典以线程为key,runloop为value进行保存
- 获取RunLoop对象和转换
//获取主线程对应的RunLoop NSRunLoop * mainRunLoop = [NSRunLoop mainRunLoop]; //获取当前对应的RunLoop NSRunLoop * currentRunLoop = [NSRunLoop currentRunLoop]; NSLog(@"%p --- %p", mainRunLoop, currentRunLoop); //获取主线程对应的RunLoop CFRunLoopRef mainRunLoopf = CFRunLoopGetMain(); //获取当前对应的RunLoop CFRunLoopRef currentRunLoopf = CFRunLoopGetCurrent(); NSLog(@"%p --- %p", mainRunLoopf, currentRunLoopf); //转C的RunLoop对象 使用 mainRunLoop.getCFRunLoop NSLog(@"%p --- %p", mainRunLoop.getCFRunLoop, mainRunLoopf);
- 注意:
1. currentRunLoop在没有创建子线程的时候他获取到的就是和>>mainRunLoop是一致的。
2. 获得子线程的RunLoop,currentRunLoop 该方法本身是一个懒加载,>>如果是第一次调用,则会创建当前线程对应的RunLoop并保存,以后调用>>则直接获取
进入本文的主要内容,先把之前关于的RunLoop的官方图拿进来
一、 Core Foundation中关于RunLoop的5个类
- CFRunLoopRef --- RunLoop本身类
- CFRunLoopModeRef ------- RunLoop的运行模式类
- CFRunLoopSourceRef ------- RunLoop的事件的类,对应的是事件源中的InPut Sources
- CFRunLoopTimerRef ------- RunLoop的定时器事件的类,对应的是事件源中的Timer Sources
- CFRunLoopObserverRef ------- RunLoop的监听状态的类
二、CFRunLoopModeRef (RunLoop的运行模式)
如上图所示,RunLoop有三个甚至更多的运行模式,那么塔启动的时候需要选择一种运行模式,然后判断这种运行模式是否为空。
判断的依据是
1.source中是否有事件、Timer中是否有事件
2. 如果source和Timer中一个事件都没有则为空,那么RunLoop将退出
3. 如果source和Timer中有一个或者多个事件,则启动运行循环
值得注意的是:Observer观察者并不会参与 判断运行模式是否为空,也就是说运行模式判读是否为空跟Observer没什么关系
CFRunLoopModeRef的说明:
一个RunLoop包含若干个Model,每个Mode有包含若干个Source/Timer/Observer
每次RunLoop启动的时候,只能选定其中的一个Mode,这个Mode称之为CurrentMode
如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入,这样的目的是为了隔开不同组的Source/Timer/Observer ,让他们互不影响
系统默认注册了5个Mode:
-
kCFRunLoopDefaultMode
(App的默认Mode,通常主线程是在这个Mode下运行的) -
UITrackingRunLoopMode
(界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动的时候不受其他Mode的影响) - UIInitalizationRunLoopMode (在刚启动App时进入的第一个Mode,启动完成以后就不再使用)
- GSEventReceiveRunLoopMode (接受系统事件的内部Mode,通常用不到)
- kCFRunLoopCommonModes (这是一个占位用的Mode,不是一种真正的Mode)
三、CFRunLoopTimerRef 、CFRunLoopTimerRef和 CFRunLoopModeRef的混合使用
- timerWithTimeInterval方式创建的定时器
- (void)timer1 {
//1. 创建定时器 timerWithTimeInterval这种方式创建的定时器需要手动添加到RunLoop中
NSTimer * timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
//2.添加到RunLoop中
//Mode: RunLoop的5种运行模式(默认| 界面跟踪、 占位)
//把定时器添加到runloop中,并指定为默认模式,并且当运行模式为NSDefaultRunLoopMode的时候,定时器才能工作
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}
- (void)run {
NSLog(@"run --- %@", [NSRunLoop currentRunLoop].currentMode);
}
注意点: 如果在此种的定时器下,页面添加一个滚动视图并且在滚动的时候,定时器将停止工作。
原因是:主运行模式会(kCFRunLoopDefaultMode) 切换成 界面追踪运行模式(UITrackingRunLoopMode)
如果想要实现在拖动情况下也能正常工作,则可以这样写
1.比较2B的写法
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
- 推荐以下的方式
//CommonMode这中模式
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
在主运行模式下,被指定的CommonModes 包含了 kCFRunLoopDefaultMode和 UITrackingRunLoopMode
NSLog(@"%@", [NSRunLoop currentRunLoop]);
//打印结果
RunLoopMode[17128:1191536] <CFRunLoop 0x600001dcc400 [0x10495fae8]>{wakeup port = 0x1e07, stopped = false, ignoreWakeUps = false,
current mode = kCFRunLoopDefaultMode,
common modes = <CFBasicHash 0x600002fe9c50 [0x10495fae8]>{type = mutable set, count = 2,
entries =>
0 : <CFString 0x107d2f070 [0x10495fae8]>{contents = "UITrackingRunLoopMode"}
2 : <CFString 0x104971ed8 [0x10495fae8]>{contents = "kCFRunLoopDefaultMode"}
}
- scheduledTimerWithTimeInterval方式创建的定时器
- (void)timer2 {
//创建定时器 这种方式创建默认添加到了CurrenRunLoop中,并指定运行模式为NSDefaultRunLoopMode
//这种模式下 页面滚动的时候也会影响定时器的工作
NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
///想要实现页面滚动的时候定时器也能正常工作,需要加入
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
}
3.在子线程里面创建Timer
创建一个子线程
//开启一个子线程
[self performSelectorInBackground:@selector(timer2) withObject:nil];
先 子线程runloop再添加定时器是无法正常工作的,因为RunLoop的mode在检查的时候,是为空的所以他会退出.
所以 先添加定时器,再手动创建RunLoop
- (void)timer2 {
NSLog(@"timer ----- %@", [NSThread currentThread]);
//需要手动创建子线程runloop
// [[NSRunLoop currentRunLoop] run]; //放在这里的时候,runloop会立即退出,即定时器无法正常使用,因为RunLoop的mode在检查的时候,是为空的所以他会退出
//创建定时器 这种方式创建默认添加到了CurrenRunLoop中,并指定运行模式为NSDefaultRunLoopMode
//这种模式下 页面滚动的时候也会影响定时器的工作
NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
//需要手动创建子线程runloop
[[NSRunLoop currentRunLoop] run];
}
#######补充一个精准的定时器
GCD-定时器,不受runloop的影响
@interface ViewController ()
{
dispatch_source_t _timer;
}
@end
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
///dispatchQueue 队列(GCD -4),dispatch_get_main_queue() 主队列就在主线程,非主队列在子线程
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(_timer, ^{
//执行发的任务
NSLog(@"555");
});
//开启定时器
dispatch_resume(_timer);
}
四、 CFRunLoopSourceRef 事件输入源
以前对于事件输入源的分法,即按照官方文档
- Port-Based Sources
- Custom Input Sources
- Cocoa Perform Selector Sources
现在对于事件输入源的分法,即按照函数调用栈
- Source0: 非基于Port的, 用于用户主动触发事件
- Source1:基于Port的 , 通过内核和其他线程相互发送消息
五、 CFRunLoopObserverRef
CFRunLoopObserverRef 是观察者,能够监听RunLoop的状态改变,可以监听的时间点有以下几个
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), 即将进入RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), 即将处理Timer
kCFRunLoopBeforeSources = (1UL << 2), 即将处理Source
kCFRunLoopBeforeWaiting = (1UL << 5), 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), 即将呗唤醒
kCFRunLoopExit = (1UL << 7), 即将退出RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU 全部状态
};
实现RunLoop的监听只能使用C语言的实现,只需要实现以下的代码:
/**
创建观察者
allocator 分配存储空间 默认 activities 要监听的状态 repeats 是否持续监听 order 优先级相关,暂不用考虑
runloop状态改变回调 observer 监听对象 activity 回调时候的状态
*/
CFRunLoopObserverRef observerRef = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), 0, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"runloop启动");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"runloop即将处理 Timer事件");
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的状态
CFRunLoopRef rl runloop对象 CFRunLoopObserverRef observer监听者 CFRunLoopMode mode runloop模式
NSDefaultRunLoopMode == kCFRunLoopDefaultMode
NSRunLoopCommonModes == kCFRunLoopCommonModes
**/
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observerRef, kCFRunLoopDefaultMode);
RunLoop处理逻辑具体请查看下图(改图来源于网络):
理解上图的逻辑图,基本就了解RunLoop的具体工作流程。
六、 RunLoop的应用
-
NSTimer 上面已经有例子提到
-
ImageView显示
-
PerformSelector
//两秒钟以后给uiimageView添加图片
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@""] afterDelay:2.0];
注: 如果此时,页面有个滚动视图,你不断滑动,那么这个imageView一直都不会展示图片
原因是: performSelector该方法会自动把事件添加到runloop中,指定运行模式为默认
此时就需要我们设置该runloop的运行模式为
- NSDefaultRunLoopMode和 UITrackingRunLoopMode
2.或者设置 NSRunLoopCommonModes
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@""] afterDelay:2.0 inModes:@[NSDefaultRunLoopMode, UITrackingRunLoopMode]];
-
常驻线程
下面是一个小小的案例,我们创建两个按钮,一个按钮创建一条线程,另一个按钮点击让创建的哪条线程继续执行任务。
//点击按钮创建一个线程
- (IBAction)createThread:(id)sender {
//1.创建一个线程
NSThread * thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadRun) object:nil];
//2.启动线程
[thread start];
self.thread = thread;
}
- (void)threadRun {
NSLog(@"threadRun --- %@", [NSThread currentThread]);
}
///点击按钮让上个线程继续执行任务
- (IBAction)goOnThread:(id)sender {
//这个是线程之间的通信 让之前创建的线程继续干活 这是从 主线程 -> 子线程
[self performSelector:@selector(goOnTask) onThread:self.thread withObject:nil waitUntilDone:YES];
}
- (void)goOnTask {
NSLog(@"threadRun ==== %@", [NSThread currentThread]);
}
这个时候,点击第二个按钮让刚刚创建的线程继续执行任务,那么程序就会崩溃。原因是threadRun 执行完毕,线程对象会进入死亡状态。想要这个线程执行goOnTask 里面的任务,那么就要保证线程不死,也就是要让threadRun 方法里面的任务执行不完,即进入运行循环。那么这个时候RunLoop就可以发挥作用啦!
那么修改一下 threadRun方法
- (void)threadRun {
NSLog(@"threadRun --- %@", [NSThread currentThread]);
//1.创建一个runLoop, 子线程需要手动创建 + 启动
//运行模式(默认模式), 判断运行模式是否为空
NSRunLoop * runloop = [NSRunLoop currentRunLoop];
//2.运行模式添加一个事件source事件或者Timer事件
[NSTimer scheduledTimerWithTimeInterval:4.0 target:self selector:@selector(threadTimerRun) userInfo:nil repeats:YES];
//3.启动runloop
[runloop run];
}
- (void)threadTimerRun {
NSLog(@"threadTimerRun ====");
}
这时候是可以达到了效果了,但是一直会有一个定时器在跑,我们这里是不要的,所以这种Timer事件在这里不合适,所以我们在这里应该使用Source事件。
如下所示,完美处理:
- (void)threadRun {
NSLog(@"threadRun --- %@", [NSThread currentThread]);
//1.创建一个runLoop, 子线程需要手动创建 + 启动
//运行模式(默认模式), 判断运行模式是否为空
NSRunLoop * runloop = [NSRunLoop currentRunLoop];
//2.运行模式添加一个source事件 port、custom、 selector
[runloop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
//3.启动runloop
[runloop run];
}
-
自动释放池
自动释放池第一次创建: 当RunLoop启动的时候
自动释放池最后一次销毁: 当RunLoop退出的时候
自动释放池其他时间的创建和销毁: 当RunLoop即将进入到休眠的时候,会把之前的自动释放池释放,重新创建一个新的自动释放池