iOS 金三银四涨薪系列(5) runloop & 计时器

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,等效于NSDefaultRunLoopModeNSEventTrackingRunLoopMode的结合 )
  • GSEventReceiveRunLoopMode(接受系统事件的内部 Mode,通常用不到)

runLoop的基本原理

  • 系统中的主线程会默认开启runloop检测事件,没有事件需要处理的时候runloop会处于休眠状态。
  • 一旦有事件触发,例如用户点击屏幕,就会唤醒runloop使进入监听状态,然后处理事件。
  • 事件处理完成后又会重新进入休眠,等待下一次事件唤醒

runloop和线程的关系

  • runloop和线程一一对应。
  • 主线程的创建的时候默认开启runloop,为了保证程序一直在跑。
  • 支线程的runloop是懒加载的,需要手动开启。

runloop事件处理流程

  • 事件会触发runloop的入口函数CFRunLoopRunSpecific,函数内部首先会通知observer把状态切换成kCFRunLoopEntry,然后通过__CFRunLoopRun启动runloop处理事件
  • __CFRunLoopRun的核心是是一个do - while循环,循环内容如下

image-20220123003341954.png

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 处理耗时主要下面两种情况
    • kCFRunLoopBeforeSourceskCFRunLoopBeforeWaiting 之间
    • 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模式,简单好用
    • 缺点是容易不小心造成循环引用。需要依赖runlooprunloop如果被阻塞就要延迟到下一次runloop周期才执行,所以时间精度上也略为不足
  • CADisplayLink
    • 优点是精度高,每次刷新结束后都调用,适合不停重绘的计时,例如视频
    • 缺点容易不小心造成循环引用。selector循环间隔大于重绘每帧的间隔时间,会导致跳过若干次调用机会。不可以设置单次执行。
  • dispatch_source_t
    • 基于GCD,精度高,不依赖runloop,简单好使,最喜欢的计时器
    • 需要注意的点是使用的时候必须持有计时器,不然就会提前释放。

NSTimer在子线程执行会怎么样?

  • NSTimer在子线程调用需要手动开启子线程的runloop
  • [[NSRunLoop currentRunLoop] run];

NSTimer为什么不准?

如果runloop正处在阻塞状态的时候NSTimer到达触发时间,NSTimer的触发会被推迟到下一个runloop周期

NSTimer的循环引用?

timertarget互相强引用导致了循环引用。可以通过中间件持有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);
复制代码

Guess you like

Origin juejin.im/post/7076028177570594847