NSRunLoop简介
一. 什么是RunLoop
-
RunLoop
- 从字面上了解, RunLoop即是运行循环, 就像是在一个圆形循环中去运作
- RunLoop的基本作用
- 他是App持续运行的保证, 如果RunLoop不存在了, 程序也就终止运行了
- RunLoop会在循环中处理App的各种事件, 如
触摸事件, 定时器事件, Selector事件
- RunLoop最大的优势就是能节省CPU的资源, 提高程序的性能, 他会在需要执行任务的时候被唤醒, 当没有任务执行的时候进入休眠状态
-
Main函数中的RunLoop
首先, 重温一遍App的启动原理
当Main函数执行到UIApplicationMain时, 就开启了RunLoop运行循环
在运行循环开启时, 就会保证程序的持续运行并且处理App的各种事件, 不会退出
-
Main函数中的RunLoop, 被称为主运行循环, 而主运行循环在整个App的声明周期中都不会被销毁, 他是程序运行的保证
// 程序在启动时,第一步就会执行main函数,在main函数中会执行以下操作: int main(int argc, char * argv[]) { @autoreleasepool { /* nil:UIApplication类名或者子类名称,如果为nil,就等于@"UIApplication" NSStringFromClass([AppDelegate class]:UIApplication代理的名称 */ return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } } 程序启动的完整流程 1. 执行main函数 2. 执行UIApplicationMain函数 1> 指定UIApplication对象 2> 指定UIApplication的代理 3. 创建UIApplication对象,并且指定他的代理 4. **创建一个事件循环:主循环(RunLoop),并且是一个死循环,保证程序的持续运行** 5. 加载配置了所有应用程序信息的info.plist文件 1> 判断 Main storyboard file base name中有没有指定Main,即需要加载的StoryBoard文件 2> 如果指定了,就加载Main.storyboard 3> 如果没有指定的话,就会黑屏 6. 应用程序启动完毕
二. NSRunLoop和CFRunLoopRef
-
简单介绍
- CFRunLoopRef是在CoreFoundation框架中的, 它的内部API以及实现, 都是纯C语言编写, 这些API都是现成安全的
- NSRunLoop是基于CFRunLoopRef的封装, 他提供了面向对象的API, 但是这些API不是线程安全的
- 目前CFRunLoopRef已经开源了, 大家可以在官方文档中查看: 友情提示: 需要很好的C语言功底
-
NSRunloop和CFRunLoopRef都是RunLoop对象, 他们的区别是
- 这两个对象的地址不同, 因为他们的对象来自于完全不同的类
- CFRunLoop可以调用
getCfRunLoop
方法, 将NSRunLoop转化为CFRunLoop - 线程: RunLoop在主线程中, 保持持续循环状态, 当所有的事件处理结束, 就会进入休眠状态
- 当外界传入各种时间时: Prot接口事件时, RunLoop就会被唤醒, 处理相关的事件, 当事件处理完毕时, 会再次进入休眠状态
- UI交互事件
- PerformSelector: onThread: 让线程执行任务
- Timer: 定时器事件
RunLoop运行图解
-
简单的应用
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { // 1. 获取当前线程对应的RunLoop对象 NSRunLoop *curRunLoop = [NSRunLoop currentRunLoop]; NSLog(@"%p", curRunLoop); // 2. 获取主线程对应的RunLoop对象 NSRunLoop *mainRunLoop = [NSRunLoop mainRunLoop]; NSLog(@"%p", mainRunLoop); /* Core Foundation */ // 1. 获得当前线程的RunLoop对象 CFRunLoopRef runloop = CFRunLoopGetCurrent(); NSLog(@"%p", runloop); // 2. 获得主线程的RunLoop对象 CFRunLoopRef cfMainRunLoop = CFRunLoopGetMain(); NSLog(@"%p", cfMainRunLoop); // 从这里可以看出这两种运行循环是完全不同的对象 // NSRunLoop --> CFRunLoopRef NSLog(@"%p---%p", cfMainRunLoop, mainRunLoop.getCFRunLoop); // 开启子线程,执行task方法 [NSThread detachNewThreadSelector:@selector(task) toTarget:self withObject:nil]; } - (void)task { /* 子线程和RunLoop 1. 每一个子线程,都对应一个自己的RunLoop 2. 主线程的RunLoop在程序运行的时候就已经创建了,而子线程的RunLoop则需要手动开启 3. [NSRunLoop currentRunLoop],此方法会开启一个新的RunLoop 4. RunLoop需要执行run方法,来开启,但如果RunLoop中没有任何任务,就会关闭 */ // 1. 当前RunLoop NSLog(@"%p--%p", [NSRunLoop currentRunLoop], [NSRunLoop mainRunLoop]); // 2. 开启一个新的RunLoop [[NSRunLoop currentRunLoop] run]; NSLog(@"tast---%@", [NSThread currentThread]); }
三. RunLoop与线程
- 每一条线程, 都有一个与之相对应的RunLoop对象, 负责处理线程中的任务
- 线程的创建
- 主线程: RunLoop是在程序已经启动的时候就创建好了, 当程序关闭的时候主线程才被销毁
- 子线程: 子线程需要手动创建RunLoop, 并且手动开启, 当没有任务执行时, 该线程会被关闭, RunLoop被销毁
- 子线程和RunLoop
子线程会单独开启RunLoop去执行任务
子线程和RunLoop是一一对应的关系, 每个子线程都有自己的RunLoop(但需要主动创建)
-
创建子线程的RunLoop:
[NSRunloop currentRunLoop]
- 通过对CFRunLoop原码的分析可以判断出, 这个方法是懒加载获取RunLoop对象的, 当第一次调用这个方法时, 他就会在对应的线程中创建一个RunLoop, 并且保存到一个字典中便于随时取出
- 也就是说, 如果不主动去获取RunLoop, 那么默认是不会给子线程创建一个RunLoop的
子线程的RunLoop需要手动开启:
[[NSRunLoop currentRunLoop] run]
-
如果RunLoop内部没有任何任务需要去处理时, 就会被关闭
// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef static CFMutableDictionaryRef loopsDic; // 访问 loopsDic 时的锁 static CFSpinLock_t loopsLock; // 获取一个 pthread 对应的 RunLoop CFRunLoopRef _CFRunLoopGet(pthread_t thread) { OSSpinLockLock(&loopsLock); if (!loopsDic) { // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。 loopsDic = CFDictionaryCreateMutable(); CFRunLoopRef mainLoop = _CFRunLoopCreate(); CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop); } // 直接从 Dictionary 里获取。 CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread)); if (!loop) { // 取不到时,创建一个 loop = _CFRunLoopCreate(); CFDictionarySetValue(loopsDic, thread, loop); // 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop); } OSSpinLockUnLock(&loopsLock); return loop; } CFRunLoopRef CFRunLoopGetMain() { return _CFRunLoopGet(pthread_main_thread_np()); } CFRunLoopRef CFRunLoopGetCurrent() { return _CFRunLoopGet(pthread_self()); }
四. RunLoop的相关类
- RunLoop的运行模式图
- 一个RunLoop包含多个Mode, 而每个Mode又包含若干个Source/Timer/Observer
-
相关类
- RunLoop: RunLoop对象本身
- RunLoopMode: 即RunLoop的运行模式
- 一个RunLoop至少要制定一个运行模式, 当运行模式指定之后, 至少有一个Source或者Timer任务在执行
- RunLoop启动之后, 只能指定一个运行模式, 可以使用
currentMode
来获取 - 如果要切换RunLoop的运行模式, 就要先退出当前的RunLoop, 重新指定Mode再次进入运行
- 系统默认注册的5个Mode
- kCFRunLoopDefaultMode: App的默认Mode, 通常主线程是在这个Mode下运行的
- UITrackingRunLoopMode: 界面跟踪Mode, 用于ScrollView/TableView等追踪触摸滑动, 保证界面滑动的时候不受其他Mode影响
- UIInitializationRunLoopMode: 当App启动时, 第一个进入的Mode, 启动完成之后就不会再使用这个Mode
- GSEventReceiveRunLoopMode: 接收系统事件的内部Mode, 通常由系统自动管理
- kCFRunLoopCommonMode: 一个类似于占位的Mode, 并不是一个真正的Mode
-
Mode中的类
- CFRunLoopSourceRef: 事件源, 事件, 输入等都属于事件源, 他有两个分类
- Source0, 非基于Port, 用户触发的事件, 例如点击事件等
- Source1, 基于Port的事件, 他用于系统内部与线程之间交互
- CFRunLoopTimerRef: 定时器事件
- NSTimer:
- 如果将NSTimer添加到子线程中, 需要先创建一个RunLoop, 然后再启动RunLoop
- NSTimer受到RunLoop的印象, 一般会有一些轻微的误差, 所以对于精密计时, GCD定时器较为精准
- GCD定时器: 精确到纳秒, 较为精准
GCD定时器的创建步骤: 创建定时器 -> 设置定时器 -> 设置定时器的回调方法 -> 恢复定时器
-
GCD定时器一定要添加一个强引用, 否则会被立即释放
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { // 1. 创建GCD定时器 /* DISPATCH_SOURCE_TYPE_TIMER 定时器 uintptr_t handle 描述信息 unsigned long mask 传入0 dispatch_queue_t queue 定时器运行的队列,决定定时器在哪个线程中运行 */ dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0)); // 2. 设置定时器 /* dispatch_source_t source, 定时器的对象 dispatch_time_t start, 定时器什么时候开始 uint64_t interval, 定时器多长时间执行一次 uint64_t leeway 精准度,0为绝对精准 */ dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC); // 3. 设置定时器的任务 dispatch_source_set_event_handler(timer, ^{ NSLog(@"---GCD---%@", [NSThread currentThread]); }); // 4. 恢复定时器 dispatch_resume(timer); // 5. 强引用定时器,否则创建出来就会被释放 self.timer = timer; }
- NSTimer:
- CFRunLoopObserverRef: 观察者
观察者可以观察到RunLoop不同的运行状态
-
通过判断RunLoop的运行状态, 可以执行一些操作
// 1. 创建监听者 /* CFAllocatorRef allocator 分配存储空间 CFOptionFlags activities 要监听哪个状态,kCFRunLoopAllActivities监听所有状态 Boolean repeats 是否持续监听RunLoop的状态 CFIndex order 优先级,默认为0 Block activity RunLoop当前的状态 */ CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { /* kCFRunLoopEntry = (1UL << 0), 进入工作 kCFRunLoopBeforeTimers = (1UL << 1), 即将处理Timers事件 kCFRunLoopBeforeSources = (1UL << 2), 即将处理Source事件 kCFRunLoopBeforeWaiting = (1UL << 5), 即将休眠 kCFRunLoopAfterWaiting = (1UL << 6), 被唤醒 kCFRunLoopExit = (1UL << 7), 退出RunLoop kCFRunLoopAllActivities = 0x0FFFFFFFU 监听所有事件 */ // 当activity处于什么状态的时候,调用一次 switch (activity) { case kCFRunLoopEntry: NSLog(@"进入"); break; case kCFRunLoopBeforeTimers: NSLog(@"即将处理Timer事件"); break; case kCFRunLoopBeforeSources: NSLog(@"即将处理Source事件"); break; case kCFRunLoopBeforeWaiting: NSLog(@"即将休眠"); break; case kCFRunLoopAfterWaiting: NSLog(@"被唤醒"); break; case kCFRunLoopExit: NSLog(@"退出RunLoop"); break; default: break; } }); // 2. 给对应的RunLoop添加一个监听者,并制定监听的是那种运行模式 /* CFRunLoopRef rl 要添加监听者的RunLoop CFRunLoopObserverRef observer, 要添加的监听者 CFStringRef mode RunLoop的运行模式 */ CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
- CFRunLoopSourceRef: 事件源, 事件, 输入等都属于事件源, 他有两个分类