一、简介
一、什么是runloop
runloop是一个通过管理内部循环来接收和处理App运行期间消息事件的一个对象
事件(比如说触摸事件、UI刷新事件、定时器事件、Selector事件)和消息,
二、三个重要的作用
1.保持程序的持续运行 保活
2.处理App中的各种事件(比如触摸事件、定时器事件等)
3.节省CPU资源,提高程序性能:该做事时做事,该休息时休息
三、可以检测的三种事件类型
分别是CFRunLoopSource、CFRunLoopTimer、CFRunLoopObserver
要让一个RunLoop跑起来还需要run loop modes,每一个source, timer和observer添加到RunLoop中时必须要与一个模式(CFRunLoopMode)相关联才可以运行。
RunLoop主要处理以下6类事件 主线程所有函数 都是从6个函数之一 调起
1、RunLoopObserver 触发
2、消息通知、非延迟的perform、dispatch调用、block回调、KVO
3、MAIN_DISPATCH_QUEUE - 主调度队列
4、TIMER_CALLBACK - 延迟的perform, 延迟dispatch调用
5、Source0 - 处理App内部事件、App自己负责管理(触发),如UIEvent、CFSocket。普通函数调用,系统调用
6、Source1 - 由RunLoop和内核管理,Mach port驱动,如CFMachPort、CFMessagePort
下面具体介绍
CFRunLoopSourceRef
source是RunLoop的数据源(输入源)的抽象类(protocol),
Source有两个版本:Source0 和 Source1
- source0:处理App内部事件,App自己负责管理(触发runloop),如UIEvent,CFSocket;只包含了一个回调(函数指针),使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。 处理App内部事件,App自己负责管理(出发),如UIEvent(Touch事件等,GS发起到RunLoop运行再到事件回调到UI)、CFSocketRef。
- Source1:由RunLoop和内核管理,由mach_port驱动(特指port-based事件),如CFMachPort(轻量级的进程间通信的方式,NSPort就是对它的封装,还有Runloop的睡眠和唤醒就是通过它来做的)、CFMessagePort、NSSocketPort。特别要注意一下Mach port的概念,它是一个轻量级的进程间通讯的方式,可以理解为它是一个通讯通道,假如同时有几个进程都挂在这个通道上,那么其它进程向这个通道发送消息后,这些挂在这个通道上的进程都可以收到相应的消息。这个Port的概念非常重要,因为它是RunLoop休眠和被唤醒的关键,它是RunLoop与系统内核进行消息通讯的窗口。
CFRunLoopTimerRef
CFRunLoopObserverRef
观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个
enum CFRunLoopActivity {
kCFRunLoopEntry = (1 << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1 << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1 << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1 << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1 << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1 << 7), // 即将退出Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 包含上面所有状态
};
typedef enum CFRunLoopActivity CFRunLoopActivity;
这里要提一句的是,timer和source1(也就是基于port的source)可以反复使用,比如timer设置为repeat,port可以持续接收消息,而source0在一次触发后就会被runloop移除。
上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。
RunLoop的Mode
CFRunLoopMode 和 CFRunLoop的结构大致如下:
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};
每次调用 RunLoop的主函数时,只能指定其中一个Mode,这个Mode被称作CurrentMode。
如果需要切换 Mode,只能退出Loop,再重新指定一个Mode进入。
- kCFDefaultRunLoopMode
- App的默认Mode,通常主线程是在这个Mode下运行
- UITrackingRunLoopMode
- 界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响
- UIInitializationRunLoopMode
- 在刚启动App时第进入的第一个Mode,启动完成后就不再使用
- GSEventReceiveRunLoopMode
- 接受系统事件的内部Mode,通常用不到
- kCFRunLoopCommonModes
- 这是一个占位用的Mode,不是一种真正的Mode
RunLoop运行机制
RunLoop的挂起和唤醒
Runloop的挂起,实际上是指定一个端口,给内核发消息,这里是一个等待消息,就是等待被唤醒。
1、指定用于被唤醒的mach port端口;
2、调用mach_msg监听唤醒端口,被唤醒前,系统内核将这个线程挂起,停留在mach_msg_trap状态;
3、由另一个线程(或者另一个进程中的某个线程)向内核发送这个端口的msg后,trap状态被唤醒,RunLoop继续开始干活。
这接种情况下会被唤醒
- 存在Source0被标记为待处理,系统调用CFRunLoopWakeUp唤醒线程处理事件
- 定时器时间到了
- RunLoop自身的超时时间到了
- RunLoop外部调用者唤醒
RunLoop 的核心就是一个 mach_msg() ,RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。
RunLoop和线程
线程的作用是用来执行特定的一个或多个任务,但是在默认情况下,线程执行完之后就会退出,就不能再执行任务了。RunLoop让线程能够处理任务,并不退出
1. iOS开发中会遇到两个线程对象: pthread_t和NSThread,pthread_t和NSThread 是一一对应的
pthread_main_thread_np()或 [NSThread mainThread]来获取主线程;
pthread_self()或[NSThread currentThread]来获取当前线程。
CFRunLoop 是基于 pthread 来管理的。
2. 线程与RunLoop是一一对应的关系(对应关系保存在一个全局的Dictionary里)
RunLoop实例
实例1:RunLoop角度,如何处理一次点击事件的
1. 苹果注册了一个source1 (基于mach port的)用来接收系统事件,其回调__IOHIDEventSystemClientQueueCallback()。
****为什么是source1 ,不是处理App事件的Source0?****
Source1 接收IOHIDEvent,之后再回调__IOHIDEventSystemClientQueueCallback()内触发的Source0,Source0再触发的 _UIApplicationHandleEventQueue()。
2. 当硬件事件(触摸/锁屏/摇晃等)发生后,先由IOKit.framework 生成 IOHIDEvent事件,并由springBoard接收。
3. 随后mach port 转发给需要的APP进程。苹果注册的source1会触发回调,Source0再触发_UIApplicationHandleEventQueue() 进行应用内部的分发,并处理成UIEvent,识别手势、屏幕旋转静音等 ,发送给UIwindow。
4. UIWindow调用- (UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event方法, 遍历每一个子视图,查找出事件产生的视图
实例2: GCD如何实现调回主线程执行的,GCD由 子线程 返回到 主线程
当调用 dispatch_async(dispatch_get_main_queue(), block) 时,
libDispatch 会向主线程的 RunLoop 发送消息,会触发 RunLoop 的 Source 1 事件,RunLoop会被唤醒,并从消息中取得这个 block,并在回调里执行这个 block。
Runloop只处理主线程的block,dispatch 到其他线程仍然是由 libDispatch 处理的。
实例3:实现一个常驻线程
- 为当前线程开启一个RunLoop
- 向该RunLoop中添加一个Port/Source等维持RunLoop的事件循环
- 启动该RunLoop
AFN中常驻线程的最好示例
1. currentRunLoop方法是获得RunLoop,如果没有就会创建RunLoop
2. [thread start]为了让线程活下来,如果没有这一行代码,RunLoop并不会挂起,线程运行完就会退出;
实例4:TableView延迟加载图片的新思路:
UIImage *downLoadImage = ...;
[self.avatarImageView performSelector:@selector(setImage:)
withObject:downloadImage
afterDelay:0
inModes:@[NSDefaultRunLoopMode]];
因为tableview在滑动时,所在的runloop的mode为UITrackingRunLoopMode,
这是两种完全不同的Mode,且水火不容。
滑动时,只会执行设置为UITrackingRunLoopMode的runloop,故而设置为NSDefaultRunLoopMode的runloop就不会执行,只有停止滑动的时候才会自动切换到NSDefaultRunLoopMode,这时就会去设置图片了。
实例5:接收处理Crash
2、EXC_BAD_ACCESS是访问已被释放的内存导致,野指针错误。
由 SIGABRT 引起的Crash 是系统发这个signal给App,程序收到这个signal后,就会把主线程的RunLoop杀死,程序就Crash了 (该例只针对 SIGABRT引起的Crash有效。)
CFRunLoopRef runloop = CFRunLoopGetCurrent();
//获取所有Mode,因为可能有很多Mode,每个Mode都需要跑,此处可以选择提交下崩溃信息之类的
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"程序崩溃了" message:@"崩溃信息"
delegate:nil cancelButtonTitle:@"取消" otherButtonTitles:nil];
[alertView show];
NSArray *allModes = CFBridgingRelease(CFRunLoopCopyAllModes(runloop));
while (1) {
//快速切换Mode
for (NSString *mode in allModes) {
CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
}
}实例6:NSRunloop 卡顿监控
1. 背景
先说屏幕,苹果移动设备屏幕,即显示器的刷新频率是60HZ,这是硬件设备决定。
显示器显示的内容是由显卡渲染的,显卡渲染一帧并显示到显示器上的时间点,程序可以通过CADisplayLink捕获。
2. 导致丢帧原因
渲染数据从哪来。
2.1. App主线程在CPU中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。
随后CPU会将计算好的内容提交到GPU去
2.2. GPU进行变换、合成、渲染。把渲染结果提交到帧缓冲区去。
CPU和GPU不论哪个阻碍了显示流程,都会造成掉帧现象。
3. 该怎么做
3.1 在程序中能做的只有监控CPU了,GPU无能为力,而且通过观察instruments,会发现除了离屏渲染,其他情况下GPU并不是瓶颈,平时开发中尽量避免即可。
3.2 主线程上的CPU工作都是在RunLoop中进行的,从下面的伪代码可以看到主要计算工作都在kCFRunLoopAfterWaiting和下一次kCFRunLoopBeforeWaiting之间。(刚从休眠中唤醒 到 准备进入休眠)
3.3 所以可以将监控kCFRunLoopAfterWaiting开始到下一次kCFRunLoopBeforeWaiting结束的运行时间。(这段时间 不是一次完整的runloop,但是这是autoreleasepool一次重载时机)
综上,为主线程的 RunLoop 添加一个 Observer ,来检测 RunLoop 的运行情况。
CFRunLoopObserverContext context = {0, NULL, NULL, NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
在回调中,使用mach_absolute_time()记录kCFRunLoopAfterWaiting的时间点,在下一次kCFRunLoopBeforeWaiting时计算RunLoop的运行时间,如果超时,可以根据需求处理,比如dump函数堆栈,并上传监控服务器等。示例中使用的是断言处理。
static const NSTimeInterval kRunLoopThreshold = 0.3;
//粗略计算一下,以运行时低于40帧为卡,则会掉20帧,卡住的时间约为320ms。
//可以假设如果runloop执行超过了0.3,主线程无法将计算好的内容提交给 GPU,会造成卡顿。
static uint64_t kStartTime = 0;
static void runLoopObserverCallBack(CFRunLoopObserverRef observer,
CFRunLoopActivity activity, void *info)
{
switch (activity) {
case kCFRunLoopAfterWaiting:
kStartTime = mach_absolute_time();
break;
case kCFRunLoopBeforeWaiting:
if (kStartTime != 0 ) {
uint64_t elapsed = mach_absolute_time() - kStartTime;
mach_timebase_info_data_t timebase;
mach_timebase_info(&timebase);
NSTimeInterval duration = elapsed * timebase.numer / timebase.denom / 1e9;
if (duration > kRunLoopThreshold) {
assert(0);
}
}
break;
default:
break;
}
}
上述计算中,在kCFRunLoopBeforeWaiting时每次都需要将mach_absolute_time()的时间转换成秒,会比较浪费,可以通过context参数传进来。