什么是 RunLoop?
- 运行循环
- 内部就是一个 do-while 循环, 在这个循环里面不断的处理各种任务
- 一个线程对应有一个 RunLoop, 主线程的 RunLoop 默认已经启动, 子线程的 RunLoop 需要手动去启动 (调用 run 方法)
- RunLoop 只能选择一个 Mode 启动, 如果当前 Mode 中没有任何Source(Sources0、Sources1)、Timer, 那么就直接退出 RunLoop.
- 基本的作用就是保持程序的持续运行, 处理 app 中的各种事件. 通过 RunLoop, 有事运行, 没事就休息, 可以节省 cpu 资源, 提高程序性能.
RunLoop对象
iOS 中有2套API来访问和使用RunLoop
- Foundation: NSRunLoop
- Core Foundation: CFRunLoopRef
- NSRunLoop 和 CFRunLoopRef 都代表着 RunLoop对象
- NSRunLoop 是基于 CFRunLoopRef 的一层 OC 包装, 所以要了解 RunLoop内部结构, 需要多研究 CFRunLoopRef 层面的 API
RunLoop和线程
- 每条线程都有唯一的一个与之对应的 RunLoop 对象
- 主线程的 RunLoop 已经自动创建好了, 子线程的 RunLoop 需要主动创建
- RunLoop 在第一次获取时创建, 在线程结束时销毁
获取 RunLoop 对象
- Foundation
[NSRunLoop currentRunLoop]; // 获取当前 RunLoop 对象
[NSRunLoop mainRunLoop]; // 获取主线程 RunLoop 对象 - Core Foundation
CFRunLoopGetCurrent(): // 获取当前 RunLoop 对象
CFRunLoopGetMain(); // 获取主线程 RunLoop 对象
RunLoop相关类
Core Foundation 中关于 RunLoop 的5个类
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
CFRunLoopModeRef
- CFRunLoopModeRef 代表 RunLoop 的运行模式.
- 一个 RunLoop 包含若干个 Mode, 每个 Model 又包含若干个(set)Source/(array)Timer/(array)Observer
- 每次 RunLoop 启动时, 只能制定其中一个 Mode, 这个 Mode 被称作 CurrentMode
- 如果需要切换 Mode, 只能退出 Loop, 重新制定一个 Mode 再进入
- mode 主要是用来制定事件在运行循环中的优先级, 分为:
- NSDefaultRunLoopMode (kCFRunLoopDefaultMode): 默认, 空闲状态
- UITrackingRunLoopMode: ScrollView 滑动时会切换到这个Mode
- UIInitializationRunLoopMode: run loop 启动时, 会切换到该 Mode
- NSRunLoopCommonModes (kCFRunLoopCommonModes) : mode 集合
苹果公开提供的mode有俩个: NSDefaultRunLoopMode (kCFRunLoopDefaultMode), NSRunLoopCommonModes (kCFRunLoopCommonModes)
CFRunLoopTimerRef
- CFRunLoopTimerRef 是基于时间的触发器
- CFRunLoopTimerRef 基本上说的就是 NSTimer, 它受 RunLoop 的 Mode 的影响
CFRunLoopSourceRef
- CFRunLoopSourceRef 是事件源 (输入源)
- 按照官方的文档, Source 的分类
- Port-Based Source
- Custom Input Sources
- Cocoa Perform Selector
- 按照函数调用栈, Source 的分类
- Source0: 非基于 Port 的
- Source1: 基于 Port 的, 通过内核和其他线程通信, 接受、分发系统事件
CFRunLoopObserverRef
- CFRunLoopObserverRef 是观察者, 能够箭筒 RunLoop 的改变状态
- 可以监听的时间点有以下几个:
- kcfRunLoopEntry (即将进入 loop ) // 1
- kcfRunLoopBeforeTimers (即将处理 Timer) // 2
- kcfRunLoopBeforeSource (即将处理 source) // 4
- kcfRunLoopBeforeWaiting (即将进入休眠) // 32
- kcfRunLoopAfterWaiting (刚从休眠中唤醒) // 64
- kcfRunLoopExit (即将退出 loop) // 128
- 添加观察者
CFRunLoopObserverRef observer =
CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(),
kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer,
CFRunLoopActivity activity) {
NSLog(@"----RunLoop---%zd", activity);
});
// RunLoop
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer,
kCFRunLoopDefaultMode);
// Observer
CFRelease(observer);
RunLoop处理逻辑
- 通知 Observer: 即将进入 Loop (1)
- 通知 Observer: 将要处理 Timer (2)
- 通知 Observer: 将要处理 Source0 (3)
- 处理 Source0 (4)
- 如果有 Source0, 跳到第9步(5)
- 通知 Observer: 线程即将休眠(6)
- 休眠, 等待唤醒: (7)
- Source0(port).
- timer 启动
- RunLoop 设置的 Timer 已经超时
- RunLoop 被外部手动唤醒
- 通知 Observer: 线程将被唤醒 (8)
- 处理未处理的时间 (9)
- 如果用户定义的定时器启动, 处理定时器时间并重启 RunLoop. 进入步骤 (2)
- 如果输入源启动, 传递相应的消息.
- 如果 RunLoop 被显示唤醒二时间还没有超时, 重启 RunLoop, 进入步骤 (2)
- 通知 Observer: 即将退出 Loop
RunLoop的应用
- NSTimer
- ImageView 显示
- PerformSelector
- 常驻线程
- 自动释放池
RunLoop 定时源和输入源
- RunLoop 处理的输入事件有俩种不同的来源: 输入源 (input source) 和定时源 (timer source).
- 输入源传递异步消息, 通常来自于其他线程或程序.
- 定时源则传递同步消息, 在特定时间或者一定时间间隔发生.
NSRunLoop 的实现机制, 以及在多线程中如何使用
- 实现机制: RunLoop 的基本作用, 处理逻辑.
- 程序创建子程序的时候, 才需要手动启动 runLoop. 主线程的 runLoop 已经默认启动.
- 在多线程中, 你需要判断是否需要 RunLoop. 如果需要 RunLoop, 那么你要负责配置 RunLoop 并启动. 你不需要在任何情况下都去启动 RunLoop. 比如, 你使用线程去处理一个预先定义好的耗时极长的任务时, 你就可以无需启动 RunLoop. RunLoop 只在你要和线程有交互事才需要.
RunLoop和线程有什么关系?
- 主线程的 RunLoop 默认是启动的
iOS的应用程序里面, 程序启动后会有一个如下的main () 函数
重点的是 UIApplicationMain()函数, 这个方法会为 mainThread 设置一个 RunLoop 对象.int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }
这就解释了: 为什么我们的应用可以在无人操作的时候休息, 需要让它干活的时候又能立马响应. - 对其他的线程来说, RunLoop 默认是没有启动的, RunLoop 只有你在要和线程有交互的时候才有需要.
- 在任何一个 coco 程序中, 都可以通过下面的代码来获取当前的 RunLoop.
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
autorelease 对象在什么情况下会被释放?
- 分俩种情况: 手动干预释放和系统自动释放.
- 手动干预释放就是指定 autoreleasePool, Autorelease 对象会在当前的 runLoop 迭代结束时释放.
- kCFRunLoopEntry(1): 第一次进去自动创建一个 autorelease
- kCFRunLoopBeforeWaiting(32): 进入休眠状态前会自动销毁一个 autorelease, 然后重新创建一个新的 autorelease
- kCFRunLoopExit(128): 退出 RunLoop 时会自动销毁最后一个创建的 autorelease
测试, RunLoop 的理解不正确的是?
A 每一个线程都有其对应的RunLoop
B 默认非主线程的RunLoop是没有运行的
C 在一个单独的线程中没有必要去启用RunLoop
D 可以将NSTimer添加到runloop中
- 参考答案: C
- 理由: RunLoop, 它是多线程的法宝, 通常来说一个线程一次只执行一次任务, 执行完任务会退出线程. 但是, 对于主线程是不能退出的, 因此我们需要让主线程即时任务执行完毕, 也可以继续等待接受事件而不退出,那么 RunLoop 就成关键法宝了. 但是非主线程通常来说就是为了执行某一任务的, 执行完毕冀需要归还资源, 因此默认是不运行 RunLoop 的. NSRunLoop 提供了一个添加 NSTimer 的方法, 这个方法是正常状态下就会回调.
RunLoop 的 Mode 作用是什么?
mode 主要是用来指定时间在运行循环中的优先级, 分为:
- NSDefaultRunLoopMode (kCFRunLoopDefaultMode): 默认, 空闲状态
- UITrackingRunLoopMode: ScrollView 滑动的时候会切换到这个 mode
- UIInitializationRunLoopMode: RunLoop 启动时, 会切换到该 mode
- NSRunLoopCommonModes (kCFRunLoopCommonModes) : Mode 集合
苹果公开提供的 Mode 有俩个:
- NSDefaultRunLoopMode (kCFRunLoopDefaultMode)
- NSRunLoopCommonModes (kCFRunLoopCommonModes)
如果我们把一个 NSTimer 对象以 NSDefaultRunLoopMode (kCFRunLoopDefaultMode) 添加到主运行循环中的时候, ScrollView 的滑动会导致 Mode 的切换, 而导致 NSTimer 将不再被调度, 如果希望滑动的时候也能够被调度, 我们就可以是用 NSRunLoopCommonMode (包含, NSDefaultRunLoopMode 和 NSTrackingRunLoopMode 俩个状态)
测试, 请写出 NSTimer 使用时的注意事项
思路: 如果想要销毁 timer , 应该先把 timer 置为失效, 否则 timer 就一直占用内存而不会释放. 造成逻辑上的内存泄漏. 而且这种泄漏不能用 Xcode 和 instruments 测出来. 未将 timer 置为失效, 每次创建一次, 则之前的不能得到释放, 那么同时存在多个 timer 的实例在内存中.
参考答案:
- 注意 timer 添加到 runloop 时应该设置什么 mode.
- 注意timer 在不需要时, 一定要调用 invalidate 方法使定时器失效, 否则得不到释放.
测试, UITableViewCell 上有个 UILabel, 显示 NSTimer 实现的秒表时间, 手指滚动 cell 过程中, label 是否刷新, 为什么?
思路同上, 自己作答.
测试, 为什么 UIScrollView 的滚动会导致 NSTimer 失效?
思路同上, 自己作答.
测试, 在滑动页面上的列表, timer 会暂停回调, 为什么? 如何解决?
思路同上, 自己作答.
在开发中如何使用 RunLoop? 什么应用场景?
- 开启一个常驻线程 (让一个子线程不进入消亡状态, 等待其他线程发来消息, 处理其他事情)
- 在子线程开启一个定时器
- 在子线程中进行一些长期监控
- 可以控制定时器在特定模式下执行
- 可以让某些事件 (行为, 任务) 在特定模式下执行
- 可以添加 Observer 监听 RunLoop 的状态, 比如监听点击事件的处理 (在所有点击事件之前做一些事情)
网友评论