Runloop

作者: 乂滥好人 | 来源:发表于2018-07-31 15:26 被阅读7次

    转载自李峰峰博客:https://juejin.im/entry/587c2c4ab123db005df459a1
    Runloop官方文档
    参考:https://www.cnblogs.com/kenshincui/p/6823841.html#

    1、Runloop与线程间的关系

    • 每条线程都有唯一的一个与之对应的RunLoop对象。
    • 主线程的RunLoop已经自动创建好了,子线程的RunLoop需要主动创建。
    • RunLoop在第一次获取时创建,在线程结束时销毁。

    2、主线程相关联的RunLoop创建

    CFRunLoopRef源码:
    // 创建字典
     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)
    

    3、子线程相关联的RunLoop创建

    苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:

    • 获得主线程的RunLoop对象:CFRunLoopGetMain()
    • 获得当前线程的RunLoop对象:CFRunLoopGetCurrent()
    CFRunLoopRef源码:
    // 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
    static CFMutableDictionaryRef loopsDic;
    // 访问 loopsDic 时的锁
    static CFSpinLock_t loopsLock;
    
    // 获取一个 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;
    }
    
    CFRunLoopRef CFRunLoopGetMain() {
        return _CFRunLoopGet(pthread_main_thread_np());
    }
    
    CFRunLoopRef CFRunLoopGetCurrent() {
        return _CFRunLoopGet(pthread_self());
    }
    

    可以看出,线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。

    [NSRunLoop currentRunLoop];方法调用时,会先看一下字典里有没有存子线程相对用的RunLoop,如果有则直接返回RunLoop,如果没有则会创建一个,并将与之对应的子线程存入字典中。


    201901更新


    runloop是为线程而生,没有线程runloop就没有存在的必要!

    1、什么是runloop?

    runloop是事件接收和分发机制的一个对象,处理运行中出现的各种事件(如:选择器事件、计时器事件、UI刷新事件、触摸事件),保证程序的持续运行。一个线程只能执行一个任务,执行完就休眠(节省CPU资源,提高性能),等待下次唤醒继续工作。

    2、基本作用

    2.1、保持程序运行
    程序一启动,默认创建并开启主线程,主线程不会被销毁,保证持续运行,从而让程序持续运行。
    2.2、处理各类事件
    选择器事件、计时器事件、UI刷新事件、触摸事件
    2.3、节省CPU、提高性能
    当程序启动,没有事件处理,runloop会通知cpu现在无需工作,要去休眠,此时,cpu就会把资源释放出来去处理其他事情。当接收到事件开启线程处理,runloop会立即唤醒处理事件。

    runloop运行原理

    右边 【输入源】将异步事件传递给相应的处理程序,并调用runUntilDate:方法。【计时器源】交给左边线程的runloop处理,左边runloop会把右边输入源传过来的消息交给runloop对应的方法去处理,处理完成结束。

    3、runloop开启

    3.1、主线程runloop开启
    程序入口main函数开启主线程的runloop,一点开启,只有但程序关闭才会销毁主线程的runloop,这也证明了runloop保证了程序的持续运行。

    int main(int argc, char * argv[]) {
        @autoreleasepool {
            return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        }
    }
    

    runloop源码

    // 用DefaultMode启动
    void CFRunLoopRun(void) {    /* DOES CALLOUT */
        int32_t result;
        do {
            result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
            CHECK_FOR_FORK();
        } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
    }
    

    源码表明,runloop是do while判断result的值来实现的,如果没有开启主线程runloop 则程序将直接返回,结束运行。

    4、runloop与线程

    4.1、runloop与线程的关系
    每一条线程都有唯一对应的runloop对象。
    主线程的runloop在程序启动的时候默认开启,子线程的runloop需要手动创建并开启。
    runloop在第一次获取的时候创建,在线程结束的时候销毁/休眠。
    4.2、runloop的创建
    主线程创建源码

    // 创建字典
     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);
    

    子线程创建源码

    不允许直接创建runloop,只能通过API获取
    获取主线程runloop:CFRunLoopGetMain()
    获取当前线程runloop:CFRunLoopGetCurrent()

    线程和runloop是一一对应的,源码说明 【线程为key, runloop为value】的形式存储在一个【全局字典】当中。
    流程:当使用[NSRunLoop currentRunLoop];获取runloop的时候,如果值(runloop)为空,则初始化全局字典并创建一个runloop,存储到全局字典中。如果有则直接从字典中取,当取不到的时候则创建一个,并注册一个回调,当线程处理事件后销毁,把对应的runloo也销毁/休眠。

    // 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
    static CFMutableDictionaryRef loopsDic;
    // 访问 loopsDic 时的锁
    static CFSpinLock_t loopsLock;
    
    // 获取一个 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;
    }
    
    CFRunLoopRef CFRunLoopGetMain() {
        return _CFRunLoopGet(pthread_main_thread_np());
    }
    
    CFRunLoopRef CFRunLoopGetCurrent() {
        return _CFRunLoopGet(pthread_self());
    }
    
    5、runloop相关类

    CFRunLoopRef //获得当前RunLoop和主RunLoop
    CFRunLoopModeRef //运行模式,只能选择一种,在不同模式中做不同的操作
    CFRunLoopSourceRef //事件源,输入源
    CFRunLoopTimerRef //定时器时间
    CFRunLoopObserverRef //观察者

    5.1、CFRunLoopModeRef 运行Model
    系统默认注册了5个Model

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

    其中 CFRunLoopModeRef 类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装。他们的关系如下:


    runloopModel

    在runloop中包含了若干个Model,每个 Model又包含了若干个 Source/Timer/Observer。每次调用runloop主函数时,只能指定一个model,这个model称为CurrentMode(当前线程model)。如果需要切换model,需先退出当前的runloop,再指定新的model。这样的目的是分隔不同组的model,互不影响。
    mode中的Source/Timer/Observer 被统称为model item,一个item可以被同时加入到多个model中,但一个item被重复添加到同一个model是不会有效果。如果model中一个item都没有,则runloop会直接退出,不会进入循环。

    Model间的切换

    使用NSTimer每一段时间执行一些事情时滑动UIScrollView,NSTimer就会暂停,当我们停止滑动以后,NSTimer又会重新恢复的情况。
    解决方案:使用占位的运行Model NSRunLoopCommonModes标记,凡是被打上NSRunLoopCommonModes标记的在默认model及滑动Model中都可以运行。

    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        // [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
        NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
        // 加入到RunLoop中才可以运行
        // 1. 把定时器添加到RunLoop中,并且选择默认运行模式NSDefaultRunLoopMode = kCFRunLoopDefaultMode
        // [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
        // 当textFiled滑动的时候,timer失效,停止滑动时,timer恢复
        // 原因:当textFiled滑动的时候,RunLoop的Mode会自动切换成UITrackingRunLoopMode模式,因此timer失效,当停止滑动,RunLoop又会切换回NSDefaultRunLoopMode模式,因此timer又会重新启动了
    
        // 2. 当我们将timer添加到UITrackingRunLoopMode模式中,此时只有我们在滑动textField时timer才会运行
        // [[NSRunLoop mainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    
        // 3. 那个如何让timer在两个模式下都可以运行呢?
        // 3.1 在两个模式下都添加timer 是可以的,但是timer添加了两次,并不是同一个timer
        // 3.2 使用站位的运行模式 NSRunLoopCommonModes标记,凡是被打上NSRunLoopCommonModes标记的都可以运行,下面两种模式被打上标签
        //0 : <CFString 0x10b7fe210 [0x10a8c7a40]>{contents = "UITrackingRunLoopMode"}
        //2 : <CFString 0x10a8e85e0 [0x10a8c7a40]>{contents = "kCFRunLoopDefaultMode"}
        // 因此也就是说如果我们使用NSRunLoopCommonModes,timer可以在UITrackingRunLoopMode,kCFRunLoopDefaultMode两种模式下运行
        [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
        NSLog(@"%@",[NSRunLoop mainRunLoop]);
    }
    -(void)show
    {
        NSLog(@"-------");
    }
    

    5.2、CFRunLoopSourceRef事件源,输入源
    Source分为两种:
    Source0:非基于Port的 用于用户主动触发的事件(点击button 或点击屏幕)
    Source1:基于Port的 通过内核和其他线程相互发送消息(与内核相关)
    注意:Source1在处理的时候会分发一些操作给Source0去处理

    【事件响应和手势识别底层处理是一致的吗,为什么?】
    事件响应:
    苹果注册了一个 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

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