JHHK

欢迎来到我的个人网站
行者常至 为者常成

RunLoop(2)

目录

runloop 与 定时器

一、timer

timer 事件会唤醒runloop

从调用栈中我们可以看到__CFRunLoopDoTimers的字样

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 6.1
  * frame #0: 0x000000010e0e6619 XYTestModule`__44-[XYRunLoopPageViewController clickHandle1:]_block_invoke(.block_descriptor=0x000000010e1103a8, timer=0x0000600000532580) at XYRunLoopPageViewController.m:155:13
    frame #1: 0x00000001148e34a2 Foundation`__NSFireTimer + 67
    frame #2: 0x000000010e260353 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 20
    frame #3: 0x000000010e25feb0 CoreFoundation`__CFRunLoopDoTimer + 799
    frame #4: 0x000000010e25f637 CoreFoundation`__CFRunLoopDoTimers + 243
    frame #5: 0x000000010e259ec8 CoreFoundation`__CFRunLoopRun + 2183
    frame #6: 0x000000010e259264 CoreFoundation`CFRunLoopRunSpecific + 560
    frame #7: 0x000000011684a24e GraphicsServices`GSEventRunModal + 139
    frame #8: 0x00000001239507bf UIKitCore`-[UIApplication _run] + 994
    frame #9: 0x00000001239555de UIKitCore`UIApplicationMain + 123
    frame #10: 0x000000010447dd0d XYApp`main(argc=1, argv=0x000000030cdc2c00) at main.m:16:19
    frame #11: 0x000000010cae7384 dyld_sim`start_sim + 10
    frame #12: 0x0000000204857310 dyld`start + 2432
(lldb) 

下面代码的本质也是timer

[self performSelector:@selector(test1) withObject:nil afterDelay:0];

二、解决NSTimer在scrollview滚动时不调用的问题

//该timer 只能在NSDefaultRunLoopMode模式下工作
//当程序中有ScrollView滑动时 runloop 会在 UITrackingRunLoopMode 模式下运行,此时timer就失效了
NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
    NSLog(@"timer调用");
}];

//解决办法 将该timer也添加到 UITrackingRunLoopMode
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

//或者 NSRunLoopCommonModes 不是一个具体的模式,代表多个模式的集合
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
//该timer默认没有加入到任何Mode下 所以直接这么写是不会触发timer的事件的
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
    NSLog(@"timer调用");
}];

//在默认模式下 运行timer
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

//在追踪模式下 运行timer
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

//在多模式下 运行timer
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

三、NSTimer不准时,gcd的定时器比较准

NSTimer并不能完全保证准时,当runloop的任务过重时或被阻塞时,可能错误timer的调用时机,导致timer不准确

Grand Central Dispatch(GCD)是苹果提供的一个用于并发编程的框架,其中的dispatch_source_t类型可以用于创建定时器。

GCD 定时器相对于一些其他定时器实现(比如NSTimer)更加准时的原因是:

GCD 定时器是基于系统时钟的,因此它更精确。它使用系统底层的时钟机制,不受主运行循环模式的限制,因此可以提供更准确的定时器。

runloop 与 gcd

dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"current3  %@",[NSThread currentThread]);
});

当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒

唤醒主线程的调用栈,我们可以看到__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__字样

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 8.1
  * frame #0: 0x000000010e0e6750 XYTestModule`__44-[XYRunLoopPageViewController clickHandle2:]_block_invoke(.block_descriptor=0x000000010e1103c8) at XYRunLoopPageViewController.m:166:9
    frame #1: 0x000000010fe757ec libdispatch.dylib`_dispatch_client_callout + 8
    frame #2: 0x000000010fe78a44 libdispatch.dylib`_dispatch_continuation_pop + 836
    frame #3: 0x000000010fe90851 libdispatch.dylib`_dispatch_source_invoke + 2226
    frame #4: 0x000000010fe86554 libdispatch.dylib`_dispatch_main_queue_drain + 1064
    frame #5: 0x000000010fe8611e libdispatch.dylib`_dispatch_main_queue_callback_4CF + 31
    frame #6: 0x000000010e25f6cc CoreFoundation`__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
    frame #7: 0x000000010e259fbe CoreFoundation`__CFRunLoopRun + 2429
    frame #8: 0x000000010e259264 CoreFoundation`CFRunLoopRunSpecific + 560
    frame #9: 0x000000011684a24e GraphicsServices`GSEventRunModal + 139
    frame #10: 0x00000001239507bf UIKitCore`-[UIApplication _run] + 994
    frame #11: 0x00000001239555de UIKitCore`UIApplicationMain + 123
    frame #12: 0x000000010447dd0d XYApp`main(argc=1, argv=0x000000030cdc2c00) at main.m:16:19
    frame #13: 0x000000010cae7384 dyld_sim`start_sim + 10
    frame #14: 0x0000000204857310 dyld`start + 2432
