- Runloop学习背景
在调研卡顿监控的过程中,接触到了Runloop机制及其用法,其中一种方案是通过监听每次Runloop循环的时间来判断是否出现时间的阻塞,并同时dump出正在执行的调用栈来定位执行时间较长的函数,进一步进行优化。同时还有通过开启另外的线程给主线程发送消息,看主线程Runloop是否能够处理Source事件来进行处理返回的时间来判断阻塞状况。
- RunLoop定义
在有持续的异步任务需求场景下,我们会创建一个独立的生命周期可控的线程。每个线程都存在一个RunLoop,并且RunLoop就是控制线程生命周期并接收事件进行处理的机制。RunLoop是iOS事件响应与任务处理最核心的机制,它贯穿iOS整个系统。
通过RunLoop机制能够根据具体情况进行代码优化,达到省电,提升流畅度和响应速度的目的,从而提升用户体验。
- Runloop特性
- 主线程的RunLoop在应用启动的时候就会自动创建
- 其他线程则需要在该线程下自己启动
不能自己创建RunLoop - RunLoop并不是线程安全的,所以需要避免在其他线程上调用当前线程的RunLoop
- RunLoop负责管理autorelease pools
- RunLoop负责处理消息事件,即输入源事件和计时器事件
- RunLoop支持的消息事件(Events)
- Runloop结构
从Runloop.h文件中可以看到四个字段,如下图所示:
同时我们也可以通过以下代码来打印出Runloop的信息:
NSLog(@"%@", [NSRunLoop mainRunLoop]);
由上图可以看到一个Runloop包含了modes,而它当前只运行在一个mode下,而每个mode都包含CFRunLoopSource,CFRunLoopObserver还有一个CFRunLoopTimerRef。
- CFRunLoopMode
RunloopMode就是流水线上支持生产的产品类型,流水线在一个时刻只能在一种模式下运行,生产某一类型的产品。消息事件就是订单。
CFRunLoopModeRef
CFRunLoopModeRef 代表 RunLoop 的运行模式
每个RunLoop都包含若干个Mode,每个Mode又包含若干个 Source/Timer/Observer,每次RunLoop 启动时,只能指定其中一个 Mode,这个 Mode 被称作CurrentMode,如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入,这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响(可以通过切换 Mode,完成不同的 timer/source/observer)。
Cocoa系统默认注册了5个Mode:
NSDefaultRunLoopMode: App的默认 Mode,通常主线程是在这个 Mode 下运行(默认情况下运行)
UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响(操作 UI 界面的情况下运行)
UIInitializationRunLoopMode: 在刚启动 App 时进入的第一个 Mode,启动完成后就不再使用
GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到(绘图服务)
NSRunLoopCommonModes: 这是一个占位用的 Mode,不是一种真正的 Mode (RunLoop无法启动该模式,设置这种模式下,默认和操作 UI 界面时线程都可以运行,但无法改变 RunLoop 同时只能在一种模式下运行的本质)
CFRunLoopTimerRef
CFRunLoopTimerRef 是基于事件的触发器
CFRunLoopTimerRef 基本上就是 NSTimer,它受 RunLoop的Mode 影响
创建 Timer 有两种方式,下面的这种方式必须手动添加到 RunLoop 中去才会被调用。
// 这种方式创建的timer 必须手动添加到RunLoop中去才会被调用
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(time) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer
forMode:NSDefaultRunLoopMode];
Mode典型案例
RunLoop可以通过[acceptInputForMode:beforeDate:]和[runMode:beforeDate:]来指定在一段时间内的运行模式。如果不指定的话,RunLoop默认会运行在Default下(不断重复调用runMode:NSDefaultRunLoopMode beforDate:)
在主线程启动一个计时器Timer,然后拖动UITableView或者UIScrollView,计时器不执行。这是因为,为了更好的用户体验,在主线程中Event tracking模式的优先级最高。在用户拖动控件时,主线程的Run Loop是运行在Event tracking Mode下,而创建的Timer是默认关联为Default Mode,因此系统不会立即执行Default Mode下接收的事件。解决方法:
NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(timerFireMethod:)
userInfo:nil
repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
//或
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
[timer fire];
CFRunLoopSourceRef
CFRunLoopSourceRef 其实是事件源(输入源)
按照官方文档,Source的分类:
- Port-Based Sources: 基于端口的:跟其他线程进行交互的,Mac内核发过来一些消息
- Custom Input Sources: 自定义输入源
- Cocoa Perform Selector Sources: (self performSelector:…)
按照函数调用栈,Source的分类:
- Source0: 非基于Port的(触摸事件、按钮点击事件)
- Source1: 基于Port的,通过内核和其他线程通信,接收分发系统事件
(触摸硬件,通过 Source1 接收和分发系统事件到 Source0 处理)
可以看到在调用点击事件时,中间会经过 CFRunLoopSource0 ,这说明点击事件是属于 Source0 的。
而 Source1 是基于 Port 的,就是说,Source1 是和硬件交互的,触摸首先在屏幕上被包装成一个 event 事件,再通过 Source1 进行分发到 Source0,最后通过 Source0 进行处理。
CFRunLoopObserverRef
CFRunLoopObserverRef 是观察者,能够监听 RunLoop 的状态改变,主要监听以下几个时间节点:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity)
{
kCFRunLoopEntry = (1UL << 0), // 1 即将进入 Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 2 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 4 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 32 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 64 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 128 即将退出 Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 监听所有事件
};
// 1.创建观察者 监听 RunLoop
// 参1: 有个默认值 CFAllocatorRef :CFAllocatorGetDefault()
// 参2: CFOptionFlags activities 监听RunLoop的活动 枚举 见上面
// 参3: 重复监听 Boolean repeats YES
// 参4: CFIndex order 传0
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
// 该方法可以在添加timer之前做一些事情, 在添加source之前做一些事情
NSLog(@"%zd", activity);
});
// 2.添加观察者,监听当前的RunLoop对象
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
CFRelease(observer);
通过打印可以观察的 RunLoop 的状态
- RunLoop工作机制
在CFRunLoop.c中,可以看到RunLoop的执行代码大致如下:
{
/// 1. 通知Observers,即将进入RunLoop
/// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {
/// 2. 通知 Observers: 即将触发 Timer 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 4. 触发 Source0 (非基于port的) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 6. 通知Observers,即将进入休眠
/// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();
/// 8. 通知Observers,线程被唤醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
/// 9. 如果是被Timer唤醒的,回调Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
/// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
/// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
} while (...);
/// 10. 通知Observers,即将退出RunLoop
/// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}
- Runloop的实例运用
通过Runloop实现卡顿的监控
主线程绝大部分计算或者绘制任务都是以Runloop为单位发生。单次Runloop如果时长超过16ms,就会导致UI体验的卡顿。那如何检测单次Runloop的耗时呢?
Runloop的生命周期及运行机制虽然不透明,但苹果提供了一些API去检测部分行为。我们可以通过如下代码监听Runloop每次进入的事件:
- (void)setupRunloopObserver{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
CFRunLoopRef runloop = CFRunLoopGetCurrent();
CFRunLoopObserverRef enterObserver;
enterObserver = CFRunLoopObserverCreate(CFAllocatorGetDefault(),
kCFRunLoopEntry | kCFRunLoopExit,
true,
-0x7FFFFFFF,
_RunloopObserverCallBack,
NULL);
CFRunLoopAddObserver(runloop, enterObserver, kCFRunLoopCommonModes);
CFRelease(enterObserver);
});
}
static void _RunloopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
switch (activity) {
case kCFRunLoopEntry: {
NSLog(@"Enter Runloop...");
}
break;
case kCFRunLoopExit: {
NSLog(@"Leave Runloop...");
}
break;
default: break;
}
最终kCFRunLoopExit的时间,减去kCFRunLoopEntry的时间,即为一次Runloop所耗费的时间,这样就能找出大于16ms的runloop。
通过Runloop解决界面图片过多的滑动卡顿问题
+ (void)_registerRunLoopWorkDistributionAsMainRunloopObserver:(DWURunLoopWorkDistribution *)runLoopWorkDistribution {
static CFRunLoopObserverRef defaultModeObserver;
_registerObserver(kCFRunLoopBeforeWaiting, defaultModeObserver, NSIntegerMax - 999, kCFRunLoopDefaultMode, (__bridge void *)runLoopWorkDistribution, &_defaultModeRunLoopWorkDistributionCallback);
}
static void _registerObserver(CFOptionFlags activities, CFRunLoopObserverRef observer, CFIndex order, CFStringRef mode, void *info, CFRunLoopObserverCallBack callback) {
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopObserverContext context = {
0,
info,
&CFRetain,
&CFRelease,
NULL
};
observer = CFRunLoopObserverCreate( NULL,
activities,
YES,
order,
callback,
&context);
CFRunLoopAddObserver(runLoop, observer, mode);
CFRelease(observer);
}
static void _runLoopWorkDistributionCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
DWURunLoopWorkDistribution *runLoopWorkDistribution = (__bridge DWURunLoopWorkDistribution *)info;
if (runLoopWorkDistribution.tasks.count == 0) {
return;
}
BOOL result = NO;
while (result == NO && runLoopWorkDistribution.tasks.count) {
//加入Runloop分配的加载图片代码块
RunLoopWorkDistributionUnit unit = runLoopWorkDistribution.tasks.firstObject;
result = unit();
}
}
static void _defaultModeRunLoopWorkDistributionCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
_runLoopWorkDistributionCallback(observer, activity, info);
}
其原理是通过监听Runloop的运行状态,并在kCFRunLoopBeforeWaiting的时候也就是下一个Loop执行代码前去执行耗时操作,减少主线程Loop的负担。
AFNetworking中network的单开请求
下面是AFNetworking中的一部分源码
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 通过Runloop监听某个port,保持Thread不退出
[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;
}