关于RunLoop

今天学习runloop,开始啦~~

一.RunLoop的概念

一般来讲一个线程只能执行一次任务,执行完后就会退出。Runloop就可以让线程能随时处理事件但不退出。

1.什么是runloop?(面试题

  • runloop是通过内部维护的事件循环来对事件、消息进行管理的一个对象
  • 没有消息处理时,用户态--》内核态。休眠以避免资源占用。
  • 有消息时,内核态--》用户态。立刻被唤醒。

2.关于用户态、内核态

  • 应用程序一般是运行在用户态上面的,开发中绝大多数的api都是在用户层面的。
  • 需要使用到操作系统、底层内部的指令,就发生了系统调用。有的系统调用会触发空间的切换。在内核态上。
  • 之所以区分用户态和内核态,实际上是对计算机的一些资源调度、资源管理进行统一。这样就可以合理的进行资源调度,避免异常。

比如在内核态会有一些指令、终端、关机开机的操作。假如每个app都可以进行开机关机的操作,这个场景导致的效果是无法想象的。

3.main()函数为什么不会退出?(面试题

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

如上,main函数里面调用了UIApplicationMain,UIApplicationMain里面会启动线程的runloop。
main函数会一直处于“接受消息->处理->等待” 的循环中,直到这个循环结束。达到runloop可以做到有事情的时候做事情,没有事情的时候从用户态转换为内核态,避免资源浪费。

main()函数状态切换

4.Runloop对象

关于runloop,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。

  1. CFRunLoopRef是在 CoreFoundation 框架内(这个框架是开源的http://opensource.apple.com/tarballs/CF/)的,它提供了纯 C 函数的 API。
  2. NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API。

CFRunLoopRef

获得主线程的runloop

    CFRunLoopRef mainRef = CFRunLoopGetMain();

获得当前的runloop对象

   CFRunLoopRef currentRef = CFRunLoopGetCurrent();

NSRunLoop

获得主线程的runloop

    NSRunLoop * mainRunloop = [NSRunLoop mainRunLoop];

获得当前的runloop对象

    NSRunLoop * currentRunloop = [NSRunLoop currentRunLoop];

runloop的OC和C的API互相转换:

    NSRunLoop * mainRunloop = [NSRunLoop mainRunLoop];
    NSLog(@"%p-----%p",mainRunloop.getCFRunLoop,mainRunloop);

二. RunLoop与线程的关系

  1. 每个线程都有唯一的一个与之对应的Runloop对象。(其关系是保存在一个全局的 Dictionary 里。)

  2. 主线程的Runloop已经创建好了,子线程的Runloop需要主动创建

  3. 苹果不允许直接创建 RunLoop,可以通过CFRunLoopGetMain() 和 CFRunLoopGetCurrent()第一次获取的时候创建,在线程结束时销毁

  4. 只能在一个线程的内部获取其 RunLoop(主线程除外)

如下

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

     //获取子线程的runloop
    [NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
}
-(void)run{
    
    //获得子线程对应的runloop|currentRunloop
    //该方法本身是懒加载的,如果是第一次调用那么会创建当前线程对应的runloop并保存,以后则直接获取。
    //创建
    NSRunLoop * newThreadRunloop = [NSRunLoop currentRunLoop];

    //开启runloop(该runloop开启后马上退出了,因为runloop需要一个mode才能运行)
    [newThreadRunloop run];
    
}

三.RunLoop相关类/数据结构/对外的接口

CoreFoundation 里面关于 RunLoop 有5个类:

CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

1.CFRunLoopRef

RunLoop.png

如上图:

  • RunLoop和Mode关系:一对多
    Mode和 Source/Timer/Observer关系:一对多

  • 每次runloop启动的时候,只能指定一个mode。即currentMode。

  • 如果要切换mode,只能退出loop,再重新指定一个mode进入。
    (这么做是为了分开不同的mode里面的source/timer/observer),让其互不影响。

2. CFRunLoopSourceRef

CFRunLoopSourceRef 是事件产生的地方.

  • source0
    需要手动唤醒线程。先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
  • source1
    具备唤醒线程的能力。包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。

3. CFRunLoopTimerRef

CFRunLoopTimerRef 是基于时间的触发器。和 NSTimer 是toll-free bridged(免费桥接) 的, 可以混用。

4. CFRunLoopObserverRef

CFRunLoopObserverRef 是观察者。
每个 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
};

如下:为当前runloop的状态添加监听

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
    //01 创建观察者
    /*
     第一个参数:allocator,用于分配存储空间的;用默认的CFAllocatorGetDefault
     第二个参数:要监听的状态
     第三个参数:是否要持续监听
     第四个参数:和优先级相关的;传0
     第五个参数:当runloop状态改变的时候会调用这个block块
     */
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
       
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"runloop启动");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"runloop即将处理timer事件");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"runloop即将处理sourece事件");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"runloop即将进入休眠");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"runloop休眠结束");
                break;
            case kCFRunLoopExit:
                NSLog(@"runloop退出");
                break;
            default:
                break;
        }
        
    });
    
    //02 监听runloop的状态
    /*
     第一个参数:runloop对象
     第二个参数:监听者
     第三个参数:runloop在哪种运行模式下的状态
     kCFRunLoopDefaultMode == NSDefaultRunLoopMode
     kCFRunLoopCommonModes == NSRunLoopCommonModes
     
     */
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    
  
}

