美文网首页
iOS RunLoop

iOS RunLoop

作者: coder_my | 来源:发表于2019-10-11 22:34 被阅读0次

    官方文档:
    Apple CoreFoundation源码
    RunLoop 文档(旧)
    NSRunLoop
    CFRunLoopRef

    RunLoop作用:

    Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.(运行循环是与线程相关的基本基础设施的一部分。一个 run loop是一个事件处理循环,您可以使用它来调度工作并协调传入事件的接收。一个run loop的目的是在有工作要做时让线程保持忙碌,在没有工作时让线程休眠。)

    Run loop management is not entirely automatic. You must still design your thread’s code to start the run loop at appropriate times and respond to incoming events. Both Cocoa and Core Foundation provide run loop objects to help you configure and manage your thread’s run loop. Your application does not need to create these objects explicitly; each thread, including the application’s main thread, has an associated run loop object. Only secondary threads need to run their run loop explicitly, however. The app frameworks automatically set up and run the run loop on the main thread as part of the application startup process.(运行循环管理不是完全自动的。您仍然必须设计线程的代码,以便在适当的时间启动运行循环并响应传入的事件。Cocoa和Core Foundation都提供了run loop对象来帮助您配置和管理线程的run loop。您的应用程序不需要显式地创建这些对象;每个线程,包括应用程序的主线程,都有一个关联的run loop对象。但是,只有辅助线程需要显式地运行它们的运行循环。作为应用程序启动过程的一部分,应用程序框架自动在主线程上设置并运行运行循环。)

    RunLoop实际上也是一个对象,这个对象管理了线程内部需要处理的事件和消息,存在RunLoop的线程一直处于“消息接收->等待->处理”的循环中,直到这个循环结束(RunLoop被释放)。(Run Loop是一个do while运行循环)

    do{
        var message = getNewmessages();//接收来自外部的消息
        exec(message);//处理消息任务
    }while(0==isQuit)
    

    进程、线程、RunLoop之间的关系

    线程与RunLoop是一一对应的关系(对应关系保存在一个全局的Dictionary里),线程创建之后是没有RunLoop的(主线程除外),RunLoop的创建是发生在第一次获取时。

    苹果不允许直接创建RunLoop,但是可以通过[NSRunLoop currentRunLoop]或者CFRunLoopGetCurrent()来获取(如果没有就会自动创建一个)。

    一般开发中使用的RunLoop就是NSRunLoop和CFRunLoopRef,CFRunLoopRef属于Core Foundation框架,提供的是C函数的API,是线程安全的,NSRunLoop是基于CFRunLoopRef的封装,提供了面向对象的API,这些API不是线程安全的。

    由于NSRunLoop是基于CFRunLoop封装的,下文关于RunLoop的原理讨论都会基于CFRunLoop来进行。NSRunLoop和CFRunLoop所有类都是一一对应的关系。

    • 每条线程都有唯一的一个与之对应的RunLoop对象
    • RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
    • 主线程的RunLoop已经自动创建好了,子线程的RunLoop需要主动创建
    • RunLoop在第一次获取时创建,在线程结束时销毁

    通过源码查看上述对应

    // 拿到当前Runloop 调用_CFRunLoopGet0
    CFRunLoopRef CFRunLoopGetCurrent(void) {
        CHECK_FOR_FORK();
        CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
        if (rl) return rl;
        return _CFRunLoopGet0(pthread_self());
    }
    
    // 查看_CFRunLoopGet0方法内部
    CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
        if (pthread_equal(t, kNilPthreadT)) {
        t = pthread_main_thread_np();
        }
        __CFLock(&loopsLock);
        if (!__CFRunLoops) {
            __CFUnlock(&loopsLock);
        CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        // 根据传入的主线程获取主线程对应的RunLoop
        CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
        // 保存主线程 将主线程-key和RunLoop-Value保存到字典中
        CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
        if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
            CFRelease(dict);
        }
        CFRelease(mainLoop);
            __CFLock(&loopsLock);
        }
        
        // 从字典里面拿,将线程作为key从字典里获取一个loop
        CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        __CFUnlock(&loopsLock);
        
        // 如果loop为空,则创建一个新的loop,所以runloop会在第一次获取的时候创建
        if (!loop) {  
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
            __CFLock(&loopsLock);
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        
        // 创建好之后,以线程为key runloop为value,一对一存储在字典中,下次获取的时候,则直接返回字典内的runloop
        if (!loop) { 
            CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
            loop = newLoop;
        }
            // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
            __CFUnlock(&loopsLock);
        CFRelease(newLoop);
        }
        if (pthread_equal(t, pthread_self())) {
            _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
            if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
                _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
            }
        }
        return loop;
    }
    

    从上面的代码可以看出,线程和 RunLoop 之间是一一对应的,其关系是保存在一个 Dictionary 里。所以我们创建子线程RunLoop时,只需在子线程中获取当前线程的RunLoop对象即可[NSRunLoop currentRunLoop];如果不获取,那子线程就不会创建与之相关联的RunLoop,并且只能在一个线程的内部获取其RunLoop
    [NSRunLoop currentRunLoop];方法调用时,会先看一下字典里有没有存子线程相对用的RunLoop,如果有则直接返回RunLoop,如果没有则会创建一个,并将与之对应的子线程存入字典中。当线程结束时,RunLoop会被销毁。

    RunLoop主要组成

    RunLoop结构体,通过源码我们找到__CFRunLoop结构体

    struct __CFRunLoop {
        CFRuntimeBase _base;
        pthread_mutex_t _lock;          /* locked for accessing mode list */
        __CFPort _wakeUpPort;           // used for CFRunLoopWakeUp 
        Boolean _unused;
        volatile _per_run_data *_perRunData;              // reset for runs of the run loop
        pthread_t _pthread;
        uint32_t _winthread;
        CFMutableSetRef _commonModes;
        CFMutableSetRef _commonModeItems;
        CFRunLoopModeRef _currentMode;
        CFMutableSetRef _modes;
        struct _block_item *_blocks_head;
        struct _block_item *_blocks_tail;
        CFAbsoluteTime _runTime;
        CFAbsoluteTime _sleepTime;
        CFTypeRef _counterpart;
    };
    

    除一些记录性属性外,主要来看一下以下两个成员变量

    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    

    CFRunLoopModeRef 其实是指向__CFRunLoopMode结构体的指针,__CFRunLoopMode结构体源码如下

    typedef struct __CFRunLoopMode *CFRunLoopModeRef;
    
    struct __CFRunLoopMode {
        CFRuntimeBase _base;
        pthread_mutex_t _lock;  /* must have the run loop locked before locking this */
        CFStringRef _name;
        Boolean _stopped;
        char _padding[3];
        CFMutableSetRef _sources0;
        CFMutableSetRef _sources1;
        CFMutableArrayRef _observers;
        CFMutableArrayRef _timers;
        CFMutableDictionaryRef _portToV1SourceMap;
        __CFPortSet _portSet;
        CFIndex _observerMask;
    #if USE_DISPATCH_SOURCE_FOR_TIMERS
        dispatch_source_t _timerSource;
        dispatch_queue_t _queue;
        Boolean _timerFired; // set to true by the source when a timer has fired
        Boolean _dispatchTimerArmed;
    #endif
    #if USE_MK_TIMER_TOO
        mach_port_t _timerPort;
        Boolean _mkTimerArmed;
    #endif
    #if DEPLOYMENT_TARGET_WINDOWS
        DWORD _msgQMask;
        void (*_msgPump)(void);
    #endif
        uint64_t _timerSoftDeadline; /* TSR */
        uint64_t _timerHardDeadline; /* TSR */
    };
    

    一个RunLoop包含了多个Mode,每个Mode又包含了若干个Source/Timer/Observer。每次调用 RunLoop的主函数时,只能指定其中一个Mode,这个Mode被称作CurrentMode。如果需要切换 Mode,只能退出Loop,再重新指定一个Mode进入。这样做主要是为了分隔开不同Mode中的Source/Timer/Observer,让其互不影响。下面是5种Mode

    • kCFDefaultRunLoopMode App的默认Mode,通常主线程是在这个Mode下运行
    • UITrackingRunLoopMode 界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响
    • UIInitializationRunLoopMode 在刚启动App时第进入的第一个Mode,启动完成后就不再使用
    • GSEventReceiveRunLoopMode 接受系统事件的内部Mode,通常用不到
    • kCFRunLoopCommonModes 这是一个占位用的Mode,不是一种真正的Mode

    你可以在这里看到更多的苹果内部的 Mode,但那些 Mode 在开发中就很难遇到了。

    其中kCFDefaultRunLoopMode、UITrackingRunLoopMode是苹果公开的,其余的mode都是无法添加的。既然没有CommonModes这个模式,那我们平时用的这行代码怎么解释呢?

    什么是CommonModes?
    一个 Mode 可以将自己标记为"Common"属性(通过将其 ModeName 添加到 RunLoop 的 "commonModes" 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 "Common" 标记的所有Mode里;主线程的 RunLoop 里有 kCFRunLoopDefaultMode 和 UITrackingRunLoopMode,这两个Mode都已经被标记为"Common"属性。当你创建一个Timer并加到DefaultMode时,Timer会得到重复回调,但此时滑动一个 scrollView 时,RunLoop 会将 mode 切换为TrackingRunLoopMode,这时Timer就不会被回调,并且也不会影响到滑动操作。
    如果想让scrollView滑动时Timer可以正常调用,一种办法就是手动将这个 Timer 分别加入这两个 Mode。另一种方法就是将 Timer 加入到CommonMode 中。
    怎么将事件加入到CommonMode?
    我们调用上面的代码将 Timer 加入到CommonMode 时,但实际并没有 CommonMode,其实系统将这个 Timer 加入到顶层的 RunLoop 的 commonModeItems 中。commonModeItems 会被 RunLoop 自动更新到所有具有"Common"属性的 Mode 里去。
    这一步其实是系统帮我们将Timer加到了kCFRunLoopDefaultMode和UITrackingRunLoopMode中。

    主要查看以下成员变量

    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    

    通过上面分析我们知道,CFRunLoopModeRef代表RunLoop的运行模式,一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer,而RunLoop启动时只能选择其中一个Mode作为currentMode。

    Source1/Source0/Timers/Observer分别代表什么

    1、Source0:处理App内部事件,App自己负责管理(触发),如UIEvent(触摸事件),PerformSelectors,CFSocket;非基于Port的,只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。

    • 点击屏幕,在点击回调代码设置断点,输入:bt 打印调用栈
    (lldb) bt
    * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
      * frame #0: 0x00000001006efbb6 runloop`-[ViewController touchesBegan:withEvent:](self=0x00007f8bb5609ce0, _cmd="touchesBegan:withEvent:", touches=1 element, event=0x0000600000678090) at ViewController.m:95:6
        frame #1: 0x00000001047cba09 UIKitCore`forwardTouchMethod + 353
        frame #2: 0x00000001047cb897 UIKitCore`-[UIResponder touchesBegan:withEvent:] + 49
        frame #3: 0x00000001047dac48 UIKitCore`-[UIWindow _sendTouchesForEvent:] + 1869
        frame #4: 0x00000001047dc5d2 UIKitCore`-[UIWindow sendEvent:] + 4079
        frame #5: 0x00000001047bad16 UIKitCore`-[UIApplication sendEvent:] + 356
        frame #6: 0x000000010488b293 UIKitCore`__dispatchPreprocessedEventFromEventQueue + 3232
        frame #7: 0x000000010488dbb9 UIKitCore`__handleEventQueueInternal + 5911
        frame #8: 0x000000010198cbe1 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
        frame #9: 0x000000010198c463 CoreFoundation`__CFRunLoopDoSources0 + 243
        frame #10: 0x0000000101986b1f CoreFoundation`__CFRunLoopRun + 1231
        frame #11: 0x0000000101986302 CoreFoundation`CFRunLoopRunSpecific + 626
        frame #12: 0x0000000109f222fe GraphicsServices`GSEventRunModal + 65
        frame #13: 0x00000001047a0ba2 UIKitCore`UIApplicationMain + 140
        frame #14: 0x00000001006f03e0 runloop`main(argc=1, argv=0x00007ffeef50ff98) at main.m:14:16
        frame #15: 0x0000000103304541 libdyld.dylib`start + 1
        frame #16: 0x0000000103304541 libdyld.dylib`start + 1
    (lldb) 
    

    frame #9: 0x000000010198c463 CoreFoundation`__CFRunLoopDoSources0 + 243
    验证点击事件为source0

    [self performSelectorOnMainThread:@selector(testPerformSelector1) withObject:nil waitUntilDone:YES];
    

    testPerformSelector1方法内设置断点,输入:bt 打印调用栈

    (lldb) bt
    * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
      * frame #0: 0x00000001061d6bcc runloop`-[ViewController testPerformSelector1](self=0x00007fb8d8405670, _cmd="testPerformSelector1") at ViewController.m:111:1
        frame #1: 0x0000000106591e4a Foundation`-[NSObject(NSThreadPerformAdditions) performSelector:onThread:withObject:waitUntilDone:modes:] + 1140
        frame #2: 0x0000000106591f76 Foundation`-[NSObject(NSThreadPerformAdditions) performSelectorOnMainThread:withObject:waitUntilDone:] + 131
        frame #3: 0x00000001061d6bad runloop`-[ViewController test1](self=0x00007fb8d8405670, _cmd="test1") at ViewController.m:106:5
        frame #4: 0x00000001061d6b4a runloop`-[ViewController touchesBegan:withEvent:](self=0x00007fb8d8405670, _cmd="touchesBegan:withEvent:", touches=1 element, event=0x0000600001b64b40) at ViewController.m:94:5
        frame #5: 0x000000010a2b3a09 UIKitCore`forwardTouchMethod + 353
        frame #6: 0x000000010a2b3897 UIKitCore`-[UIResponder touchesBegan:withEvent:] + 49
        frame #7: 0x000000010a2c2c48 UIKitCore`-[UIWindow _sendTouchesForEvent:] + 1869
        frame #8: 0x000000010a2c45d2 UIKitCore`-[UIWindow sendEvent:] + 4079
        frame #9: 0x000000010a2a2d16 UIKitCore`-[UIApplication sendEvent:] + 356
        frame #10: 0x000000010a373293 UIKitCore`__dispatchPreprocessedEventFromEventQueue + 3232
        frame #11: 0x000000010a375bb9 UIKitCore`__handleEventQueueInternal + 5911
        frame #12: 0x0000000107474be1 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
        frame #13: 0x0000000107474463 CoreFoundation`__CFRunLoopDoSources0 + 243
        frame #14: 0x000000010746eb1f CoreFoundation`__CFRunLoopRun + 1231
        frame #15: 0x000000010746e302 CoreFoundation`CFRunLoopRunSpecific + 626
        frame #16: 0x000000010fa0a2fe GraphicsServices`GSEventRunModal + 65
        frame #17: 0x000000010a288ba2 UIKitCore`UIApplicationMain + 140
        frame #18: 0x00000001061d73a0 runloop`main(argc=1, argv=0x00007ffee9a28f98) at main.m:14:16
        frame #19: 0x0000000108dec541 libdyld.dylib`start + 1
        frame #20: 0x0000000108dec541 libdyld.dylib`start + 1
    (lldb) 
    

    frame #13: 0x0000000107474463 CoreFoundation`__CFRunLoopDoSources0 + 243
    验证performSelectorOnMainThread为source0

    2、Source1:基于Port的线程间通信,被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程。由Runloop和内核管理,mach port驱动,如CFMachPort(轻量级的进程间通信的方式,NSPort就是对它的封装,还有Runloop的睡眠和唤醒就是通过它来做的),CFMessagePort;
    3、CFRunloopObserver:这个是用来向外界报告Runloop当前的状态的更改。

    kCFRunLoopEntry = (1UL << 0),// 即将进入Loop  
    kCFRunLoopBeforeTimers = (1UL << 1),// 即将处理 Timer  
    kCFRunLoopBeforeSources = (1UL << 2),// 即将处理 Source  
    kCFRunLoopBeforeWaiting = (1UL << 5),// 即将进入休眠  
    kCFRunLoopAfterWaiting = (1UL << 6),// 刚从休眠中唤醒  
    kCFRunLoopExit = (1UL << 7),// 即将退出Loop  
    kCFRunLoopAllActivities = 0x0FFFFFFFU//所有状态
    

    4、Timers : 定时器,NSTimer

    CFRunLoopTimerRef是基于时间的触发器,基本上说的就是NSTimer,它受RunLoop的Mode影响(GCD的定时器不受RunLoop的Mode影响),当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。如果线程阻塞或者不在这个Mode下,触发点将不会执行,一直等到下一个周期时间点触发。

    [self performSelector:@selector(testPerformSelector0) withObject:nil afterDelay:1];
        [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
            NSLog(@"NSTimer ---- timer调用了");
        }];
    

    设置断点,输入:bt 打印调用栈

    (lldb) bt
    * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
      * frame #0: 0x0000000107990bfc runloop`-[ViewController testPerformSelector1](self=0x00007fe75dd15e60, _cmd="testPerformSelector1") at ViewController.m:112:1
        frame #1: 0x0000000107e88151 Foundation`__NSFireDelayedPerform + 414
        frame #2: 0x0000000108d783e4 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 20
        frame #3: 0x0000000108d77ff2 CoreFoundation`__CFRunLoopDoTimer + 1026
        frame #4: 0x0000000108d7785a CoreFoundation`__CFRunLoopDoTimers + 266
        frame #5: 0x0000000108d71efc CoreFoundation`__CFRunLoopRun + 2220
        frame #6: 0x0000000108d71302 CoreFoundation`CFRunLoopRunSpecific + 626
        frame #7: 0x00000001112c52fe GraphicsServices`GSEventRunModal + 65
        frame #8: 0x000000010bb47ba2 UIKitCore`UIApplicationMain + 140
        frame #9: 0x00000001079913d0 runloop`main(argc=1, argv=0x00007ffee826ef98) at main.m:14:16
        frame #10: 0x000000010a6ab541 libdyld.dylib`start + 1
        frame #11: 0x000000010a6ab541 libdyld.dylib`start + 1
    (lldb) 
    

    frame #4: 0x0000000108d7785a CoreFoundation`__CFRunLoopDoTimers + 266
    验证事件类型是timer

    RunLoop 运行机制

    RunLoop.png

    每次线程运行RunLoop都会自动处理之前未处理的消息,并且将消息发送给观察者,让事件得到执行。RunLoop运行时首先根据modeName找到对应mode,如果mode里没有source/timer/observer,直接返回。
    流程如下:
    Step1 通知观察者 RunLoop 启动(之后调用内部函数,进入Loop,下面的流程都在Loop内部do-while函数中执行)
    Step2 通知观察者: RunLoop 即将触发 Timer 回调。(kCFRunLoopBeforeTimers)
    Step3 通知观察者: RunLoop 即将触发 Source0 回调。(kCFRunLoopBeforeSources)
    Step4 RunLoop 触发 Source0 回调。
    Step5 如果有 Source1 处于等待状态,直接处理这个 Source1 然后跳转到第9步处理消息。
    Step6 通知观察者:RunLoop 的线程即将进入休眠(sleep)。(kCFRunLoopBeforeWaiting)
    Step7 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒

    1. 存在Source0被标记为待处理,系统调用CFRunLoopWakeUp唤醒线程处理事件
    2. 定时器时间到了
    3. RunLoop自身的超时时间到了
    4. RunLoop外部调用者唤醒

    Step8 通知观察者线程已经被唤醒 (kCFRunLoopAfterWaiting)
    Step9 处理事件

    1. 如果一个 Timer 到时间了,触发这个Timer的回调
    2. 如果有dispatch到main_queue的block,执行block
    3. 如果一个 Source1 发出事件了,处理这个事件

    事件处理完成进行判断:

    1. 进入loop时传入参数指明处理完事件就返回(stopAfterHandle)
    2. 超出传入参数标记的超时时间(timeout)
    3. 被外部调用者强制停止__CFRunLoopIsStopped(runloop)
    4. source/timer/observer 全都空了__CFRunLoopModeIsEmpty(runloop, currentMode)

    上面4个条件都不满足,即没超时、mode里没空、loop也没被停止,那继续loop。此时跳转到步骤2继续循环。
    Step10 系统通知观察者: RunLoop 即将退出。
    满足步骤9事件处理完成判断4条中的任何一条,跳出do-while函数的内部,通知观察者Loop结束。

    RunLoop退出

    主线程销毁RunLoop退出
    Mode中有一些Timer 、Source、 Observer,这些保证Mode不为空时保证RunLoop没有空转并且是在运行的,当Mode中为空的时候,RunLoop会立刻退出
    我们在启动RunLoop的时候可以设置什么时候停止

    - (void)addRunLoopObserver {
        //1.创建监听者
        /*
         第一个参数:怎么分配存储空间
         第二个参数:要监听的状态 kCFRunLoopAllActivities 所有的状态
         第三个参数:时候持续监听
         第四个参数:优先级 总是传0
         第五个参数:当状态改变时候的回调
         */
        CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
            
            /*
             kCFRunLoopEntry = (1UL << 0),        即将进入runloop
             kCFRunLoopBeforeTimers = (1UL << 1), 即将处理timer事件
             kCFRunLoopBeforeSources = (1UL << 2),即将处理source事件
             kCFRunLoopBeforeWaiting = (1UL << 5),即将进入睡眠
             kCFRunLoopAfterWaiting = (1UL << 6), 被唤醒
             kCFRunLoopExit = (1UL << 7),         runloop退出
             kCFRunLoopAllActivities = 0x0FFFFFFFU
             */
            switch (activity) {
                case kCFRunLoopEntry:
                    NSLog(@"即将进入runloop");
                    break;
                case kCFRunLoopBeforeTimers:
                    NSLog(@"即将处理timer事件");
                    break;
                case kCFRunLoopBeforeSources:
                    NSLog(@"即将处理source事件");
                    break;
                case kCFRunLoopBeforeWaiting:
                    NSLog(@"即将进入睡眠");
                    break;
                case kCFRunLoopAfterWaiting:
                    NSLog(@"被唤醒");
                    break;
                case kCFRunLoopExit:
                    NSLog(@"runloop退出");
                    break;
                    
                default:
                    break;
            }
        });
        
        /*
         第一个参数:要监听哪个runloop
         第二个参数:观察者
         第三个参数:运行模式
         */
        CFRunLoopAddObserver(CFRunLoopGetCurrent(),observer, kCFRunLoopDefaultMode);
    }
    
    - (void)test2 {
        [self performSelector:@selector(testPerformSelector2) onThread:self.thread withObject:nil waitUntilDone:NO];
    }
    
    - (NSThread *)thread {
        _thread = [[NSThread alloc] initWithTarget:self selector:@selector(testSel2) object:nil];
        [_thread start];
        return _thread;
    }
    
    - (void)testSel2 {
        [self addRunLoopObserver];
        [[NSThread currentThread] setName:@"test2"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        //启动runloop保持线程活跃
    //    [runLoop run];
        //启动runloop保持线程活跃,并设定时间退出runloop
        [runLoop runUntilDate:[NSDate date]];
    //    [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate date]];
    }
    

    RunLoop实际应用

    AutoreleasePool

    App启动之后,系统启动主线程并创建了RunLoop,在 main thread 中注册了两个 observer ,回调都是_wrapRunLoopWithAutoreleasePoolHandler()

    第一个observer

    监听了一个事件:

    1. 即将进入Loop(kCFRunLoopEntry)
      其回调会调用 _objc_autoreleasePoolPush() 创建一个栈自动释放池,这个优先级最高,保证创建释放池在其他操作之前。

    第二个observer

    监听了两个事件:

    1. 准备进入休眠(kCFRunLoopBeforeWaiting)
      此时调用 _objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 来释放旧的池并创建新的池。
    2. 即将退出Loop(kCFRunLoopExit)

    此时调用 _objc_autoreleasePoolPop()释放自动释放池。这个 observer 的优先级最低,确保池子释放在所有回调之后。

    在主线程中执行代码一般都是写在事件回调或Timer回调中的,这些回调都被加入了main thread的自动释放池中,所以在ARC模式下我们不用关心对象什么时候释放,也不用去创建和管理pool。(如果事件不在主线程中要注意创建自动释放池,否则可能会出现内存泄漏)。

    事件响应

    苹果注册了一个 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 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

    界面更新

    当在操作 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];
    

    定时器

    NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

    如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

    我们在平时开发中一个很常见的现象:

    在界面上有一个UIscrollview控件(tableview,collectionview等),如果此时还有一个定时器在执行一个事件,你会发现当你滚动scrollview的时候,定时器会失效。

    这是因为,为了更好的用户体验,在主线程中UITrackingRunLoopMode的优先级最高。在用户拖动控件时,主线程的Run Loop是运行在UITrackingRunLoopMode下,而创建的Timer是默认关联为Default Mode,因此系统不会立即执行Default Mode下接收的事件。

    解决方法1:
    将当前 Timer 加入到 UITrackingRunLoopMode 或 kCFRunLoopCommonModes 中

    NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(TimerFire:) userInfo:nil repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];  
    // 或 [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    [timer fire];
    

    解决方法2: 因为GCD创建的定时器不受RunLoop的影响,可以使用GCD创建的定时器

        //dispatch_source_t必须是全局或static变量,否则timer不会触发
        static dispatch_source_t timer;
        //创建新的调度源(这里传入的是DISPATCH_SOURCE_TYPE_TIMER,创建的是Timer调度
        timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
        dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
        dispatch_source_set_event_handler(timer, ^{
            NSLog(@"%@",[NSThread currentThread]);
        });
        //启动或继续定时器
        dispatch_resume(timer);
    

    CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop,这个稍后我会再单独写一页博客来分析。

    PerformSelecter

    当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。

    当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

    关于GCD

    实际上 RunLoop 底层也会用到 GCD 的东西,比如 RunLoop 是用 dispatch_source_t 实现的 Timer(评论中有人提醒,NSTimer 是用了 XNU 内核的 mk_timer,我也仔细调试了一下,发现 NSTimer 确实是由 mk_timer 驱动,而非 GCD 驱动的)。但同时 GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async()。

    当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。
    block内断点调用栈:

    2019-09-05 00:01:16.998971+0800 runloop[50722:722751] 2
    (lldb) bt
    * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
      * frame #0: 0x0000000103e54187 runloop`__23-[ViewController test5]_block_invoke(.block_descriptor=0x0000000103e56168) at ViewController.m:186:9
        frame #1: 0x00000001069f3d7f libdispatch.dylib`_dispatch_call_block_and_release + 12
        frame #2: 0x00000001069f4db5 libdispatch.dylib`_dispatch_client_callout + 8
        frame #3: 0x0000000106a02080 libdispatch.dylib`_dispatch_main_queue_callback_4CF + 1540
        frame #4: 0x00000001050f18a9 CoreFoundation`__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
        frame #5: 0x00000001050ebf56 CoreFoundation`__CFRunLoopRun + 2310
        frame #6: 0x00000001050eb302 CoreFoundation`CFRunLoopRunSpecific + 626
        frame #7: 0x000000010d6872fe GraphicsServices`GSEventRunModal + 65
        frame #8: 0x0000000107f05ba2 UIKitCore`UIApplicationMain + 140
        frame #9: 0x0000000103e54340 runloop`main(argc=1, argv=0x00007ffeebdabf98) at main.m:14:16
        frame #10: 0x0000000106a69541 libdyld.dylib`start + 1
        frame #11: 0x0000000106a69541 libdyld.dylib`start + 1
    (lldb) 
    

    关于网络请求
    iOS 中,关于网络请求的接口自下至上有如下几层:

    CFSocket
    CFNetwork ->ASIHttpRequest
    NSURLConnection ->AFNetworking
    NSURLSession ->AFNetworking2, Alamofire

    • CFSocket 是最底层的接口,只负责 socket 通信。
    • CFNetwork 是基于 CFSocket 等接口的上层封装,ASIHttpRequest 工作于这一层。
    • NSURLConnection 是基于 CFNetwork 的更高层的封装,提供面向对象的接口,AFNetworking 工作于这一层。
    • NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 并列的,但底层仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 线程),AFNetworking2 和 Alamofire 工作于这一层。

    下面主要介绍下 NSURLConnection 的工作过程。

    通常使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。实际上,start 这个函数的内部会会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4个 Source0 (即需要手动触发的Source)。CFMultiplexerSource 是负责各种 Delegate 回调的,CFHTTPCookieStorage 是处理各种 Cookie 的。

    当开始网络传输时,我们可以看到 NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。

    RunLoop_network.png

    NSURLConnectionLoader 中的 RunLoop 通过一些基于 mach port 的 Source 接收来自底层 CFSocket 的通知。当收到通知后,其会在合适的时机向 CFMultiplexerSource 等 Source0 发送通知,同时唤醒 Delegate 线程的 RunLoop 来让其处理这些通知。CFMultiplexerSource 会在 Delegate 线程的 RunLoop 对 Delegate 执行实际的回调。

    AFNetworking 的工作原理

    在AFNetworking2.6.3版本之前是有 AFURLConnectionOperation 这个类的,
    AFNetworking 3.0 版本开始已经移除了这个类,AFN没有自己创建线程,而是采用的下面的这种方式

    [inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];  
    
    SCNetworkReachabilityUnscheduleFromRunLoop(self.networkReachability, CFRunLoopGetMain(), kCFRunLoopCommonModes);
    

    由于本文讨论的是RunLoop,所以这里我们还是回到2.6.3版本AFN自己创建线程并添加RunLoop的这种方式讨论,在 AFURLConnectionOperation 类中可以找到下面的代码

    + (void)networkRequestThreadEntryPoint:(id)__unused object {
        @autoreleasepool {
            [[NSThread currentThread] setName:@"AFNetworking"];
            NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
            [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
            [runLoop run];
        }
    }
    
    + (NSThread *)networkRequestThread {
        static NSThread *_networkRequestThread = nil;
        static dispatch_once_t oncePredicate;
        dispatch_once(&oncePredicate, ^{
            _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
            [_networkRequestThread start];
        });
    
        return _networkRequestThread;
    }
    

    从上面的代码可以看出,AFN创建了一个新的线程命名为 AFNetworking ,然后在这个线程中创建了一个 RunLoop ,在上面2.3章节 RunLoop 运行机制中提到了,一个RunLoop中如果source/timer/observer 都为空则会退出,并不进入循环。所以,AFN在这里为 RunLoop 添加了一个 NSMachPort ,这个port开启相当于添加了一个Source1事件源,但是这个事件源并没有真正的监听什么东西,只是为了不让 RunLoop 退出。

    //开始请求
    - (void)start {
        [self.lock lock];
        if ([self isCancelled]) {
            [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
        } else if ([self isReady]) {
            self.state = AFOperationExecutingState;
            [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
        }
        [self.lock unlock];
    }
    //暂停请求
    - (void)pause {
        if ([self isPaused] || [self isFinished] || [self isCancelled]) {
            return;
        }
        [self.lock lock];
        if ([self isExecuting]) {
            [self performSelector:@selector(operationDidPause) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    
            dispatch_async(dispatch_get_main_queue(), ^{
                NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
                [notificationCenter postNotificationName:AFNetworkingOperationDidFinishNotification object:self];
            });
        }
        self.state = AFOperationPausedState;
        [self.lock unlock];
    }
    //取消请求
    - (void)cancel {
        [self.lock lock];
        if (![self isFinished] && ![self isCancelled]) {
            [super cancel];
            if ([self isExecuting]) {
                [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
            }
        }
        [self.lock unlock];
    }
    

    可以看到,AFN每次进行的网络操作,开始、暂停、取消操作时都将相应的执行任务扔进了自己创建的线程的 RunLoop 中进行处理,从而避免造成主线程的阻塞。

    参考:
    RunLoop的前世今生
    深入理解RunLoop

    Demo地址:RunLoop

    相关文章

      网友评论

          本文标题:iOS RunLoop

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