概念
-
Runloop是通过内部维护的事件循环来对事件/消息进行管理的一个对象。
-
事件循环
-
就是没有消息需要处理时,休眠以避免资源占用。
用户态
切换到内核态
。 -
有消息需要处理时,立刻被唤醒。
内核态
切换到用户态
。
-
基本作用
- 保持程序的持续运行
- 处理App中的各种事件(比如触摸事件、定时器事件等)
- 节省CPU资源,提高程序性能:该做事时做事,该休息时休息
Q:main函数为什么可以保证不退出?
UIApplicationMain
启动主线程的runloop
Runloop 对象
-
Foundation:NSRunLoop
-
Core Foundation:CFRunLoopRef
CFRunLoopRef
Mode
CFRunLoopModeRef代表RunLoop的运行模式一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer,RunLoop启动时只能选择其中一个Mode,作为currentMode。如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出。
当在mode1
下运行时,只能接收和处理mode1
下的sources
、observers
、timers
,是无法接收mode2
、mode3
下的事件回调的。
常见的2种Mode
- kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行
- UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
Q:如何将一个Timer同时加入到两个Mode中?
commonMode
不是实际存在的一种Mode
。- 是同步
Source/Timer/Observer
到多个Mode
中的一种技术方案。
ModeItem
- Source0
- 当添加事件到
Source0
,它并不会主动唤醒线程,需要手动唤醒线程。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
- 当添加事件到
- Source1
- 具备唤醒线程的能力。包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。
- CFRunLoopTimer
- 基于事件的定时器,和
NSTimer
是可以转换的。
- 基于事件的定时器,和
- CFRunLoopObserver
- 观测时间点
kCFRunLoopEntry
即将进入LoopkCFRunLoopBeforeTimers
即将处理 TimerkCFRunLoopBeforeSources
即将处理 SourcekCFRunLoopBeforeWaiting
(用户态切内核态)kCFRunLoopAfterWaiting
(内核态切用户态)kCFRunLoopExit即将退出
- 观测时间点
Q:唤醒休眠的Runloop?
- Source1
- Timer事件
- 外部手动唤醒
事件循环机制
RunLoop事件循环机制
RunLoop内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。RunLoop t通过调用mach_msg函数进入休眠等待唤醒状态。
runloop 和线程关系
- 每条线程都有唯一的一个与之对应的RunLoop对象
- RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
- 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
- RunLoop会在线程结束时销毁
- 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop
线程安全
- CFRunLoop系列函数是线程安全的
- NSRunLoop系列函数不是线程安全的
启动RunLoop
通过CFRunLoopRun系列函数启动RunLoop,启动时可以指定超时时间。RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,你可以添加一个一次性timer到RunLoop然后再调用CFRunLoopRun。
退出RunLoop
- 启动RunLoop时制定超时时间
- 通过 CFRunLoopStop主动退出
Q:怎样实现一个常驻线程?
- 为当前线程开启一个
RunLoop
。 - 向该
RunLoop
中添加一个Port/Source
等维持RunLoop
的事件循环。 - 启动该
RunLoop
。
Q:怎样保证子线程数据回来更新UI的时候不打断用户的滑动操作?
- 把
子线程
抛回给主线程
更新UI
的逻辑,包装起来,提交到主线程的kCFRunLoopDefaultMode
下。 - 当前用户滑动
tableView
处于UITrackingRunLoopMode
模式下就不会执行该逻辑。 - 当用户停止滑动之后,当前线程
mode
切换到kCFRunLoopDefaultMode
下,就会处理更新UI
的逻辑。
RunLoop应用
苹果用RunLoop实现的功能
AutoreleasePool、事件响应、手势识别、界面更新、定时器、PerformSelecter、GCD、网络请求底层等都用到了RunLoop
解决NSTimer事件在列表滚动时不执行问题
- 在滑动时,
currentMode
会从kCFRunLoopDefaultMode
切换到UITrackingRunLoopMode
。 - 可以通过
CFRunLoopAddTimer(runLoop,timer,commonMode)
函数,将timer
添加到commonMode
当中。 commonMode
的作用就是将某一个事件源同步到多个mode
当中。
Runloop中自动释放池创建和释放时机
-系统在Runloop开始处理一个事件时创建一个autoreleaspool。
- 系统会在处理完一个事件后释放 autoreleaspool 。
- 我们手动创建的 autoreleasepool 会在 block 执行完成之后进行 drain 操作。需要注意的是:当 block 以异常结束时,pool 不会被 drain Pool 的 drain 操作会把所有标记为 autorelease 的对象的引用计数减一,但是并不意味着这个对象一定会被释放掉,我们可以在 autorelease pool 中手动 retain 对象,以延长它的生命周期(在 MRC 中)。 通过_objc_autoreleasePoolPush和_objc_autoreleasePoolPop来创建和释放自动释放池,底层是通过AutoreleasePoolPage来实现的。
- 自动释放池是由 AutoreleasePoolPage 以双向链表的方式实现的
- 当对象调用 autorelease 方法时,会将对象加入 AutoreleasePoolPage 的栈中
- 调用 AutoreleasePoolPage::pop 方法会向栈中的对象发送 release 消息 关于自动释放池的原理,可以参考这篇文章自动释放池的前世今生
监控卡顿
可以通过监控runloop的 kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting的事件间隔来监控卡顿。
创建子线程执行任务
你可以创建子线程,然后在别的线程通过performSelector:onThread:withObject:waitUntilDone:路由到该子线程进行处理。
AsyncDisplayKit
AsyncDisplayKit( 现在更名为Texture),是Facebook开源的用来异步绘制UI的框架。ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。