三. RunLoop的Mode

  • 系统默认注册了5个mode
  1. NSDefaultRunLoopMode
    默认的mode,通常主线程是在这个mode下运行;
  1. UITrackingRunLoopMode
    界面跟踪的mode,用于scrollview追踪滑动,保证界面滑动时不受其他mode的影响;
  1. UIInitalizationRunLoopMode
    在app启动时第一个mode,启动完成后就不再使用
  1. GSEventReceiveRunLoopMode:
    接收系统内部的mode,通常用不到
  1. NSRunLoopCommonModes
    这是一个占位符mode,不是一个实际存在的mode
    是同步Source/Timer/Oberver到多个Mode的一种技术方案
  • CFRunLoopMode 和 CFRunLoop 的结构大致如下:
struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
    ...
};
 
struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set<NSString *>
    CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode;    // 当前的 RunloopMode
    CFMutableSetRef _modes;           // Set< CFRunLoopMode *>
    ...
};

上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

上面的common modes有两种:kCFRunLoopDefaultMode和UITrackingRunLoopMode

     common modes = <CFBasicHash 0x604000445430 [0x10589f960]>{type = mutable set, count = 2,
     entries =>
     0 : <CFString 0x106c0f060 [0x10589f960]>{contents = "UITrackingRunLoopMode"}
     2 : <CFString 0x105875790 [0x10589f960]>{contents = "kCFRunLoopDefaultMode"}
     }

     */

RunLoop需要选择一个Mode才可以运行起来。
1)Runloop下面有很多个mode,运行的时候需要选择其中一个mode。
2)然后判断这个mode的item是否为空。Mode里面有一些Source、timer、Observer。判断有Source或者Observer,或者两者都有,说明这个mode不为空。则可以运行这个runloop了。

1. RunLoop的运行模式和NSTimer

1.1 NSTimer创建定时器方法1--需要指定mode

下面的mode,如果指定为NSDefaultRunLoopMode,则默认情况下定时器可以运行。
但是界面滑动的时候定时器也要可以操作,就需要指定为TrackingRunLoopMode。
想要默认和滑动模式下都可以运行定时器,那么就需要指定模式为NSRunLoopCommonModes。

   //01创建定时器对象
    NSTimer * timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(run) userInfo:nil repeats:YES];

    //02 把定时器对象添加到runloop中,并指定运行模式为默认。
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
 

1.2 NSTimer创建定时器方法2--mode为kCFRunLoopDefaultMode

scheduledTimerWithTimeInterval这个方法不需要手动启动runloop,会自动设置运行模式为kCFRunLoopDefaultMode。

如果需要让定时器在UITrackingRunLoopMode也运行,添加即可。

     //该方法会自动将创建定时器对象添加到当前的runloop中
    //运行模式为默认
   NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(run) userInfo:nil repeats:YES];
    
    //把当前的定时器也可以在滚动下运行,添加缺少的tracking模式即可
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

1.3 子线程添加timer--需要手动开启runloop

子线程里面没有runloop,所以使用NSTimer在子线程添加定时器是没有用的。需要开启runloop。

