RunLoop - 知识点总结

作者: devZhang | 来源:发表于2016-07-26 18:18 被阅读446次

    RunLoop 初窥:

    • 从字面意思看:运行循环, 跑圈
    • 其实它内部就是一个 do-while 循环, 在这个循环内部不断的处理各种任务(比如 source, timer, observer)

    基本作用

    1. 保持程序的持续运行
    2. 处理 APP 中的各种事件(触摸事件, 定时器事件, Selector 事件等)
    3. 节省 CPU 资源, 提高程序性能, 该做事时做事, 该休息时休息

    在程序中的体现

    • 在有 RunLoop 的情况下,由于 main 函数里面启动了 RunLoop, 所以程序不会马上退出, 保持持续运行状态
    • main.m 里的 UIApplicationMain 函数内部会启动一个 RunLoop
    • 所以 UIApplicationMain 函数一直没有返回,保持了程序的持续运行
    • 这个默认启动的 RunLoop 是跟主线程相关联的

    RunLoop 对象

    iOS 中有两套 API 来访问和使用 RunLoop

    • Foundation -- 用 NSRunLoop 访问

    • Core Foundation -- 用 CFRunLoopRef 访问

    NSRunLoop 和 CFRunLoopRef 都代表 RunLoop 对象, NSRunLoop是在基于CFRunLoopRef的一层 OC 封装

    RunLoop 与线程

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

    获得 RunLoop 对象

    Foundation

        [NSRunLoop currentRunLoop];     // 获得当前线程的 RunLoop 对象
        [NSRunLoop mainRunLoop];     // 获得主线程的 RunLoop 对象
    

    Core Foundation

       CFRunLoopGetCurrent(); // 获得当前线程的 RunLoop 对象
       CFRunLoopGetMain();  // 获得主线程的 RunLoop 对象
    

    在代码中是这样的

    // 1. 获得主线程对应的 RunLoop
       NSRunLoop *mainRunLoop = [NSRunLoop mainRunLoop];
       
       // 2. 获得当前线程对应的 RunLoop
       NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
       
       // 两者内存地址是一样的
       NSLog(@"%p - %p", mainRunLoop, currentRunLoop);
       //NSLog(@"%@", mainRunLoop);
       
       // 3. core
       NSLog(@"%p - %p", CFRunLoopGetMain(), CFRunLoopGetCurrent());
    

    通过打印结果我们可以发现, 当前线程的 RunLoop 和主线程的 RunLoop 内存地址是一样的

    RunLoop 和线程的关系

    两者是一一对应的. 只是主线程的 RunLoop 已经创建, 子线程的 RunLoop 需要手动创建.

    [[[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil] start];
    

    实现run 方法

    - (void)run {
        // 创建子线程对应的 RunLoop.   currentRunLoop是懒加载方法,当第一次调用时先判断对应的 RunLoop 是否存在,如果不存在,自己创建并返回,如果存在就直接返回
        NSLog(@"%@", [NSRunLoop currentRunLoop]);
        
        NSLog(@"run - %@", [NSThread currentThread]);
    }
    

    RunLoop 相关类

    Core Foundation 中关于 RunLoop 的5个类

    • CFRunLoopRef
    • CFRunLoopModeRef
    • CFRunLoopSourceRef
    • CFRunLoopTimeRef
    • CFRunLoopObserverRef

    CFRunLoopModeRef

    系统默认注册了5个 Mode:

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

    我们平时主要用到的是前两种

    每个 RunLoop 中可能有两个或者多个 Mode,

    mode 元素:

    • Source - 事件源
    • Observer - 观察者
    • Timer - 定时器事件

    注意:

    • 虽然 RunLoop 中有多个运行模式, 但是启动之后只能选择一种模式运行 ,这种 Mode 被称作 CurrentMode
    • mode 里至少要有一个 Timer 或者 Source
    • 如果需要切换 Mode, 只能先退出 Loop, 再重新指定一个 Mode 进入. 这样做的好处主要是为了分隔开不同组的 Source/Timer/Observer, 让其互不影响

    当 RunLoop 中添加定时器时, 选择不同的 mode 会有不同的效果

    NSDefaultRunLoopMode

    // 1. 创建定时器
        NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run2) userInfo:nil repeats:YES];
        
        // 2. 添加到 RunLoop 中, 指定 RunLoop 的运行模式为默认模式
        /*
         第一个参数: 定时器
         第二个参数: RunLoop 的运行模式
         */
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; 
    

    但是这时候有个问题, 当拖拽界面其他控件(例如 scrollView 和 textView)的时候,定时器不工作. 松开点击后才工作

    UITrackingRunLoopMode

        [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    

    这个模式下只有拖拽界面其他控件的时候,定时器才工作,但停止拖拽, 依然不工作

    想要实现无论在什么情况下, 定时器都能正常工作,可以这样

    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    

    确实能达到效果,但看起来并不是那么完美. 这是时候可以用另一种 mode
    NSRunLoopCommonModes

    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    

    我们先来看下NSRunLoopCommonModes里默认保存是什么 mode

    NSLog(@"%@", [NSRunLoop currentRunLoop]);
    

    打印结果:


    由此可知,NSRunLoopCommonModes里面就有前面前面需要的两个 mode,这个时候在运行工程, 无论是拖拽还是不拖拽, 定时器都能正常工作了.

    CFRunLoopTimerRef

    • CFRunLoopTimerRef 是基于时间的触发器, 基本说的都是 NSTimer
    • GCD 也有定时器功能, 和 NSTimer 定时器有一些区别, 这个在之前专门学习GCD使用的博客里也有提到(iOS - GCD 编程). 今天采用传统方法创建 GCD 来继续学习下
     // 1. 创建 GCD 中的定时器
        /*
         第一个参数:source 的类型,DISPATCH_SOURCE_TYPE_TIMER 表示定时器
         第二个参数:描述信息,线程 ID
         第三个参数:更详细的描述信息
         第四个参数:队列, 决定 GCD 定时器中的任务在哪个线程中执行
         */
        dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0)); // 程序并发队列
        
        // 2. 设置定时器-- 起始时间和间隔时间以及精准度
        /*
         第一个参数:timer, 定时器对象
         第二个参数:起始时间,DISPATCH_TIME_NOW 从现在开始几时
         第三个参数:间隔时间 GCD 中的时间单位是纳秒
         第四个参数:精准度 绝对精准 0
         */
        dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
        
        // 3. 设置定时器要执行的任务
        dispatch_source_set_event_handler(timer, ^{
            NSLog(@"GCD - %@", [NSThread currentThread]);
        });
        
        // 4. 启动执行
        dispatch_resume(timer);
    

    这个时候调用该方法,发现定时器并没有工作,需要先生命一个 定时器属性

    @property (nonatomic, strong) dispatch_source_t timer;
    

    然后在上面代码的最后给属性赋值

    self.timer = timer;
    

    这个时候定时器就正常工作了.

    GCD 与 NSTimer 比较
    优点: GCD 不受 RunLoop 运行运行模式的影响.

    CFRunLoopSourceRef

    CFRunLoopSourceRef 是事件源, 也就是输入源

    以前的方法:

    • Port-Based Sources : 基于端口的事件
    • Custom Input Sources : 自定义的事件
    • Cocoa Perform Selector Sources : perform selector 事件

    现在的方法:

    • Sources0: 非基于 Port 的, 可以理解为是用户主动触发的事件
    • Sources1: 基于 Port 的,可以理解为是系统事件

    用户点击按钮事件触发的就是Source0, 应征了上面所说的


    CFRunLoopObserverRef

    CFRunLoopObserverRef 是观察者, 能够监听 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
        kCFRunLoopAllActivities = 0x0FFFFFFFU
    };
    

    声明一个方法

    - (void)observer {
        
        // 1. 创建观察者
        /*
         第一个参数: 怎么分配存储空间
         第二个参数: 要监听 RunLoop 的哪些状态
         第三个参数: 是否要持续监听
         第四个参数: 优先级 0 就可以
         第五个参数: 当状态改变的时候会回调 block
         */
        CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler( CFAllocatorGetDefault(),  kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
            
            switch (activity) {
                case kCFRunLoopEntry:
                    NSLog(@"即将进入 Loop");
                    break;
                case kCFRunLoopBeforeTimers:
                    NSLog(@"即将处理 Timer");
                    break;
                case kCFRunLoopBeforeSources:
                    NSLog(@"即将处理 Source");
                    break;
                case kCFRunLoopBeforeWaiting:
                    NSLog(@"即将进入休眠");
                    break;
                case kCFRunLoopAfterWaiting:
                    NSLog(@"刚从休眠中唤醒");
                    break;
                case kCFRunLoopExit:
                    NSLog(@"即将退出 Loop");
                    break;
            }
            
        });
        
        // 2. 给 RunLoop 添加观察者
        /*
         第一个参数: 要监听哪个 RunLoop
         第二个参数: 观察者
         第三个参数: 运行模式, 需要传 C语言方法
         */
        CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode); 
    }
    

    在给 RunLoop 添加观察者的时候有提到, 运行mode 需要传 C语言方法,其实
    NSDefaultRunLoopMode 就等同于 kCFRunLoopDefaultMode
    NSRunLoopCommonModes 就等同于 kCFRunLoopCommonModes

    运行结果:


    我们可以看到 RunLoop 运行的各个状态,加上定时器,效果更加明显一些

    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(task) userInfo:nil repeats:YES];
    

    会看到 RunLoop 状态会重复变化,说明 RunLoop 不会彻底退出,只要有任务,会一直执行


    所以,我们可以得出一条结论: 通过监听 RunLoop 的状态可以让其进行相应的操作

    RunLoop 处理逻辑

    官方版文字介绍


    这里找到了网上一个处理流程图文介绍


    可以清晰的看出 RunLoop 的整个处理流程, 而 RunLoop 最终的状态就是 休眠 状态, 一旦有任务就会唤醒

    RunLoop 应用

    RunLoop 介绍的差不多了,来看下它都有哪些应用:

    开启常驻线程

    常驻线程就是让一个子线程不进入消亡状态, 等待其他线程发来消息,及时处理其他任务事件

    下图是没有开启常驻线程时, 线程直接就 end 了,显然不能满足我们的需求


    我们可以创建一个定时器,以保证线程不会结束,但这样显然也不完美,因为无论有没有任务,定时器都一直在工作


    这时候,我们就可以让 RunLoop 调用 addPort: forMode:方法,这个时候在运行, 线程就不会 end 了,在有新任务要执行的时候也能及时处理,也就达到了我们的目的.

    总结

    1. RunLoop 概念:
    • 从字面意思看:运行循环, 跑圈
    • 其实它内部就是一个 do-while 循环, 在这个循环内部不断的处理各种任务(比如 source, timer, observer)
    • 一个线程对应一个 RunLoop, 主线程的 RunLoop 默认已经启动, 子线程的 RunLoop 需要手动启动(调用 run 方法)
    • RunLoop 只能选个一个mode 启动,如果当前 mode 中没有任何 Source(Source0, Source1), Timer, Observer, 那么就直接退出 RunLoop
    1. RunLoop自动释放池什么时候创建
    • 启动 RunLoop 的时候就创建
    1. RunLoop自动释放池什么时候释放
    • RunLoop 退出的时候释放
    • RunLoop 即将进入睡眠的时候会销毁之前的自动释放池. 并重新创建一个新的
    1. 在开发中如何使用 RunLoop? 什么应用场景?
    • 开启一个常驻线程(让一个子线程不进入消亡状态, 等待其他线程发来消息,处理其他事件)
    • 在子线程中开启一个定时器
    • 在子线程中进行一些长期监控
    • 可以控制定时器在特定模式下执行
    • 可以让某些事情(行为或者任务)在特定模式下执行
    • 可以添加 Observer 监听 RunLoop 的状态, 比如监听点击事件的处理(在所有点击事件之前做一些事情)

    相关文章

      网友评论

      • _方丈:感觉不是很深入,大致概念性,不过也已经不错了
      • 眯大帅:好多main都写成了mian
        devZhang:@眯大帅 不好意思,这个当时是直接在 MWeb 里敲的,没有在 Xcode 里复制. 我刚查了下,有两处地方写错的, 已经改正了. 谢谢之处我的错误 :blush:

      本文标题:RunLoop - 知识点总结

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