(lldb) 

runloop 与 selector

下面的两个方法依赖运行循环

// 从调用栈看,是source0
[self performSelector:@selector(test1) onThread:currentThread withObject:nil waitUntilDone:NO];

// 也是timer事件,依赖runloop,所以如果当前线程没有 RunLoop,则这个方法会失效
[self performSelector:@selector(test1) withObject:nil afterDelay:0];

runloop 与 UI刷新等

当在操作UI时,比如改变了Frame、更新了 UIView/CALayer 的层次时,
或者手动调用了 UIView/CALayer的 setNeedsLayout/setNeedsDisplay方法后,
这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。

苹果注册了一个 Observer监听 BeforeWaiting(即将进入休眠)Exit (即将退出Loop)事件,
回调去执行一个很长的函数:_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。
这个函数里会遍历所有待处理的 UIView/CAlayer以执行实际的绘制和调整,并更新 UI 界面。

这个函数内部的调用栈大概是这样的:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
    QuartzCore:CA::Transaction::observer_callback:
        CA::Transaction::commit();
            CA::Context::commit_transaction();
                CA::Layer::layout_and_display_if_needed();
                    CA::Layer::layout_if_needed();
                        [CALayer layoutSublayers];
                            [UIView layoutSubviews];
                    CA::Layer::display_if_needed();
                        [CALayer display];
                            [UIView drawRect];

runloop 与 事件响应和手势

一、事件响应

苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。

当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。
SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。
随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。

_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。
通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。

二、手势识别

当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。 随后系统将对应的 UIGestureRecognizer 标记为待处理。

苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,
这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,
并执行GestureRecognizer的回调。

当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

runloop 与 autoreleasePool

App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。
其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

第二个 Observer 监视了两个事件:
BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;
Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。
这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。
这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

runloop 与 卡顿检测

如果主线程正忙,在垂直同步信号来的时候数据没有准备好,那么就会渲染上次的数据,用户就会感觉到卡顿(显示器的刷新率是固定的不会变)。

监控主线程的runloop,根据runloop处理事件的原理可知runloop发出下面两个通知后,就开始处理事件了
发出 kCFRunLoopBeforeSources 开始处理source0/source1
发出 kCFRunLoopAfterWaiting 开始处理唤醒runloop的事件

如果主线程的runloop在发出kCFRunLoopBeforeSources或者kCFRunLoopAfterWaiting通知后,迟迟没有收到下一个通知,证明停留的时间过长(这个时间一般认为是250ms),就会造成页面卡顿。

开启一个子线程监控主线程的runloop状态,如果在上边两个状态停留时间过长,就说明页面卡顿了。
1、开启一个子线程,在子线程内启动一个死循环
2、死循环内用long st = dispatch_semaphore_wait(250ms);来卡住子线程
3、主线程runloop每次状态改变就发送一个信号
4、如果在250ms内收到信号量,说明状态改变,证明停留在状态改变前的那个状态的时间没有超时。
4、如果在250ms内没有收到信号量,dispatch_semaphore_wait就会超时,st == 0,证明runloop当前状态超时。
5、如果当前的状态是kCFRunLoopBeforeSources或者kCFRunLoopAfterWaiting,说明主线程任务过重,这时会导致页面卡顿(这里需要注意超时不一定是任务重,比如beforeWaiting状态也会超时,因为已经休眠了,所以第五步需要进行状态判断)

深入分析下:
当收到kCFRunLoopBeforeSources或者kCFRunLoopAfterWaiting通知时,记录一个时间点A,当收到下一个通知时记录一个时间点B,A和B两个时间差大于250ms就会发生卡顿。那么用这种方式行不行呢?
如果只是监测卡顿有没有发生这样是可以的,但我们监测到卡顿后需要上报一些信息,那么在B这个时间点我们要上报的信息已经获取不到了,卡顿发生时的调用栈已经结束。所以我们需要在B之前就获取到信息。这在主线程是办不到的

示例代码如下

