美文网首页
对RunLoop的一点理解

对RunLoop的一点理解

作者: FarmGuo | 来源:发表于2021-07-31 19:25 被阅读0次

    1.RunLoop与dispatch的关系

    1.在__CFRunLoopRun函数中,用dispatch_source_create创建一个定时器。处理此次runLoop mode的运行时间,唤醒runloop。
    2.使用dispatch执行的block(main_queue中),例如
    dispatch_async 提交的任务
    dispatch_after 提交的延时任务
    
    dispatch_source_create
    dispatch_source_set_timer
    dispatch_source_set_event_handler
    设置的定时器
    

    它们的执行流程是
    BeforeSources后,开始处理提交到Runloop的Block(通过-[NSRunLoop performBlock:]方式),然后开始处理source0,如果在source0中处理了,再处理一下Block。调用__CFRunLoopServiceMachPort,在其中用循环的方式通过mach_msg接受消息,mach_msg_header_t类型的消息体的msgh_local_port来自_dispatch_get_main_queue_port_4CF函数,当有dispatch的任务时,Runloop就会接收到消息。在调用CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE,其内部又调用了_dispatch_main_queue_callback_4CF,最终回到dispatch库中执行任务。

    堆栈调用

      * frame #0: 0x0000000103912c83 testblock`__29-[ViewController viewDidLoad]_block_invoke(.block_descriptor=0x000060000193ee20) at ViewController.m:82:10
        frame #1: 0x00000001057ef9c8 libdispatch.dylib`_dispatch_client_callout + 8
        frame #2: 0x00000001057f2316 libdispatch.dylib`_dispatch_continuation_pop + 557
        frame #3: 0x0000000105805e8b libdispatch.dylib`_dispatch_source_invoke + 2205
        frame #4: 0x00000001057fdca4 libdispatch.dylib`_dispatch_main_queue_callback_4CF + 687
        frame #5: 0x0000000104201dab CoreFoundation`__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
        frame #6: 0x00000001041fc62e CoreFoundation`__CFRunLoopRun + 2685
        frame #7: 0x00000001041fb6c6 CoreFoundation`CFRunLoopRunSpecific + 567
        frame #8: 0x000000011010cdb3 GraphicsServices`GSEventRunModal + 139
        frame #9: 0x00000001079ad187 UIKitCore`-[UIApplication _run] + 912
        frame #10: 0x00000001079b2038 UIKitCore`UIApplicationMain + 101
        frame #11: 0x000000010391431a testblock`main(argc=1, argv=0x00007ffeec2eed18) at main.m:18:12
        frame #12: 0x000000010587e409 libdyld.dylib`start + 1
        frame #13: 0x000000010587e409 libdyld.dylib`start + 1
    

    2.Runloop与定时器的关系

    dispatch类型的定时器,上面已经谈过了,我们谈下其他类型的定时器,

    1.NSTimer类型的

    这种类型会转换为CFRunLoopTimerRef,然后加入到RunLoop的mode中去,最终在__CFRunLoopDoTimers中进行触发。触发后判断timer是否是repeat的进行移除。

    2.perform类型

    @interface NSObject (NSDelayedPerforming)
    - (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
    - (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
    @end
    

    此类型和NSTimer类型一样的处理流程。后一个方法是将定时器放在default mode,这就意味着如果后面有滑动的操作,selector的延迟会不准确。因为考虑到准确性可以使用后者,并将mode指定为common mode。
    NSObject类里面还声明了以下方法

    - (id)performSelector:(SEL)aSelector;
    - (id)performSelector:(SEL)aSelector withObject:(id)object;
    - (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
    

    这些方法最终是转化成了objc_msgSend,和RunLoop无关。

    3.CADisplayLink类型

    CADisplayLink必须调用-[CADisplayLink addToRunLoop:forMode:]才能生效,最终会通过CFMachPortCreateRunLoopSource来创建一个source1(基于port),添加到指定的RunLoop Mode中去。触发时在__CFRunLoopDoSource1中,通过display_timer_callback被调用。

    3.Runloop与performSelectorOnThread

    NSThreadPerformAdditions分类中的方法

    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
    
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
    - (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg ;
    

    执行后会在目标thread中的RunLoop中添加一个source0,其callback最终会调到selector。当执行performSelector系列时,会在找到这个source0,然后调用最终在__CFRunLoopDoSources0中调用CFRunLoopSourceSignal标记该souece0为ready状态,然后通过CFRunLoopWakeUp唤起目标thread的runloop来处理这个source0,最终selector得以执行。由此可见,若目标线程没有RunLoop则selector不会执行。

    4.Runloop的运行

    NSRunLoop暴露了3个关于run方法

    - (void)run; 
    - (void)runUntilDate:(NSDate *)limitDate;
    - (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
    

    NSRunLoop没有开源,但Swift版本的RunLoop是开源的

        public func run() {
            while run(mode: .default, before: Date.distantFuture) { }
        }
    
        public func run(until limitDate: Date) {
            while run(mode: .default, before: limitDate) && limitDate.timeIntervalSinceReferenceDate > CFAbsoluteTimeGetCurrent() { }
        }
    
        public func run(mode: RunLoop.Mode, before limitDate: Date) -> Bool {
            if _cfRunLoop !== CFRunLoopGetCurrent() {
                return false
            }
            let modeArg = mode._cfStringUniquingKnown
            if _CFRunLoopFinished(_cfRunLoop, modeArg) {
                return false
            }
            
            let limitTime = limitDate.timeIntervalSinceReferenceDate
            let ti = limitTime - CFAbsoluteTimeGetCurrent()
            CFRunLoopRunInMode(modeArg, ti, true)
            CFRunLoopStop(_cfRunLoop)
            return true
        }
    

    runMode:beforeDate:则直接调用了CFRunLoopRunInMode(),即只运行一次。
    其中run是一个while循环,内部不停调用runMode:beforeDate:,当[runMode: beforeDate:]返回NO时,则退出循环,-[NSRunLoop run]后面的代码就会被执行。而[runMode: beforeDate:]返回NO,通常是因为_CFRunLoopFinished返回了YES。当RunLoop里面没有指定的mode,或者对应的mode里面没有timer/source(source0和source1)/block时,就会返回YES。
    runUntilDate:和run类似,但调用前会先对limitDate进行判断。
    这也是为什么多数情况下 [[NSRunLoop currentRunLoop] run] 执行后,CFRunLoopStop()无法停止RunLoop的原因。而通过CFRunLoopRun()开启的,就可以被CFRunLoopStop()停止。

    5.View的更新

    UIView的更新包含了内容和布局的更新
    主线程的runloop注册了3个observer,一个优先级为1999000,Activity为BeforeWaiting | Exit,回调为_beforeCACommitHandler。
    另一个优先级为2001000,Activity也为BeforeWaiting | Exit,回调为_afterCACommitHandler。
    还有一个observer,一个优先级为2000000,Activity为BeforeWaiting | Exit,回调为CA::Transaction::observer_callback(__CFRunLoopObserver, unsigned long, void),这个是用来更新内容和布局的。

     * frame #0: 0x0000000105e9174a testrl`-[FGView drawRect:](self=0x00007ffbbf107b70, _cmd="drawRect:", rect=(origin = (x = 0, y = 0), size = (width = 10, height = 100))) at FGView.m:34:5
        frame #1: 0x00007fff24c17ccd UIKitCore`-[UIView(CALayerDelegate) drawLayer:inContext:] + 625
        frame #2: 0x00007fff27a0491d QuartzCore`-[CALayer drawInContext:] + 288
        frame #3: 0x00007fff278b3b3d QuartzCore`CABackingStoreUpdate_ + 219
        frame #4: 0x00007fff27a0e411 QuartzCore`invocation function for block in CA::Layer::display_() + 53
        frame #5: 0x00007fff27a0415a QuartzCore`-[CALayer _display] + 2168
        frame #6: 0x00007fff27a17def QuartzCore`CA::Layer::layout_and_display_if_needed(CA::Transaction*) + 477
        frame #7: 0x00007fff27951f26 QuartzCore`CA::Context::commit_transaction(CA::Transaction*, double, double*) + 656
        frame #8: 0x00007fff279893b9 QuartzCore`CA::Transaction::commit() + 713
        frame #9: 0x00007fff2798a51f QuartzCore`CA::Transaction::observer_callback(__CFRunLoopObserver*, unsigned long, void*) + 79
        frame #10: 0x00007fff2038fd31 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 23
        frame #11: 0x00007fff2038a542 CoreFoundation`__CFRunLoopDoObservers + 541
        frame #12: 0x00007fff2038aaf5 CoreFoundation`__CFRunLoopRun + 1129
    

    以上是一次内容更新的调用堆栈,自定义FGView只重写了方法-[UIView drawRect:]方法。可以看出此次内容的更新是由observer_callback引起的。

      * frame #0: 0x000000010739871c testrl`-[FGView layoutSubviews](self=0x00007f8f67e0d4b0, _cmd="layoutSubviews") at FGView.m:16:1
        frame #1: 0x00007fff24c18c90 UIKitCore`-[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 2946
        frame #2: 0x00007fff27a055b8 QuartzCore`-[CALayer layoutSublayers] + 258
        frame #3: 0x00007fff27a0be3f QuartzCore`CA::Layer::layout_if_needed(CA::Transaction*) + 611
        frame #4: 0x00007fff27a17c53 QuartzCore`CA::Layer::layout_and_display_if_needed(CA::Transaction*) + 65
        frame #5: 0x00007fff27951f26 QuartzCore`CA::Context::commit_transaction(CA::Transaction*, double, double*) + 656
        frame #6: 0x00007fff279893b9 QuartzCore`CA::Transaction::commit() + 713
        frame #7: 0x00007fff2798a51f QuartzCore`CA::Transaction::observer_callback(__CFRunLoopObserver*, unsigned long, void*) + 79
        frame #8: 0x00007fff2038fd31 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 23
        frame #9: 0x00007fff2038a542 CoreFoundation`__CFRunLoopDoObservers + 541
        frame #10: 0x00007fff2038aaf5 CoreFoundation`__CFRunLoopRun + 1129
    

    以上是一次布局更新的调用堆栈,自定义FGView只重写了方法-[UIView layoutSubviews:]方法。可以看出此次布局的更新是由observer_callback引起的。

    UIView的layout和内容的更新流程见另一片文章UIView和CALayer

    6.AutoReleasePool

    主线程的RunLoop注册了2个observer,一个优先级为-2147483647(优先级最高),Activity为Entry。一个优先级为2147483647(优先级最低),Activity为BeforeWaiting | Exit。它们的回调为_runLoopObserverCallout。里面进行了以下操作:

    Activity是kCFRunLoopEntry时,执行__pushAutoreleasePool,kCFRunLoopBeforeWaiting时 ,先__pushAutoreleasePool,然后再__pushAutoreleasePool。是kCFRunLoopExit,执行__pushAutoreleasePool。

    7.手势

    主线程的RunLoop注册了一个observer,一个优先级为0,Activity为BeforeWaiting,回调为_UIGestureRecognizerUpdateObserver。

    8.事件

    SpringBoard捕获事件后,会通过port(source1类型)发消息给com.apple.uikit.eventfetch线程的runloop,这个port对应的回调是__IOHIDEventSystemClientQueueCallback,最终会到UIKitCore框架的-[UIEventDispatcher eventFetcherDidReceiveEvents:]中,根据UIEventDispatcher找到主线程对应的source(source0),设置ready,并唤醒主线程的runloop,这个source对应的回调是__eventFetcherSourceCallback。将事件传递给WIndow进行派发。
    source0不能主动唤起RunLoop,只能使用先给source0发信号,即设置标记表示ready,然后唤起runloop以处理该source0。

    CFRunLoopSourceSignal(source);
    CFRunLoopWakeUp(runloop);
    

    以上基于iOS 14.5系统版本,不同系统版本略有差异。

    9 有趣的事情

    在按钮的点击事件中,会执行下面代码

    - (void)threadDown {
        NSLog(@"threadDown");
        if (!_thread) {
            _thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadEntry) object:nil];
            _thread.name = @"myThread";
            [_thread start];
            // 如果休眠一会,则后面的perform会晚一会执行
            sleep(2);
            NSLog(@"after sleep");
        }
    
        [self performSelector:@selector(threadAgain) onThread:_thread withObject:nil waitUntilDone:NO];
    }
    
    - (void)threadEntry {
        NSLog(@"threadEntry");
        [[NSRunLoop currentRunLoop] run]; //这种的无法stop,只能通过[NSThread exit]了
    }
    

    现象是如果休眠2s,新线程执行完threadEntry就退出了,即则不会新线程上执行threadAgain方法。如果不休眠则一切正常。这是为什么呢?
    上面我们提到过,performSelectorOnThread系列方法会创建一个source0添到目标线程的RunLoop中去。所以我们在threadEntry直接run就行了。而新线程的初始化方法threadEntry执行的并不是立即执行的,当threadEntry方法执行时,performSelectorOnThread已经执行过了,此时RunLoop里面已经有了source0,所以RunLoop可以运行起来,这样线程就不会退出了。当我们休眠2s后再执行performSelectorOnThread时,则threadEntry已经执行过了,而执行时RunLoop里面没有source和timer,所以run方法会直接退出。然后线程就退出了。

    相关文章

      网友评论

          本文标题:对RunLoop的一点理解

          本文链接:https://www.haomeiwen.com/subject/cwekvltx.html