对RunLoop的一点理解

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方法会直接退出。然后线程就退出了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,548评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,497评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,990评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,618评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,618评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,246评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,819评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,725评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,268评论 1 320
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,356评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,488评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,181评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,862评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,331评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,445评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,897评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,500评论 2 359

推荐阅读更多精彩内容