// 开始监听
- (void)startMonitor {
    if (_observer) { return;}
    
    // 创建信号
    _semaphore = dispatch_semaphore_create(0);
    
    
    // 注册RunLoop状态观察
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    //创建Run loop observer对象
    //第一个参数用于分配observer对象的内存
    //第二个参数用以设置observer所要关注的事件,详见回调函数myRunLoopObserver中注释
    //第三个参数用于标识该observer是在第一次进入run loop时执行还是每次进入run loop处理时均执行
    //第四个参数用于设置该observer的优先级
    //第五个参数用于设置该observer的回调函数
    //第六个参数用于设置该observer的运行环境
    _observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                       kCFRunLoopAllActivities,
                                       YES,
                                       0,
                                       &runLoopObserverCallBack,
                                       &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
    
    
    
    // 在子线程监控时长
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (YES) {
            // 有信号的话 就查询当前runloop的状态
            // 假定连续5次超时50ms认为卡顿(当然也包含了单次超时250ms)
            // 因为下面 runloop 状态改变回调方法runLoopObserverCallBack中会将信号量递增 1,所以每次 runloop 状态改变后,下面的语句都会执行一次
            // dispatch_semaphore_wait:Returns zero on success, or non-zero if the timeout occurred.
            long st = dispatch_semaphore_wait(_semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
            
            
            if (st != 0) {  // 信号量超时了 - 即 runloop 的状态长时间没有发生变更,长期处于某一个状态下
                if (!_observer) {
                    _timeoutCount = 0;
                    _semaphore = 0;
                    _activity = 0;
                    return;
                }
                
                // runloop做事主要在两个时间段:
                // 一个是 kCFRunLoopBeforeSources 到 kCFRunLoopBeforeWaiting 这段时间
                // 一个是 kCFRunLoopAfterWaiting 到 kCFRunLoopBeforeTimers 这段时间
                // 所以在kCFRunLoopAfterWaiting || kCFRunLoopAfterWaiting 停留时间过长会产生卡顿
                if (_activity == kCFRunLoopBeforeSources || _activity == kCFRunLoopAfterWaiting) {
                    // 发生卡顿,记录卡顿次数
                    if (++_timeoutCount < 5) {
                        // 不足 5 次,直接 continue 当次循环,不将timeoutCount置为0
                        continue;
                    }
                    
                    
                    // 超过5次也及时250毫秒,收集Crash信息也可用于实时获取各线程的调用堆栈
                    /*
                    PLCrashReporterConfig *config = [[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD
                                                                                       symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll];
                    PLCrashReporter *crashReporter = [[PLCrashReporter alloc] initWithConfiguration:config];
                    NSData *data = [crashReporter generateLiveReport];
                    PLCrashReport *reporter = [[PLCrashReport alloc] initWithData:data error:NULL];
                    NSString *report = [PLCrashReportTextFormatter stringValueForCrashReport:reporter withTextFormat:PLCrashReportTextFormatiOS];
                    */
                }
            }
            
            // 只要状态变化就会立即清0
            _timeoutCount = 0;
        }
    });
}


//每当runloop状态变化的触发这个回调方法
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    
    // 记录状态值
    _activity = activity;
    
    // 发送信号
    dispatch_semaphore_signal(_semaphore);
    
    //
    if (activity == kCFRunLoopEntry) {  // 即将进入RunLoop
        NSLog(@"runLoopObserverCallBack - %@",@"kCFRunLoopEntry");
    } else if (activity == kCFRunLoopBeforeTimers) {    // 即将处理Timer
        NSLog(@"runLoopObserverCallBack - %@",@"kCFRunLoopBeforeTimers");
    } else if (activity == kCFRunLoopBeforeSources) {   // 即将处理Source
        NSLog(@"runLoopObserverCallBack - %@",@"kCFRunLoopBeforeSources");
    } else if (activity == kCFRunLoopBeforeWaiting) {   //即将进入休眠
        NSLog(@"runLoopObserverCallBack - %@",@"kCFRunLoopBeforeWaiting");
    } else if (activity == kCFRunLoopAfterWaiting) {    // 刚从休眠中唤醒
        NSLog(@"runLoopObserverCallBack - %@",@"kCFRunLoopAfterWaiting");
    } else if (activity == kCFRunLoopExit) {    // 即将退出RunLoop
        NSLog(@"runLoopObserverCallBack - %@",@"kCFRunLoopExit");
    } else if (activity == kCFRunLoopAllActivities) {
        NSLog(@"runLoopObserverCallBack - %@",@"kCFRunLoopAllActivities");
    }
}

CADisplayLink 跟 NSTimer的原理是一致的,同样依赖于runloop,只是它的调用间隔是每秒60次调用(于屏幕的FPS相同)

它并不能反应真实的屏幕的FPS,它反应的是线程的繁忙程度,如果线程过于繁忙,它的调用次数就会低于每秒60次

FPS是每秒切换帧的次数(一帧可以简单理解为一张图片),如果1秒切换了50帧,那么FPS就是50

xy:垂直同步信号每秒来60次,并不代表FPS就是60,如果有一次帧数据没有准备好,垂直同步信号来的时候显示了上一帧的数据,那么FPS就是59


行者常至,为者常成!





R
Valine - A simple comment system based on Leancloud.