runloop
runloop
是什么?
- 系统内部存在管理事件的循环机制
runloop
是利用这个循环,管理消息和事件的对象。
runloop
是否等于 while(1) { do something ... }
?
- 不是
while(1)
是一个忙等的状态,需要一直占用资源。runloop
没有消息需要处理时进入休眠状态,消息来了,需要处理时才被唤醒。
runloop
的基本模式
- iOS中有五种
runLoop
模式 UIInitializationRunLoopMode
(启动后进入的第一个Mode
,启动完成后就不再使用,切换到kCFRunLoopDefaultMode
)kCFRunLoopDefaultMode
(App的默认Mode
,通常主线程是在这个Mode
下运行)UITrackingRunLoopMode
(界面跟踪Mode
,用于ScrollView
追踪触摸滑动,保证界面滑动时不受其他Mode
影响)NSRunLoopCommonModes
(这是一个伪Mode
,等效于NSDefaultRunLoopMode
和NSEventTrackingRunLoopMode
的结合 )GSEventReceiveRunLoopMode
(接受系统事件的内部Mode
,通常用不到)
runLoop
的基本原理
- 系统中的主线程会默认开启
runloop
检测事件,没有事件需要处理的时候runloop
会处于休眠状态。 - 一旦有事件触发,例如用户点击屏幕,就会唤醒
runloop
使进入监听状态,然后处理事件。 - 事件处理完成后又会重新进入休眠,等待下一次事件唤醒
runloop
和线程的关系
runloop
和线程一一对应。- 主线程的创建的时候默认开启
runloop
,为了保证程序一直在跑。 - 支线程的
runloop
是懒加载的,需要手动开启。
runloop
事件处理流程
- 事件会触发
runloop
的入口函数CFRunLoopRunSpecific
,函数内部首先会通知observer
把状态切换成kCFRunLoopEntry
,然后通过__CFRunLoopRun
启动runloop
处理事件 __CFRunLoopRun
的核心是是一个do - while
循环,循环内容如下
runloop
是怎么被唤醒的
- 没有消息需要处理时,休眠线程以避免资源占用。从用户态切换到内核态,等待消息;
- 有消息需要处理时,立刻唤醒线程,回到用户态处理消息;
source0
通过屏幕触发直接唤醒source0
通过调用mach_msg()
函数来转移当前线程的控制权给内核态/用户态。
什么是用户态、核心态
- 内核态:运行操作系统程序 ,表示一个应用进程执行系统调用后,或I/O 中断,时钟中断后,进程便处于内核执行
- 用户态:运行用户程序 ,表示进程正处于用户状态中执行
runloop的状态
CFRunLoopObserverRef observerRef = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry: NSLog(@"runloop启动"); break;
case kCFRunLoopBeforeTimers: NSLog(@"runloop即将处理timer事件"); break;
case kCFRunLoopBeforeSources: NSLog(@"runloop即将处理sources事件"); break;
case kCFRunLoopBeforeWaiting: NSLog(@"runloop即将进入休眠"); break;
case kCFRunLoopAfterWaiting: NSLog(@"runloop被唤醒"); break;
case kCFRunLoopExit: NSLog(@"runloop退出"); break;
default: break;
}
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observerRef, kCFRunLoopDefaultMode);
}
复制代码
runLoop
卡顿检测的方法
NSRunLoop
处理耗时主要下面两种情况kCFRunLoopBeforeSources
和kCFRunLoopBeforeWaiting
之间kCFRunLoopAfterWaiting
之后
- 上述两个时间太长,可以判定此时主线程卡顿
- 可以添加
Observer
到主线程Runloop
中,监听Runloop
状态切换耗时,监听卡顿- 用一个
do-while
循环处理路基,信号量设置阈值判断是否卡顿 dispatch_semaphore_wait
返回值 非0 表示timeout
卡顿发生- 获取卡顿的堆栈传至后端,再分析
- 用一个
怎么启动一个常驻线程
// 创建线程
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(play) object:nil];
[thread start];
// runloop保活
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
// 处理事件
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:NO];
复制代码
计时器
NSTimer、CADisplayLink、dispatch_source_t
的优劣
NSTimer
- 优点在于使用的是
target-action
模式,简单好用 - 缺点是容易不小心造成循环引用。需要依赖
runloop
,runloop
如果被阻塞就要延迟到下一次runloop
周期才执行,所以时间精度上也略为不足
- 优点在于使用的是
CADisplayLink
- 优点是精度高,每次刷新结束后都调用,适合不停重绘的计时,例如视频
- 缺点容易不小心造成循环引用。
selector
循环间隔大于重绘每帧的间隔时间,会导致跳过若干次调用机会。不可以设置单次执行。
dispatch_source_t
- 基于
GCD
,精度高,不依赖runloop
,简单好使,最喜欢的计时器 - 需要注意的点是使用的时候必须持有计时器,不然就会提前释放。
- 基于
NSTimer在子线程执行会怎么样?
NSTimer
在子线程调用需要手动开启子线程的runloop
[[NSRunLoop currentRunLoop] run];
NSTimer
为什么不准?
如果runloop
正处在阻塞状态的时候NSTimer
到达触发时间,NSTimer
的触发会被推迟到下一个runloop
周期
NSTimer
的循环引用?
timer
和target
互相强引用导致了循环引用。可以通过中间件持有timer & target
解决
GCD计时器
NSTimeInterval interval = 1.0;
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), interval * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(_timer, ^{
NSLog(@"GCD timer test");
});
dispatch_resume(_timer);
复制代码