美文网首页
RunLoop 梳理

RunLoop 梳理

作者: 扬仔360 | 来源:发表于2018-04-03 18:03 被阅读70次

    简介

    存在的必要性:应用程序需要一直运行,所以需要一个以事件驱动为基础的事件循环机制。

    RunLoop 的架构主题就是一个死循环,一个事件循环,没有退出就一直运行。这个循环具体的实现后文有详细的描述。

    int main() {
        init();
        do {
            var message = get_next_message();
            prcess_mesage(message);
        } while (message != quit);
    }
    

    调用解耦:主调方把需要执行的逻辑放入消息队列中。执行方从消息队列中拿到并执行,与主调方实现解耦。

    RunLoop 系统上的应用

    • NSTime 完全基于 RunLoop 去实现
    • UIEvent 完全也基于 RunLoop 去执行 和 分发
    • Autorelease 也基于 RunLoop,在Objective-C 高级编程那本书中有提到
    • NSObj 中的延迟执行方法也是基于
    • NSObj 中 NSThreadPerformAddition
    • CATransition CAAnimation 也都是基于,UI 刷新基本都是基于 RunLoop
    • dispatch_get_main_queue()
    • NSURLConnection & AFNetworking: NSURLConnection 回调就是在 RunLoop 上跑执行的
    • 以及我们调试用的调用堆栈,有很多 RunLoop 的身影
    • 其实基本上 OC 中所有的方法都是 RunLoop 分发出去的,这个文中后面方法名又长又臭的那里有解释
    • ·····

    框架结构

    两个对象:

    • Foundation 层的 NSRunLoop
    • CoreFoundation 层的 CFRunLoopRed

    CFRunLoopRef 在 CoreFoundation 框架中,提供纯 C 的函数供调用,线程安全,并且开源的
    NSRunLoop 基于 CFRunLoopRef 的对象封装,体面面向对象的 API。不做具体的事情。

    创建过程

    不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()

    用于获得 mainRunLoop 以及 currentRunLoop

    下面代码这里来自:YY 的 RunLoop 博客 -- 深入理解RunLoop

    /// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
    static CFMutableDictionaryRef loopsDic;
    /// 访问 loopsDic 时的锁
    static CFSpinLock_t loopsLock;
     
    CFRunLoopRef CFRunLoopGetMain() {
        return _CFRunLoopGet(pthread_main_thread_np());
    }
     
    CFRunLoopRef CFRunLoopGetCurrent() {
        return _CFRunLoopGet(pthread_self());
    }
    
    /// 获取一个 pthread 对应的 RunLoop。
    CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
        OSSpinLockLock(&loopsLock);
        
        if (!loopsDic) {
            // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
            loopsDic = CFDictionaryCreateMutable();
            CFRunLoopRef mainLoop = _CFRunLoopCreate();
            CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
        }
        
        /// 直接从 Dictionary 里获取。
        CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
        
        if (!loop) {
            /// 取不到时,创建一个
            loop = _CFRunLoopCreate();
            CFDictionarySetValue(loopsDic, thread, loop);
            /// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
            _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
        }
        
        OSSpinLockUnLock(&loopsLock);
        return loop;
    }
    
    

    通过创建的过程,分析 RunLoop 与线程的关系

    创建 RunLoop 的方法表明了 RunLoop 与线程一定是一一对应的关系,尤其体现在 loopsDic 这个字典,字典中 key 是 pthread_t, value 是 CFRunLoopRef

    在任何线程中,只能获取当前线程的 runLoop 和主线程的 runLoop。即上文中的 mainRunLoopcurrentRunLoop

    内部结构

    runloop_img01.png

    CFRunLoop 与线程是一一对应的,一个 Thread 里面可以有很多 RunLoop。

    CFRunLoopMode:CFRunLoop 对应一个或多个 Mode,CFRunTimeMode

    Mode 之下有 Timer Sources Observer

    可以理解成: Source timer 以及 Observer 才是外部真正关心的事情,Mode 是对他们做的区分,最外通过 CFRunLoopRef 进行封装。

    CFRunLoopTimer

    是个基于事件的触发器,有时间和回调的地址,时间到达的时候,会执行回调地址的函数。

    具体的应用有:NSTime、延迟执行performSelectorAfterDelay、displayLink。

    CFRunLoopSource

    Source 是 RunLoop 的数据输入源,是一个抽象的 Protocol,符合这个 Source 的对象,才可以在 RunLoop 上面去执行,具体的看下面的 CFRunLoopSourceContext

    Source 有两个版本:Source0 Source1

    Source0:属于 App 内部事件,App 自己去负责管理和触发,比如:UIEvent、CFSocket。

    Source0 中只有回调的指函数针,不能主动出发事件。

    Source1:由 RunLoop 和 内核管理,Mach port 驱动(进程直接通讯的方式),比如:CFMachPort、CFMessagePort

    可以基于 这个 Source Protool 自己实现一个 Source,基本不会去实现,不过没想到什么应用场景

    struct __CFRunLoopSource {
        CFRuntimeBase _base;
        uint32_t _bits;
        pthread_mutex_t _lock;
        CFIndex _order;         
        CFMutableBagRef _runLoops;
        union {
            CFRunLoopSourceContext version0;
            CFRunLoopSourceContext1 version1;
        } _context;
    };
    
    // union 中的 CFRunLoopSourceContect version0:
    typedef struct {
        CFIndex version;
        void *  info;
        const void *(*retain)(const void *info);
        void    (*release)(const void *info);
        CFStringRef (*copyDescription)(const void *info);
        Boolean (*equal)(const void *info1, const void *info2);
        CFHashCode  (*hash)(const void *info);
        void    (*schedule)(void *info, CFRunLoopRef rl, CFStringRef mode);
        void    (*cancel)(void *info, CFRunLoopRef rl, CFStringRef mode);
        void    (*perform)(void *info);
    } CFRunLoopSourceContext;
    
    

    union 中的很多都是函数指针,需要实现体自己去实现,比如 内存管理的 retain release copy equal hash。最重要的是最后的一个 perform 方法,真正去调用的方法。

    CFRunLoopObserver

    RunLoop 开放给外部的 观察者,相当于 Delegate,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化:

    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
        kCFRunLoopEntry         = (1UL << 0), // 即将进入 Loop
        kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
        kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
        kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
        kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
        kCFRunLoopExit          = (1UL << 7), // 即将退出 Loop
    };
    

    框架中的一些机制结合了 RunLoopObserver,比如 CAAnimation,会有一些动画的延迟机制

    CFRunLoopMode 比较重要

    RunLoop 一定有且仅有一个 Mode

    如果要切换,这个 runLoop 会 quit,重新走一个循环

    iOS 滑动的时候,是 Mode: UITrackingRunLoopMode

    所有的Mode:

    NSDefaultRunLoopMode          默认状态、空闲状态
    UITrackingRunLoopMode         滑动 ScrollView 时,iOS 流程的关键
    UIInitializationRunLoopMode   私有,App 启动时是这个 Mode,成功后切换成第一个 Mode
    NSRunLoopCommonModes          []是一个数组,第一个 Mode 和第二个结合,都可以执行
    

    Timer 与 Mode

    滑动的时候,Time 是不会跑的,因为 RunLoop 在 UITrackingRunLoopMode 上。

    解决方法就是加入到 NSRunLoopCommonModes 中去:

    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0
                                                 target:self
                                               selector:@selector(timerTick:)
                                               userInfo:nil
                                                repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    

    注意:GCD 的 Timer 是内核单独维护的,跟 RunLoop 平级,只是落地点是在 RunLoop 上。

    RunLoop 执行过程简化记录

        //进入循环之前,调用GCDTimer,设置过期时间,否则就真的变成死循环了
        SetupThisRunLoopRunTimeoutTimer(); // by GCD timer
    
        do {
            //进入循环 告诉 Observer 告诉要进入 timers sources
            __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
            __CFRunLoopDoObservers(kCFRunLoopBeforeSources);
            
            __CFRunLoopDoBlocks();
            //遍历 Source 0 执行
            __CFRunLoopDoSource0();
            
            //需要跑的代码直执行:__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__         
            CheckIfExistMessagesInMainDispatchQueue(); // GCD
            
            //调用 Observers BeforeWaiting
            __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
            
            //挂起方法,进入 trap,卡在这里,等待唤醒,获得唤醒的端口号
            var wakeUpPort = SleepAndWaitForWakingUpPorts();
            // mach_msg_trap
    
            // Received mach_msg, wake up
            
            __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
            
            // Handle msgs   如果是Timer 唤醒的,就跑 Timer
            if (wakeUpPort == timerPort) {
                __CFRunLoopDoTimers();
                
                // 如果是 GCD 唤醒的
            } else if (wakeUpPort == mainDispatchQueuePort) {
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
                
                // 基于 port 唤醒的,比如网络来数据了,就去处理数据
            } else {
                __CFRunLoopDoSource1();
            }
           __CFRunLoopDoBlocks();
           
           // 判断是不是停止了, timeout 了没有
       } while (!stop && !timeout);
    

    RunLoop 的底层

    OS X / iOS 中最底层是 Drawin 层,这个层面中包括了 mach port,在 <mach/message.h> 中有 mach 的定义:

    typedef struct {
      mach_msg_header_t header;
      mach_msg_body_t body;
    } mach_msg_base_t;
     
    typedef struct {
      mach_msg_bits_t msgh_bits;
      mach_msg_size_t msgh_size;
      mach_port_t msgh_remote_port;
      mach_port_t msgh_local_port;
      mach_port_name_t msgh_voucher_port;
      mach_msg_id_t msgh_id;
    } mach_msg_header_t;
    

    发送和接受消息的 API 如下,option 标记了消息传递的方向:

    mach_msg_return_t mach_msg(
                mach_msg_header_t *msg,
                mach_msg_option_t option,
                mach_msg_size_t send_size,
                mach_msg_size_t rcv_size,
                mach_port_name_t rcv_name,
                mach_msg_timeout_t timeout,
                mach_port_name_t notify);
    

    RunLoop 的挂起和唤醒

    RunLoop 的另一个核心就在于 mach_msg(),RunLoop 调用这个方法后去等待唤醒,并获得唤醒源进行判断。

    调用 mach_msg 监听唤醒端口,被唤醒前,系统内核将这个线程挂起,停留在 mach_msg_trap 状态。

    由另一个线程(或另一个进程中的某个线程)向内核发送这个端口的 msg 后,trap 状态被唤醒,RunLoop 继续执行。

    RunLoop 中进入 trap 等待唤醒其实就是一个 mach_msg()。例如你在模拟器里跑起一个 iOS 的 App,然后在 App 静止时点击暂停,你会看到主线程调用栈是停留在 mach_msg_trap() 这个地方。

    RunLoop Callouts 调用外部方法的途径

    当 RunLoop 调用 modeItems 中的外部指针的时候,都是通过一个很长的函数调的。

    所以几乎所有的函数都是由这些方法调起的:

    1. __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
    2. __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
    3. __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
    4. __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
    5. __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
    6. __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
    
    1. 回调 Observer
    2. Source 0 的时候调用 block
    3. GCD 调用主线程,分发到 mainRunLoop 中执行
    4. Time 唤醒 RunLoop 的话,回调 Timer
    5. 触发 Source0 (非基于 port 的) 回调
    6. 触发 Source1 (mach_port 的) 回调

    命名成这样也是为了在调用栈里面可以自解释。

    与 GCD 的关系

    __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ 如果在 GCD 中派发到主线程,那么就会分发到 mainRunLoop 中执行。

    dispatch_after 同理。

    RunLoopObserver 与 Autorelease Pool

    Objective-C 高级编程那本书中有提到:

    RunLoop 的每次循环过程中,NSAutoreleasePool 对象被生成或废弃
    

    准确的说,RunLoop 的下两次 Sleep/Trap 过程之间,上一次的 NSAutoreleasePool 有了时机去执行 drain()。

    Apple 在 mainRunLoop 中注册两个 Observer:

    当进入 Loop 的时候,调用 _objc_autoreleasePoolPush() 创建 自动释放池,这个操作的优先级是最小的 Int,确保发生在其他所有的回调之前。

    第二个 Observer 发生在 Trap 休眠发成之前,会释放旧池,并创建新池。Quit 的时候也会释放旧池,这个操作优先级是最大的 Int,确保发生在其他所有的回调之后。

    事件响应 与 手势的应用

    事件响应上,iOS 的实现是注册一个基于 mach_port 的 Source1 去接收系统事件,回调函数为__IOHIDEventSystemClientQueueCallback()

    IOKit 生成 IOHIDEvent,仅能由 SpringBoard 接收(iOS 6.1加入的限制),然后发送给各个需要的 App 的 mach_port,_UIApplicationHandleEventQueue() 会包装成 UIEvent,包括了 gesture、屏幕旋转、点击等,发送给 UIWindow。这里罗列了目前支持的 service 以及 keyboard events

    事件响应过程图示:
    Touch发生 ----> IOKit 感知
                     |
                     |
                     V
               生成 IOHIDEvent
                     |
                     |
                     V
    ---------------     ---------------
    |                                 |
    |          SpringBoard            |
    |                                 |                 
    -----------------------------------
                |
                |  传递到需要的 App 的 mach_port ---> 
                |         入口:RunLoop 的 Source 1:
                |            _IOHIDEventSystemClientQueueCallback()
                V
           ----   ----
           |         |   _UIApplicationHandleEventQueue()
           |   App   |     接收并生成 UIEvent --> UIWindow
           |         |
           -----------
    

    像 UIButton 的点击,touchBegin 等也都是在这个回调中完成的。

    手势的实现:在 RunLoop beforeWaiting (即将休眠的时候)注册了,_UIGestureRecognizerUpdateObserver() 获得所有待处理的 GestureRecognizer,并执行回调。

    关于网络请求

        CFSocket
        CFNetWorking     -> ASIHttpRequest
        NSURLConnection  -> AFNetworking
        NSURLSession     -> AFNetworking 2, Alamofire
    

    NSURLConnection 工作的时候,创建一个 自己的线程 C 和 CFSocket 的线程 S。

    底层的 S 通过 C 的 Source 1 通知到 C,C 再通过 currentRunLoop 的 Source 0 去 通知到最上层的 Delegate。

    实践 1:AFN 中对于 RunLoop 的实践 -- 常驻线程

    下面是 AFN 的两端初始化的代码,第二个方法是 AFN 创建线程的方法,其中调用第一个方法,其结果是 AFN 这个单例创建持有了一个常驻的线程 + RunLoop,给 RunLoop 添加一个 mach port,一直去监听,这个 port 不会发送东西,让这个 RunLoop 一直不被销毁。

    这是 AFN 线程常驻的一个很好的方法。

    + (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;
    }
    

    实践 2:TableView 加载图片的优化

    滑动的时候设置图片会影响帧数,通过下面的代码,避开 trackingMode 的 RunLoop,在 default 中执行:

        UIImage *downloadedImage = ...;
        [self.avatarImageView performSelector:@selector(setImage:)
                                   withObject:downloadedImage
                                   afterDelay:0
                                      inModes:@[NSDefaultRunLoopMode]];
    

    参考资料:

    RunLoop文章:

    其他:

    相关文章

      网友评论

          本文标题:RunLoop 梳理

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