「iOS」——RunLoop学习
底层学习
- iOS--RunLoop学习
- RunLoop的概念
- RunLoop与线程的关系
- RunLoop的结构
- Mode
- Observer
- Timer
- Source
- RunLoop 执行流程
- RunLoop 的应用
- 1.AutoreleasePool是什么时候释放的
- 2.触控事件的响应
- 3.刷新界面
- 4.线程保活
- 小知识
- mach Port
- **Toll-Free Bridging(对象桥接)详解**
- `CFRunLoopTimer` 和 `NSTimer` 的桥接
iOS–RunLoop学习
RunLoop的概念
一般来讲,一个线程只能执行一次任务,任务完成后按成就会退出。但如果我们需要一个机制,让线程能随时处理事件但不退出。就需要这样一个dowhilt循环。
do {//获取消息//处理消息
} while (消息 != 退出)
这种模型通常被称为Event Loop,在很多系统和框架都有实现。例如:Node.js 的事件处理,Windows 程序的消息循环,而在macOS、iOS中就是Run Loop。
实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。
实际上,RunLoop就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面Event Loop的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。
在iOS系统中,提供了两个EventLoop 的具体实现:
NSRunLoop
和 CFRunLoopRef
CFRunLoopRef
是在 CoreFoundation
框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
NSRunLoop
是基于 CFRunLoopRef
的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
RunLoop与线程的关系
RunLoop 是和线程一一对应的,app 启动之后,程序进入了主线程,苹果帮我们在主线程启动了一个 RunLoop。如果是我们开辟的线程,就需要自己手动开启 RunLoop,而且,如果你不主动去获取 RunLoop,那么子线程的 RunLoop 是不会开启的,它是懒加载的形式。
另外苹果不允许直接创建 RunLoop,只能通过 CFRunLoopGetMain()
和 CFRunLoopGetCurrent()
去获取,其内部会创建一个 RunLoop 并返回给你(子线程),而它的销毁是在线程结束时。
这两个函数内部的逻辑大概是这样:
其中pthread_main_thread_np() 或 [NSThread mainThread] 获取主线程; pthread_self() 或 [NSThread currentThread] 获取当前线程。
/// 全局的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之间是一一对应的,其关系保存在一个全局的Dictionary中。
线程刚创建时并没有RunLoop,主动获取后系统会为我们创建一个RunLoop。在线程结束时RunLoop会被销毁。
CFRunLoopGetCurrent
函数便是获取当前线程的CFRunLoop
对象,如果不存在的话会则会创建一个。CFRunLoopGetMain
则是获取主线程的CFRunLoop
对象。
就有如下代码:
- NSRunloop类是Fundation框架中Runloop的对象,并且NSRunLoop是基于CFRunLoopRef的封装,提供了面向对象的API,但是这些API不是线程安全的。
- CFRunLoopRef类是CoreFoundation框架中Runloop的对象,并且其提供了纯C语言函数的API,所有这些API都是线程安全。
// Foundation
NSRunLoop *runloop = [NSRunLoop currentRunLoop]; // 获得当前RunLoop对象
NSRunLoop *runloop = [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象// Core Foundation
CFRunLoopRef runloop = CFRunLoopGetCurrent(); // 获得当前RunLoop对象
CFRunLoopRef runloop = CFRunLoopGetMain(); // 获得主线程的RunLoop对象
RunLoop的结构
RunLoop里面有五个类:
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
其结构图如下图所示:
Mode
Mode也就是模式,一个RunLoop当前只能处于某一种Mode中。Mode之间互不干扰,A Mode中发生的事情与B mode无关,尽管他们在一个RunLoop中。而苹果的滚动和默认状态分别对应两种不同的Mode,因此他们非常丝滑。
可以自定义 Mode,但是基本不会这样,苹果也为我们提供了几种 Mode:
-
kCFRunLoopDefaultMode
:app默认Mode,通常主线程是在这个Mode下运行 -
UITrackingRunLoopMode
:界面追踪Mode,比如ScrollView滚动时就处于这个Mode -
UIInitializationRunLoopMode
:刚启动app时进入的第一个Mode,启动完后不再使用 -
GSEventReceiveRunLoopMode
:接受系统事件的内部Mode,通常用不到 -
kCFRunLoopCommonModes
:不是一个真正意义上的Mode,但是如果你把事件丢到这里来,那么不管你当前处于什么Mode,都会触发你想要执行的事件。 -
NSModalPanelRunLoopMode:当应用程序显示一个模态对话框时使用此模式。在此模式下,RunLoop只处理与模态面板相关的事件,忽略其他非模态事件,确保用户只能与模态面板交互。
程序运行后,画面静止没有操作时,就处于 kCFRunLoopDefaultMode
状态,当滚动它时,就会处于 UITrackingRunLoopMode
状态。如果你想要这两个状态能同时相应一件事情,要么同时添加到两种Mode里,要么把这件事情放到 kCFRunLoopCommonModes
中去执行
CFRunLoop对外暴露的管理Mode接口只有下面2个:
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
Mode暴露的管理mode item的接口有下面几个:
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
-
只能通过 mode name 来操作内部的 mode,当你传入一个新的 mode name 但 RunLoop 内部没有对应 mode时,RunLoop会自动帮你创建对应的 CFRunLoopModeRef。
-
对于一个 RunLoop 来说,其内部的 mode 只能增加不能删除。
Observer
CFRunLoopObserverRef是观察者,可以观察RunLoop的各种状态,每个Observer都包含一个回调(函数指针),当RunLoop的状态发生变化时,观察者就能通过回调接受这个变化。
苹果用一个枚举值来表示RunLoop的状态:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {kCFRunLoopEntry = (1UL << 0), // 即将进入 LoopkCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 TimerkCFRunLoopBeforeSources = (1UL << 2), // 即将处理 SourcekCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒kCFRunLoopExit = (1UL << 7), // 即将退出 LoopkCFRunLoopAllActivities = 0x0FFFFFFFU // 所有的状态
};
这里通过RunLoop的执行流程,来了解Observer的工作,具体如下:
由上图也可知道,Timer和Source就是RunLoop要干的活。
Timer
从结构的那张图可以看到,Mode中有一个Timer的数组,一个Mode中可以有多个Timer。Timer其实就是定时器,它的工作原理是:生成一个 Timer,确定要执行的任务,和多久执行一次,将其注册到 RunLoop 中,RunLoop 就会根据你设定的时间点,当时间点到时,去执行这个任务,如果它正在休眠,那么就会先唤醒 RunLoop,再去执行。
但是,这个时间点并不是非常准确。因为RunLoop的执行是有一个顺序的,要处理的事情也有先后顺序。如果时间点到了,RunLoop会将Timer要执行的事情添加到待执行清单,但是也需要等待执行清单前面的事情执行完了才会执行到Timer的事情。
对于NSTimer,当我们用scheduledTimerWithTimeInterval
方法初始化定时器时
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
系统会将NSTimer自动加入NSDefaultRunLoopMode模式中,所以他就等同于下面代码:
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
Source
Source是另外一种RunLoop要干的活。RunLoop中定义了两种版本的Source,一个是Source0,一个是Source1
- Source0:处理 App 内部事件,App 自己负责管理(触发),如
UIEvent
、CFSocket
。
Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
- Source1:由 RunLoop 内核管理,Mach port 驱动,如
CFMackPort
、CFMessagePort
。
这里Source1比较不好理解。
Mach Port 是 macOS/iOS 底层(基于 Mach 微内核)的 进程间通信(IPC)机制,用于线程、进程或内核之间的安全消息传递。它是 XNU 内核(iOS/macOS 的核心)的核心组件之一,也是 Source1的底层驱动。
所以简而言之,Source1其实就是进程间或者线程间通信的一种方式。比如在同一个RunLoop下,线程A想要给线程B发送C,这就需要通过port进行传输,然后系统将传输的东西包装成Source1,在线程A中监听port是否有东西传输过来,接收到后,唤醒RunLoop进行处理。
在源码上,他们不同的点在于:Source1的结构体中会带有mach_port_t,可以接受内核消息并触发回调。
typedef struct {CFIndex version;void * info;const void *(*retain)(const void *info);void (*release)(const void *info);CFStringRef (*copyDescription)(const void *info);Boolean (*equal)(const void *info1, const void *info2);CFHashCode (*hash)(const void *info);
#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)mach_port_t (*getPort)(void *info);void * (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#elsevoid * (*getPort)(void *info);void (*perform)(void *info);
#endif
} CFRunLoopSourceContext1;
我们还可以添加Run Loop Source
在此之前,我们看一看Run Loop Mode提供给我们管理mode item的接口
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopAddSource的代码结构如下:
//添加source事件
void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) { /* DOES CALLOUT */CHECK_FOR_FORK();if (__CFRunLoopIsDeallocating(rl)) return;if (!__CFIsValid(rls)) return;Boolean doVer0Callout = false;__CFRunLoopLock(rl);//如果是kCFRunLoopCommonModesif (modeName == kCFRunLoopCommonModes) {//如果runloop的_commonModes存在,则copy一个新的复制给setCFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;//如果runl _commonModeItems为空if (NULL == rl->_commonModeItems) {//先初始化rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);}//把传入的CFRunLoopSourceRef加入_commonModeItemsCFSetAddValue(rl->_commonModeItems, rls);//如果刚才set copy到的数组里有数据if (NULL != set) {CFTypeRef context[2] = {rl, rls};/* add new item to all common-modes *///则把set里的所有mode都执行一遍__CFRunLoopAddItemToCommonModes函数CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);CFRelease(set);}//以上分支的逻辑就是,如果你往kCFRunLoopCommonModes里面添加一个source,那么所有_commonModes里的mode都会添加这个source} else {//根据modeName查找modeCFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);//如果_sources0不存在,则初始化_sources0,_sources0和_portToV1SourceMapif (NULL != rlm && NULL == rlm->_sources0) {rlm->_sources0 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);rlm->_sources1 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);rlm->_portToV1SourceMap = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, NULL);}//如果_sources0和_sources1中都不包含传入的sourceif (NULL != rlm && !CFSetContainsValue(rlm->_sources0, rls) && !CFSetContainsValue(rlm->_sources1, rls)) {//如果version是0,则加到_sources0if (0 == rls->_context.version0.version) {CFSetAddValue(rlm->_sources0, rls);//如果version是1,则加到_sources1} else if (1 == rls->_context.version0.version) {CFSetAddValue(rlm->_sources1, rls);__CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info);if (CFPORT_NULL != src_port) {//此处只有在加到source1的时候才会把souce和一个mach_port_t对应起来//可以理解为,source1可以通过内核向其端口发送消息来主动唤醒runloopCFDictionarySetValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port, rls);__CFPortSetInsert(src_port, rlm->_portSet);}}__CFRunLoopSourceLock(rls);//把runloop加入到source的_runLoops中if (NULL == rls->_runLoops) {rls->_runLoops = CFBagCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeBagCallBacks); // sources retain run loops!}CFBagAddValue(rls->_runLoops, rl);__CFRunLoopSourceUnlock(rls);if (0 == rls->_context.version0.version) {if (NULL != rls->_context.version0.schedule) {doVer0Callout = true;}}}if (NULL != rlm) {__CFRunLoopModeUnlock(rlm);}}__CFRunLoopUnlock(rl);if (doVer0Callout) {// although it looses some protection for the source, we have no choice but// to do this after unlocking the run loop and mode locks, to avoid deadlocks// where the source wants to take a lock which is already held in another// thread which is itself waiting for a run loop/mode lockrls->_context.version0.schedule(rls->_context.version0.info, rl, modeName); /* CALLOUT */}
}
传入的三个参数分别是:
- RunLoop对象
- 添加的事件源即Source
- RunLoop Mode的名称
通过添加source的这段代码可以得出如下结论:
-
如果modeName传入kCFRunLoopCommonModes,则该source会被保存到RunLoop的_commonModeItems中
-
如果modeName传入kCFRunLoopCommonModes,则该source会被添加到所有commonMode中
-
如果modeName传入的不是kCFRunLoopCommonModes,则会先查找该Mode,如果没有,会创建一个
-
同一个source在一个mode中只能被添加一次
RunLoop 执行流程
-
通知 Observer 已经进入 RunLoop
-
通知 Observer 即将处理 Timer
-
通知 Observer 即将处理 Source0
-
处理 Source0
-
如果有 Source1,跳到第 9 步(处理 Source1)
-
通知 Observer 即将休眠
-
将线程置于休眠状态,直到发生以下事件之一
-
有 Source0
-
Timer 到时间执行
-
外部手动唤醒
-
为 RunLoop 设定的时间超时
-
-
通知 Observer 线程刚被唤醒
-
处理待处理事件
-
如果是 Timer 事件,处理 Timer 并重新启动循环,跳到 2
-
如果 Source1 触发,处理 Source1
-
如果 RunLoop 被手动唤醒但尚未超时,重新启动循环,跳到 2
-
-
通知 Observer 即将退出 Loop
实际上 RunLoop 内部就是一个 do-while
循环。当你调用 CFRunLoopRun()
时,线程就会一直停留在这个循环里,直到超时或手动停止,该函数才会返回。
但是默认时间是一个巨大的数,可以理解为无穷大即不会超时。
RunLoop 进入休眠所调用的函数是 mach_msg()
,其内部会进行一个系统调用,然后内核会将线程置于等待状态,所以这是一个系统级别的休眠。因此RunLoop在休眠时不会占用CPU。
RunLoop 的应用
1.AutoreleasePool是什么时候释放的
自动释放池的释放时机和RunLoop有关。苹果在主线程的RunLoop中注册了两个Observer。
第一个 Observer,监听一个事件,就是 Entry
,即将进入 Loop 的时候,创建一个自动释放池,并且给了一个最高的优先级,保证自动释放池的创建发生在其他回调之前,这是为了保证能管理所有的引用计数。
第二个 Observer,监听两个事件,一个 BeforeWaiting
,一个 Exit
,BeforeWaiting
的时候,干两件事,一个释放旧的池,然后创建一个新的池,所以这个时候,自动释放池就会有一次释放的操作,是在 RunLoop 即将进入休眠的时候。Exit
的时候,也释放自动释放池,这里也有一次释放的操作。
即:
- 进入RunLoop,先创建一个自动释放池
- RunLoop休息**
kCFRunLoopBeforeWaiting
(即将休眠)**,释放的当前的自动释放池,创建新的自动释放池 - RunLoop退出**
kCFRunLoopExit
(退出 RunLoop)**,释放当前的自动释放池
2.触控事件的响应
苹果会提前在App内注册一个Source1来监听系统事件。
比如,当一个 触摸/锁屏/摇晃 之类的系统事情产生,系统会先包装,包装好了,通过 mach port 传输给需要的 App 进程,传输后,提前注册的 Source1 就会触发回调,然后由 App 内部再进行分发。该行为发生在kCFRunLoopAfterWaiting
阶段
- 注册一个 Source1 用于接收系统事件
- 硬件事件发生
- IOKit.framework 生成 IOHIDEvent 事件并由 SpringBoard 接收
- SpringBoard 用 mach port 转发给需要的 App
- 注册的 Source1 触发回调
- 回调中将 IOHIDEvent 包装成 UIEvent 进行处理或分发
3.刷新界面
我们都知道改变 UI 的参数后,它并不会立马刷新。而它的刷新,也是通过 RunLoop 来实现。
当 UI 需要更新,先标记一个 dirty,然后提交到一个全局容器中去,调用 setNeedsLayout
/setNeedsDisplay
后,系统将视图标记为待更新。。然后,在休眠前(BeforeWaiting
)或退出时(Exit
),调用 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
触发界面渲染。
最终执行 CA::Transaction::commit()
提交所有 UI 变更。
4.线程保活
线程保活问题,从字面意思上就是保护线程的生命周期不结束.正常情况下,当线程执行完一次任务之后,需要进行资源回收,但是当有一个任务,随时都有可能去调用,如果在子线程去执行,并且让子线程一直存活着,为了避免来回多次创建毁线程的动作, 降低性能消耗。
通俗一点,我们可以创建一个常驻线程,其RunLoop始终运行,来实现这个功能。
例如第三方库AFNetworking
Runloop启动前必须要至少一个Timer/Observer/Source,所以AFNetworking在[runLoop run]
之前创建了NSMachPort添加进去了.通常情况下调用者需要持有这个NSMachPort并在外部线程通过这个port发送消息到loop内
+ (void)networkRequestThreadEntryPoint:(id)__unused object {@autoreleasepool {[[NSThread currentThread] setName:@"AFNetworking"];NSRunLoop *runLoop = [NSRunLoop currentRunLoop];[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];[runLoop run];}
}+ (NSThread *)networkRequestThread {static NSThread *_networkRequestThread = nil;static dispatch_once_t oncePredicate;dispatch_once(&oncePredicate, ^{_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];[_networkRequestThread start];});return _networkRequestThread;
}
小知识
mach Port
Mach Port 是 macOS/iOS 底层(基于 Mach 微内核)的 进程间通信(IPC)机制,用于线程、进程或内核之间的安全消息传递。它是 XNU 内核(iOS/macOS 的核心)的核心组件之一,也是 RunLoop 事件源(Source1)的底层驱动。
Toll-Free Bridging(对象桥接)详解
在 Apple 的框架中,Toll-Free Bridging 是一种允许 Core Foundation 对象(C 语言)和 Foundation 对象(Objective-C)之间无缝转换 的机制。
- 特点:某些 Core Foundation 和 Foundation 的类实际上是同一底层实现的两种接口,可以 直接强制转换 而无需额外内存操作。
- 内存管理:转换后的对象仍遵循原有的内存管理规则(ARC 或手动
CFRetain/CFRelease
)。
CFRunLoopTimer
和 NSTimer
的桥接
CFRunLoopTimerRef
:Core Foundation 的 C 语言结构体,用于定时任务。NSTimer
:Foundation 的 Objective-C 类,封装了定时器功能。- 桥接关系:
// Core Foundation → Foundation
CFRunLoopTimerRef cfTimer = ...;
NSTimer *nsTimer = (__bridge NSTimer *)cfTimer;// Foundation → Core Foundation
NSTimer *nsTimer = ...;
CFRunLoopTimerRef cfTimer = (__bridge CFRunLoopTimerRef)nsTimer;