Runloop

转载自李峰峰博客: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_第1张图片
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 的接口进行了封装。他们的关系如下:


Runloop_第2张图片
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 *)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 : {contents = "UITrackingRunLoopMode"}
    //2 : {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)