OC学习38.1:Runloop底层原理
Runloop是什么?
RunLoop
又叫运行循环
,内部就是一个do-while循环
,在这个循环内部不断处理各种任务,保证程序持续运行
。
- 验证:下载源码 ,查看CFRunloop.c文件,找到CFRunLoopRun源码可知:确实是
do...while
循环。
1 | void CFRunLoopRun(void) { /* DOES CALLOUT */ |
- 官方文档 :也有详细的说明:
- Runloop不是线程安全的
Runloop的作用?重点
- 保持程序的持续运行。
- 处理APP中各种事件(触摸、定时器、performSelector)。
- 节省CPU资源,提高程序性能(该做事时做事,该休息时休息)。
程序中 main
函数的 UIApplicationMain
函数主要作用就是创建了一个主运行循环,主线程几乎所有的事情都是交给runloop去完成,如 UI界面的刷新、点击事件的处理、performSelector等等,但并非所有的任务都是由runloop完成。
Runloop对象
对象:iOS中有2套API来访问和使用RunLoop
- OC语言:Foundation – NSRunloop
- C语言: Core Foundaton – CFRunloopRef
NSRunLoop
是基于CFRunLoopRef
的一层OC包装。
获取RunLoop对象:
1 | Foundation |
Runloop相关的类
- Core Foundation中关于Runloop的5个类:
- CFRunloopRef: Runloop的对象
- CFRunloopModeRef: 模式
- CFRunloopSourceRef: 输入源/事件源
- CFRunloopTimerRef: Timer事件
- CFRunloopObserveRef: 监听者,监听Runloop的状态改变
- CFRunloopRef:
CFRunloopRef
是 Runloop
的对象
- CFRunloopModeRef:模式
1 | typedef struct __CFRunLoopMode *CFRunLoopModeRef; |
由上述源码和图可知:
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会立马退出。NSRunloopRef常见的几种Mode:
代码去打印一下:
1 | // CFRunloopModel研究 |
- kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行;
- UITrackingRunLoopMode:界面跟踪
Mode
,用于ScrollView
追踪触摸滑动,保证界面滑动时不受其他Mode
影响。 - NSRunLoopCommonModes:并不是一个真正的模式,只是一个标记
- GSEventReceiveRunLoopMode
- CFRunloopSourceRef:输入源/事件源
- 触摸事件:source0
由上面的截图可知:runloop处理source0事件是调用的 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
函数。
我们来看一下源码:
1 | static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__() __attribute__((noinline)); |
- CFRunloopTimerRef:
timer类型:
1 | [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { |
由上面的截图可知:runloop处理timer事件是调用的__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
函数进行事件处理:
再看一下源码:
1 | static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__() __attribute__((noinline)); |
- CFRunloopObserveRef:监听者,监听Runloop的状态改变
- 用来监听runloop的活动:
1 | / Run Loop Observer Activities */ |
- 添加Observer监听RunLoop的所有状态
1 | // 创建监听者 |
- Mode中各个成员的含义:
- source0:
1)触摸事件(TouchUp)
2)performSelector:OnThread(在指定线程)
- source1
1)基于 Port
的线程间通信
2)系统事件的捕捉
- Timer
1)NSTimer
2)performSelector :afterDelay // 这句代码的本质是往Runloop中添加定时器
- Observers
1)监听runloop的状态
2)UI刷新(在runloop休眠之前)
3)自动释放池(在runloop休眠之前)
- runloop处理items事件的总结:
- runloop处理items以下事件调用函数的总结:
1 | * block:__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ |
Runloop与线程的关系?
- 每一条线程都有一个与之对应runloop对象。
查看源码验证,线程和runloop是一一对应的关系
:
1 | CFRunLoopRef CFRunLoopGetMain(void) { |
继续查找
1 | CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) { |
- runloop保存在全局的
NSMutableDictionaryRef
字典当中,以线程为key,runloop为value
。
由上面的源码可验证。
主线程的Runloop系统自动获取(创建),子线程默认没有开启runloop。
线程刚创建时是没有runloop对象,runloop会在第一次获取时创建。
runloop在线程销毁时销毁。
NSTimer 重点
- 什么是 NSTimer?
NSTimer
是一个定时器,是一个面向对象的定时器。在经过一定的时间间隔后触发,向目标对象发送指定的消息。其工作原理是将一个监听加入到系统的 runloop
中去,当系统 runloop
执行到 timer
条件的循环时,会调用 timer
一次,如果是一个重复的定时器,当timer回调函数结束之后,timer会再一次的将自己加入到runloop中去继续监听下一次timer事件。
- NSTimer和RunLoop的关系
NSTimer
的原理是 将定时器中的事件添加到runloop中
,以实现循环的,这是因为定时器默认处于 runloop
中的 kCFRunLoopDefaultMode
,主线程默认也处于此mode下,定时器这才具备了这样的能力。所以,没有runloop,NSTimer完全无法工作。
【问题1】这里提出一个经典的案例:定时器默认无法在页面滚动时执行
原因是滚动时,主线程runloop处于 UITrackingRunLoopMode
,这时候,定时器所处runloop依然处于kCFRunLoopDefaultMode,就导致 定时器线程被阻塞
,要解决这一个问题,我们就需要定时器无论是在 kCFRunLoopDefaultMode
还是 UITrackingRunLoopMode
下都可以正常工作,这时候就需要用到 runloop
中的伪模式 kCFRunLoopCommonMode
,这并不是一个真正的mode,而是一种多mode的处理方式。具体做法如下:
1 | NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { |
【问题2】问NSTimer是怎么加入到 KCFRunloopCommonMode
下就不会造成卡顿?
答:NSTimer
通过底层函数 CFRunLoopAddTimer
加入到 items
中。然后再 runloopRun
运行的时候,通过 while循环遍历item找到timer的mode是否和当前的mode相等或等于commonMode
,如果相等调用block函数返回执行。
【问题3】子线程定时器不走?
定时器的实现是基于 Runloop
的,平时我们使用定时器或许并没有对Runloop做什么操作,那是因为 主线程的runloop默认开启运行
的,如果我们在子线程中也需要重复执行某一动作,需要手动开启定时器。
1 | queue = dispatch_queue_create("myQueue",DISPATCH_QUEUE_CONCURRENT); |
- 定时器释放的方式
1 | [_timer invalidate]; |
二者缺一不可。
如果是在VC中创建的 NSTimer
,这种情况下,self
和 _timer
相互强引用,VC
的 Dealloc
方法不会执行,所以定时器的销毁方法不能放在 Dealloc
中,需要放在 Dealloc
之前(viewWillDIsappear),原因我们放到最后说明。
- NSTimer的时间准确吗?重点
不准确
,NSTimer不是采用实时机制!
原因:NSTimer的精确度略低了点,比如NSTimer的触发时间到的时候,runloop如果在阻塞状态
,触发时间就会推迟到下一个runloop周期。
- NSTimer 创建方式
iOS10.0以前
1 | + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo; |
iOS10.0以后
1 | + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block |
- 区别是有无
Block
的回调方法,block的作用就是将自身作为参数传递给block,来帮助 避免循环引用
- NSTimer如何避免循环引用
在
Dealloc
之前销毁定时器使用
block
方式使用
NSProxy
增加一个中间层
Runloop运行流程
- NSRunLoop:
是基于 CFRunLoopRef
的OC封装,提供了面向对象的 API
,但不是线程安全的,CFRunLoopRef
是在 CoreFoundation
框架内的,它提供了纯 C
函数的 API
,是线程安全的,CoreFoundation是开源的(CoreFoundation 源码地址 )
- Runloop 运行流程图
- Post title:OC学习38.1:Runloop底层原理
- Post author:张建
- Create time:2023-02-16 17:18:17
- Post link:https://redefine.ohevan.com/2023/02/16/OC/OC学习38.1:Runloop底层原理/
- Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.