-(void)timer3{
    
    [self performSelectorInBackground:@selector(addTimerForthread) withObject:nil];
}
-(void)addTimerForthread{

    //这个方法里面指定模式为默认模式
    NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(run1) userInfo:nil repeats:YES];

    //子线程直接添加timer不会执行,需要手动添加runloop
    //注意,要在指定运行 模式之后再开启runloop,runloop才能执行
    [[NSRunLoop currentRunLoop] run];
    
    NSLog(@"thread:%@",[NSThread currentThread]);
}

2. RunLoop的运行模式和GCD中的定时器--不会受到影响

上面记录到,NSTimer中的定时器工作会受到runloop运行模式的影响,而GCD中的定时器不会受到影响

@interface ViewController ()

@property(nonatomic,strong)dispatch_source_t timer;

@end
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
    //NSTimer中的定时器工作会受到runloop运行模式的影响
    //GCD中的定时器不会受到影响
    
    //01 创建定时器对象
    //队列(GCD)决定代码块在哪个线程中执行(主队列--主线程|非主队列--子线程中)
//    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0,dispatch_get_global_queue(0, 0));

    //02 设置定时器(开始时间|调用时间|误差)
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);//2:2秒执行一次;0:0秒误差
    //03 事件回调
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"GCD Thread:%@",[NSThread currentThread]);
    });
    //04 起点定时器
    dispatch_resume(timer);
    
    _timer  = timer;
}

四.RunLoop的实现机制

RunLoop的实现机制
  • 即将进入runloop;(通知observer,对应kCFRunLoopEntry)
  • 将要处理Timer/Source0事件(通知observer,对应kCFRunLoopBeforeTimers、kCFRunLoopBeforeSources)
  • 处理Timer、Source0事件
  • 如果有Source1事件要处理,处理Source1事件
  • 没有Source1事件,线程将要休眠(通知observer,对应kCFRunLoopBeforeWaiting)
  • 线程进入休眠
  • 线程收到消息,被唤醒(通知observer,对应kCFRunLoopAfterWaiting),去处理收到的消息
  • 即将推出RunLoop的时候(通知observer,对应kCFRunLoopExit)

问:处于一个休眠的runloop,怎么唤醒?(面试)
Source1事件、NSTimer事件、外部手动唤醒

五.RunLoop的底层实现

  • RunLoop 的核心是基于 mach port 的,其进入休眠时调用的函数是 mach_msg()。
  • mach_msg() 函数实际上是调用了一个 Mach 陷阱 (trap),即函数mach_msg_trap(),陷阱这个概念在 Mach 中等同于系统调用。
  • 当你在用户态调用 mach_msg_trap() 时会触发陷阱机制,切换到核心态;核心态中内核实现的 mach_msg() 函数会完成实际的工作,如下图:
RunLoop的底层实现

-----未完待续-----

六.苹果用 RunLoop 实现的功能

概念---
与多线程---
相关类/数据结构/对外的接口---
与NSTimer---
CFRunLoopObserverRef---

内部逻辑/事件循环机制---
底层实现---

source0如何手动唤醒线程

问:处于一个休眠的runloop,怎么唤醒?(面试)
Source1事件、NSTimer事件、外部手动唤醒???

看看放哪里

苹果用runloop实现的功能
应用:
[self.imageVie performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"1.png"] afterDelay:3 inModes:@[NSRunLoopCommonModes]];

AFNet
Async

常驻线程

面试问题汇总:

https://blog.ibireme.com/2015/05/18/runloop/

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 文章转载自:ibireme 博客博客地址:https://blog.ibireme.com/2015/05/18/...
    flyrees阅读 237评论 0 0
  • 一、什么是runloop 字面意思是“消息循环、运行循环”。它不是线程,但它和线程息息相关。一般来讲,一个线程一次...
    WeiHing阅读 8,191评论 11 111
  • 今天在学习线程安全的时候,看到了多线程编程指南,提到了关于runloop的部分,再学习一下runloop。 1.什...
    8fe8946fa366阅读 339评论 1 0
  • 1.RunLoop是什么? Runloop顾名思义就是运行循环。在iOS平时开发过程中,我们的UIApplicat...
    wyler阅读 92评论 0 1
  • 前言 简书博客开篇,写些什么呢?想了好久,脑海中跳出了许多主题,Runtime、RunLoop、Swift、内存管...
    KavinZhou阅读 598评论 3 10