OC学习38.1:Runloop底层原理

张建 lol

Runloop是什么?

  1. RunLoop 又叫 运行循环,内部就是一个 do-while循环,在这个循环内部不断 处理各种任务,保证程序持续运行

  1. 验证:下载源码 ,查看CFRunloop.c文件,找到CFRunLoopRun源码可知:确实是 do...while 循环。
1
2
3
4
5
6
7
void CFRunLoopRun(void) {	/* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
  1. 官方文档 :也有详细的说明:
  • 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
2
3
4
5
6
7
Foundation
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象

Core Foundation
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象

Runloop相关的类

  1. Core Foundation中关于Runloop的5个类:
  • CFRunloopRef: Runloop的对象
  • CFRunloopModeRef: 模式
  • CFRunloopSourceRef: 输入源/事件源
  • CFRunloopTimerRef: Timer事件
  • CFRunloopObserveRef: 监听者,监听Runloop的状态改变
  1. CFRunloopRef:

CFRunloopRefRunloop 的对象

  1. CFRunloopModeRef:模式
1
2
3
4
5
6
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;

由上述源码和图可知:

  1. 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会立马退出。

  2. NSRunloopRef常见的几种Mode:

代码去打印一下:

1
2
3
4
5
6
// CFRunloopModel研究
CFRunLoopRef lp = CFRunLoopGetCurrent();
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(lp);
NSLog(@"mode:%@",mode);
CFArrayRef modeArr = CFRunLoopCopyAllModes(lp);
NSLog(@"modeArr:%@",modeArr);
  • kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行;
  • UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
  • NSRunLoopCommonModes:并不是一个真正的模式,只是一个标记
  • GSEventReceiveRunLoopMode
  1. CFRunloopSourceRef:输入源/事件源
  • 触摸事件:source0

由上面的截图可知:runloop处理source0事件是调用的 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ 函数。
我们来看一下源码:

1
2
3
4
5
6
7
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(void (*perform)(void *), void *info) {
if (perform) {
perform(info);
}
asm __volatile__(""); // thwart tail-call optimization
}
  1. CFRunloopTimerRef:

timer类型:

1
2
3
[NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"小老弟");
}];

由上面的截图可知:runloop处理timer事件是调用的__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ 函数进行事件处理:
再看一下源码:

1
2
3
4
5
6
7
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(CFRunLoopTimerCallBack func, CFRunLoopTimerRef timer, void *info) {
if (func) {
func(timer, info);
}
asm __volatile__(""); // thwart tail-call optimization
}
  1. CFRunloopObserveRef:监听者,监听Runloop的状态改变
  • 用来监听runloop的活动:
1
2
3
4
5
6
7
8
9
10
/ Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), //即将处理timers
kCFRunLoopBeforeSources = (1UL << 2), //即将处理sources
kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), //刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), //即将退出Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
  • 添加Observer监听RunLoop的所有状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 创建监听者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"kCFRunLoopEntry");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"kCFRunLoopBeforeTimers");
break;
case kCFRunLoopBeforeSources:
NSLog(@"kCFRunLoopBeforeSources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"kCFRunLoopBeforeWaiting");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"kCFRunLoopAfterWaiting");
break;
case kCFRunLoopExit:
NSLog(@"kCFRunLoopExit");
break;
default:
break;
}
});

// 添加Observer到RunLoop中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 释放
CFRelease(observer);
  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休眠之前)

  1. runloop处理items事件的总结:
  • runloop处理items以下事件调用函数的总结:
1
2
3
4
5
6
* block:__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
* timer:__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
* source0:__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
* source1:__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
* gcd主队列:_CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
* observe源:__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__

Runloop与线程的关系?

  1. 每一条线程都有一个与之对应runloop对象。

查看源码验证,线程和runloop是一一对应的关系 :

1
2
3
4
5
6
CFRunLoopRef CFRunLoopGetMain(void) {
CHECK_FOR_FORK();
static CFRunLoopRef __main = NULL; // no retain needed
if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
return __main;
}

继续查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
//获取mainloop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
//通过key value方式一一对应
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
  1. runloop保存在全局的 NSMutableDictionaryRef 字典当中,以线程为key,runloop为value

由上面的源码可验证。

  1. 主线程的Runloop系统自动获取(创建),子线程默认没有开启runloop。

  2. 线程刚创建时是没有runloop对象,runloop会在第一次获取时创建。

  3. runloop在线程销毁时销毁。

NSTimer 重点

  1. 什么是 NSTimer?

NSTimer 是一个定时器,是一个面向对象的定时器。在经过一定的时间间隔后触发,向目标对象发送指定的消息。其工作原理是将一个监听加入到系统的 runloop 中去,当系统 runloop 执行到 timer 条件的循环时,会调用 timer 一次,如果是一个重复的定时器,当timer回调函数结束之后,timer会再一次的将自己加入到runloop中去继续监听下一次timer事件。

  1. NSTimer和RunLoop的关系

NSTimer 的原理是 将定时器中的事件添加到runloop中,以实现循环的,这是因为定时器默认处于 runloop 中的 kCFRunLoopDefaultMode,主线程默认也处于此mode下,定时器这才具备了这样的能力。所以,没有runloop,NSTimer完全无法工作。

【问题1】这里提出一个经典的案例:定时器默认无法在页面滚动时执行

原因是滚动时,主线程runloop处于 UITrackingRunLoopMode,这时候,定时器所处runloop依然处于kCFRunLoopDefaultMode,就导致 定时器线程被阻塞,要解决这一个问题,我们就需要定时器无论是在 kCFRunLoopDefaultMode 还是 UITrackingRunLoopMode 下都可以正常工作,这时候就需要用到 runloop 中的伪模式 kCFRunLoopCommonMode,这并不是一个真正的mode,而是一种多mode的处理方式。具体做法如下:

1
2
3
4
NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"aaaaa");
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

【问题2】问NSTimer是怎么加入到 KCFRunloopCommonMode 下就不会造成卡顿?

答:NSTimer 通过底层函数 CFRunLoopAddTimer 加入到 items 中。然后再 runloopRun 运行的时候,通过 while循环遍历item找到timer的mode是否和当前的mode相等或等于commonMode,如果相等调用block函数返回执行。

【问题3】子线程定时器不走?

定时器的实现是基于 Runloop 的,平时我们使用定时器或许并没有对Runloop做什么操作,那是因为 主线程的runloop默认开启运行 的,如果我们在子线程中也需要重复执行某一动作,需要手动开启定时器。

1
2
3
4
5
6
queue = dispatch_queue_create("myQueue",DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(time) userInfo:nil repeat:YES];
// 子线程需要手动开启Runloop
[[NSRunLoop currentRunLoop] run];
});
  1. 定时器释放的方式
1
2
[_timer invalidate]; 
_timer = nil;

二者缺一不可。

如果是在VC中创建的 NSTimer,这种情况下,self_timer 相互强引用,VCDealloc 方法不会执行,所以定时器的销毁方法不能放在 Dealloc 中,需要放在 Dealloc 之前(viewWillDIsappear),原因我们放到最后说明。

  1. NSTimer的时间准确吗?重点

不准确,NSTimer不是采用实时机制!

原因:NSTimer的精确度略低了点,比如NSTimer的触发时间到的时候,runloop如果在阻塞状态,触发时间就会推迟到下一个runloop周期。

  1. NSTimer 创建方式

iOS10.0以前

1
2
3
4
5
6
7
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep NS_DESIGNATED_INITIALIZER;

iOS10.0以后

1
2
3
4
5
6
7
8
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block 
API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block
API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block
API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
  • 区别是有无 Block 的回调方法,block的作用就是将自身作为参数传递给block,来帮助 避免循环引用
  1. NSTimer如何避免循环引用
  • Dealloc 之前销毁定时器

  • 使用 block 方式

  • 使用 NSProxy 增加一个中间层

Runloop运行流程

  1. NSRunLoop:

是基于 CFRunLoopRef 的OC封装,提供了面向对象的 API,但不是线程安全的,CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,是线程安全的,CoreFoundation是开源的(CoreFoundation 源码地址 )

  1. 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.