原文地址:https://blog.ibireme.com/2015/05/18/runloop/
1、什么是runloop
进程就是一间工厂,线程就是工厂里的流水线,runloop就是流水线上的主管。
2、特性:
- 保持程序处于持续运行,处理用户的交互事件
- 一个线程对应一个runloop,且只能get不能创建
- 主线程的RunLoop在应用启动时自动创建,而子线程默认没有开启RunLoop
- RunLoop负责管理autorelease pools
- RunLoop负责处理消息事件,即输入源事件和计时器事件
- Runloop 存储在一个全局的可变字典里,线程是 key ,Runloop 是 value。
3、主线程中的runloop
//
// main.m
// RunLoop
//
// Created by orange on 2022/4/19.
//
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
程序启动时,作为程序入口的 main.m 中的 UIApplicationMain()会创建一个主线程,主线程会自动创建一个 runloop。runloop 本质是一个 do-while循环,只要满足条件,就一直处于循环状态。
runloop源码
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(),kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。
runLoop的5个类:
CFRunLoopRef、CFRunLoopModeRef、CFRunLoopSourceRef、CFRunLoopTimerRef、CFRunLoopObserverRef
CFRunLoopModeRef
创建RunLoop时,系统默认注册了五种mode:
kCFRunLoopDefaultMode: 默认 mode,通常主线程在这个 mode 下运行
UITrackingRunLoopMode: 追踪mode,保证Scrollview滑动顺畅不受其他 mode 影响
UIInitializationRunLoopMode: 启动程序后的过渡mode,启动完成后就不再使用
GSEventReceiveRunLoopMode: Graphic相关事件的mode,通常用不到
kCFRunLoopCommonModes: 占位用的mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用
CFRunLoopSourceRef:事件产生的地方
分为Source0和Source1两种:一般来说日常开发中我们需要关注的是source0,source1只需要了解。
- source0:诸如UIEvent(触摸,滑动等),performSelector这种需要手动触发的操作
- source1:处理系统内核的mach_msg事件(系统内部的端口事件)。诸如唤醒RunLoop或者让RunLoop进入休眠节省资源等。【具备唤醒线程的能力】
RunLoop 的核心就是一个 mach_msg(),当一个RunLoop处理完事件后,即将进入休眠时,会经历下面几步:
- 指定一个将来唤醒自己的mach_port端口
- 调用mach_msg来监听这个端口,保持mach_msg_trap状态
- 由另一个线程(比如有可能有一个专门处理键盘输入事件的loop在后台一直运行)向内核发送这个端口的msg后,mach_msg_trap状态被唤醒,RunLoop继续运行
CFRunLoopTimerRef
定时源事件,即NSTimer。其实NSTimer定时器的触发正是基于RunLoop运行的,所以使用NSTimer时添加到 [NSRunLoop currentRunLoop]。
CFRunLoopObserverRef
观察者,当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop:1
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer:2
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source:4
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠:32
kCFRunLoopAfterWaiting = (1UL << 6), // 即将从休眠中唤醒:64
kCFRunLoopExit = (1UL << 7), // 即将从Loop中退出:128
kCFRunLoopAllActivities = 0x0FFFFFFFU // 监听全部状态改变
};
总结:
上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。
获取RunLoop对象的方法
Foundation框架:
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
Core Foundation框架:
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
runloop核心逻辑
RunLoop 的核心就是系统将要休眠的时候,调用 mach_msg() 函数,mach_msg() 又调用 mach_msg_trap() 去接受消息将线程置于等待状态。例如你在模拟器里跑起一个 iOS 的 App,然后在 App 静止时点击暂停,你会看到主线程调用栈是停留在 mach_msg_trap() 这个地方。
直到被下面某一个事件唤醒:
- 一个基于 port 的Source 的事件。(主要)
- 一个 Timer 到时间了
- RunLoop 自身的超时时间到了
- 被其他什么调用者手动唤醒
AutoreleasePool
App启动后,系统在主线程 RunLoop 里注册了两个 Observer。
第一个 Observer 监视的事件是即将进入runLoop,其回调内会创建自动释放池。其优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时释放旧的池并创建新池;Exit(即将退出Loop) 时释放自动释放池,其优先级最低,保证其释放池子发生在其他所有回调之后。
在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
runloop 是怎么响应用户操作的
当一个硬件事件(触摸/锁屏/摇晃等)发生后,生成一个Event 事件,随后用 mach port 转发给App进程。随后系统注册的那个 Source1 就会触发回调,调用 _UIApplicationHandleEventQueue() 。_UIApplicationHandleEventQueue() 会把 Event 包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。
界面更新
当 UI 改变、更新时,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。
苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行更新 UI 界面。
Runloop AFNetworking 中的应用
+ (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;
}
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
//添加 MachPort 保证 RunLoop 不退出
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 loop 内;但此处添加 port 只是为了让 【RunLoop 不至于退出】,并没有用于实际的发送消息。
当需要这个后台线程执行任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中。