美文网首页
RunLoop学习笔记

RunLoop学习笔记

作者: 玉米安爱吃甜玉米 | 来源:发表于2017-03-15 20:10 被阅读0次

    一、什么是 RunLoop

    1. 从字面上来说,RunLoop 可以被翻译为跑圈,但通常状况下我们都称之为运行循环或者消息循环
    2. RunLoop 对我们开发者来说有什么意义呢?
      1.)RunLoop 可以让程序运行不退出我们所写的程序之所以可以一直运行,而不是像程序中的方法一样,运行完就结束,就是 RunLoop 的功劳,所以 RunLoop 也可以被理解为一个死循环(下面会详细解释)
      2.)Runloop 可以让程序等待事件的输入
      3.)没有事件的时候,使线程休眠,一旦有事件输入,就唤醒线程去处理

    二、初探 RunLoop
    打个招呼

    现阶段,我们在平时的开发中,与 RunLoop 接触最多的地方,大概就是 NSTimer 了。

    NSTimer *timer = [NSTimer timerWithTimeInterval:1
                                            repeats:YES
                                            block:^(NSTimer * _Nonnull timer) {
                                                      NSLog(@"定时器正在运转");
                                                  }];
        [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
    

    那么,这个 RunLoop 具体又是什么呢?我们可以打印一下
    NSLog(@"%@",[NSRunLoop currentRunLoop]

    Snip20170106_5.png

    我的天啊,怎么打印出这么一堆东西,那些 Sources/Observers/Timers 究竟是什么,OMG~~~~
    然而你会发现此时的你并不清楚这些是什么,没关系,我们会慢慢解释

    又见面了

    前面说到我们打印了 RunLoop ,结果打印出许多难以理解的东西,现在我们就来研究一下,这些让人头晕的东西究竟是什么

    打开 RunLoop 的头文件,我们看到了下面这些

    Snip20170106_6.png

    对照着前面的打印结果再看,这下是不是感觉好点了?我们再往下看

    头文件的描述是不是看的你依旧有些晕晕的?没关系,我们继续

    根据官方文档中的描述,与 RunLoop 有关的类有五个(即上图中显示的五个类)
    那么这五个类又是做什么的呢,我们再看一张图

    3F75B370-6F1D-44E3-B595-3568DC6E0790.png

    一个 RunLoop 可以有多个 Mode (模式),每个模式又包含若干个 Source/Observer/Timer
    这里请注意上图中Source/Observer/Timer 的类型,除却 Source 是 NSSet 类型,Observer 及 Timer 都是 NSArray 类型的,这说明一个 Mode 中实际上是包含了许多的 Source/Observer/Timer

    RunLoop 一次只能运行一种 Mode ,这个 Mode 被称为CurrentMode ,
    切换 Mode 需要退出当前 Mode ,这样做是为了避免不同组的 Source/Observer/Timer 混淆

    这里需要注意的是Runloop一次只能运行一个mode,并不与上述的一个RunLoop 可以有多个Mode 冲突,下面会具体解释
    那么 Source/Observer/Timer/Mode 究竟又是什么呢?

    在研究这个问题的之前,我们再来看看一些基础的东西
    我们已经知道,程序之所以可以不退出,在没有事件需要处理的时候会进入睡眠状态,等待接收事件,那么这是为什么呢?

    实际上,这是因为在主线程中有一个RunLoop 一直在运行,并且这个 RunLoop 是系统创建的,如果我们想要拿到这个 RunLoop ,只需要在主线程中调用 方法 currentRunLoop 就可以了

    Snip20170106_7.png

    主线程的 RunLoop 是在什么时候创建的呢?我们可以探究一下

    当我们创建一个干净项目的时候,项目里除了基础文件,别的什么也不会有,当然除了 Main.m 文件中,那么猫腻会不会就出现在这里呢?

    //新建项目中,唯一一个显得不是那么干净的方法
    #import <UIKit/UIKit.h>
    #import "AppDelegate.h"
    int main(int argc, char * argv[]) {
        @autoreleasepool {
            return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        }
    }
    

    我们对代码进行如下测试

    int main(int argc, char * argv[]) {
                NSLog(@"000 - 我要出来了");
                @autoreleasepool {
                    return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
                    NSLog(@"111 -- 你猜我能出来吗");
                }
                NSLog(@"222 -- 你猜我能出来吗");
            }
    
    Snip20170106_9.png

    最终只有第一句打印了出来,所以我们不得不猜测是不是因为 main 函数中的方法是一个死循环,所以后面的代码不会执行

    当然猜测并没有用,我们还是需要找到证据,打开 RunLoop 的源码,找到 RunLoop 的实现方法 (Apple的部分源文件已经开源,这里附上下载地址:https://opensource.apple.com/release/os-x-1010.html 进入页面下载 CF-1151.16 即可

    Snip20170106_10.png

    由于代码过长,我们对代码进行了折叠,这段三百多行的代码中,有我们 RunLoop 会一直运行的原因

    Snip20170106_12.png

    这段代码中,我同样折叠了一部分代码,然而我们可以很清楚的看到一个 do while 循环,这就是我们的 RunLoop 可以循环的原因。
    原先我一直认为这段代码就是RunLoop中最关键的代码,直到跟一位前辈的进行交流时才发现我对RunLoop的理解有一部分是不正确的。正真的核心代码其实是这一段。

    do {
                if (kCFUseCollectableAllocator) {
                    // objc_clear_stack(0);
                    // <rdar://problem/16393959>
                    memset(msg_buffer, 0, sizeof(msg_buffer));
                }
                msg = (mach_msg_header_t *)msg_buffer;
                
                __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
                
                if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
                    // Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer.
                    while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
                    if (rlm->_timerFired) {
                        // Leave livePort as the queue port, and service timers below
                        rlm->_timerFired = false;
                        break;
                    } else {
                        if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
                    }
                } else {
                    // Go ahead and leave the inner loop.
                    break;
                }
            } while (1);
    

    我们需要清楚的一点是,一个 RunLoop 对应一个线程,这一点我们同样可以去 RunLoop 的源文件中看到

    iOS 中有两套API访问 Foundation(NSRunLoop), CoreFoundation (CFRunLoopRef)
    使用 NSRunLoop ,我们可以通过

    //使用 NSRunloop ,我们可以通过下面两个方法分别拿到当前线程以及主线程的 Runloop 
    [NSRunLoop mainRunLoop];
    [NSRunLoop currentRunLoop];
    
    //使用 CFRunLoopRef ,我们可以通过下面两个方法分别拿到当前线程以及主线程的 Runloop CFRunLoopGetMain();
    CFRunLoopGetCurrent();
    

    打开 CFRunLoopRef 的源文件,搜索 CFRunLoopGetCurrent,找到 CFRunLoopGetCurrent 的函数方法

    Snip20170106_8.png

    我们可以看到这个方法最终返回的是一个函数方法的结果,搜索这个函数方法,我们去看看这个方法究竟是何方神圣

    // should only be called by Foundation
            // t==0 is a synonym for "main thread" that always works
            CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
                if (pthread_equal(t, kNilPthreadT)) {
                    t = pthread_main_thread_np();
                }
                __CFLock(&loopsLock);
                if (!__CFRunLoops) {
                    __CFUnlock(&loopsLock);
                    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
                    CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
                    CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
                    if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
                        CFRelease(dict);
                    }
                    CFRelease(mainLoop);
                    __CFLock(&loopsLock);
                }
                CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
                __CFUnlock(&loopsLock);
                if (!loop) {
                    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
                    __CFLock(&loopsLock);
                    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
                    if (!loop) {
                        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
                        loop = newLoop;
                    }
                    // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
                    __CFUnlock(&loopsLock);
                    CFRelease(newLoop);
                }
                if (pthread_equal(t, pthread_self())) {
                    _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
                    if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
                        _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
                    }
                }
                return loop;
            }
    

    可以很清楚的看见,这个函数实际上就是创建 RunLoop 的方法,并且需要传入一个线程

    这会儿,大家应该清醒点了吧
    那就让你们的视线集中到下面这张图上

    03AAAA02-5AAE-4F07-8FA8-E256EDE401F8.png

    这张图是官方文档中的,很直观的描述了 RunLoop
    一个线程中有一个 RunLoop ,接收到事件之后会对事件进行操作,没有事件的时候会进入睡眠状态。

    接收事件的类型分为两类
    输入源:输入源可以是键盘鼠标,NSPort, NSConnection 等对象
    定时源:NSTimer事件

    现在我们真的要去了解一下 Source/Observer/Timer/Mode 了

    CFRunLoopSourceRef
    按照官方文档,Source分类(理论性的分类,了解就好)
    Port - Based Sources
    Custom Input Sources
    Cocoa Perform Selector Sources

    按照函数调用栈,Source分类(实际应用中的)
    Source0: 非基于Port
    Source1: 基于Port,通过内核和其他线程通信,接收、分发系统事件

    通过函数调用堆栈进一步了解 Source


    Snip20170106_13.png

    函数调用堆栈是倒过来看的,在途中我们可以清楚的看见标红的位置就是 Source0 事件,它在 RunLoop 运行后被执行,虽然我们没有看见 Source1 ,但这并不代表它没有,当有事件产生式,系统会将事件包装由 Source1 分发给 Source0 处理

    CFRunLoopObserverRef
    它是一个观察者,用来监听RunLoop状态的改变,可以监听以下几个状态

    /* Run Loop Observer Activities */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   //所有行为
    };
    

    创建观察者,监听 RunLoop 的状态改变

    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
                NSLog(@"---RunLoop状态发生变化----%lu",activity);
            });
            CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);
            //注意,这里需要对 observer 进行 release
            CFRelease(observer
                      );
    

    注意内存管理,凡是带有Create、Copy、Retain等字眼的函数,创建出来的对象,都需要在最后做一次Release
    打印出来的结果如下,我们可以通过 Observer 对程序的运行进行拦截,让程序在我们需要的时候做我们想做的事情


    AB9D90E6-D6C8-4925-8674-59BCBEF715D3.png

    这里用一张图片让大家更直观的去看到 Observer 的状态改变


    C281FECD-0901-4116-9911-F84B5A2BC546.png

    CFRunLoopTimerRef

    NSTimer *timer = [NSTimer timerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) {
                NSLog(@"定时器正在运转");
            }];
            
            //定时器只在 NSDefaultRunLoopMode 的模式下运行,一旦切换到其他模式,将不再运行
            //可以使用UIScorllView及其子类来验证,这里为了方便,我们选取用UITextFiled来检验
            [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
            
            //定时器被标记为 NSRunLoopCommonModes ,切换到其他模式也可以运行
            //具体为什么,在 CFRunLoopModeRef 中我们再进行了解
            [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
    

    下图是使用UITextFiled检验定时器运行的结果


    Snip20170106_15.png

    图中标注位置,有明显时间间隔,这段时间,是对 UITextFiled 进行了拖动,定时器停止运行,一旦松开,定时器又再度运行

    CFRunLoopModeRef
    CFRunLoopModeRef 类并没有对外暴露,只是通过CFRunLoopRef 的接口进行了封装
    系统默认注册了5个Mode
    kCFRunLoopDefaultMode: App的默认Mode,通常主线程是在这个Mode下运行
    UITrackingRunLoopMode: 界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他的Mode影响
    UIInitializationRunLoopMode: 在刚启动App是进入的第一个Mode,启动完成后就不再使用
    GSEventReceiveRunLoopMode: 接受系统时间的内部Mode,通常用不到
    kCFRunLoopCommonMode: 这是一个占位用的Mode,不是一种真正的Mode

                            上文添加定时器时,给RunLoop指定为此Mode,只是起到一个标记作用,实际上是给让RunLoop拥有两种模式,即kCFRunLoopDefaultMode、UITrackingRunLoopMode,所以定时器才可以在两种模式下都能运行
    

    最后附上孙源大神,15年博客中有关RunLoop的面试题及答案
    [※※※]runloop和线程有什么关系?

    总的说来,Run loop,正如其名,loop表示某种循环,和run放在一起就表示一直在运行着的循环。实际上,run loop和线程是紧密相连的,可以这样说run loop是为了线程而生,没有线程,它就没有存在的必要。Run loops是线程的基础架构部分, Cocoa 和 CoreFundation 都提供了 run loop 对象方便配置和管理线程的 run loop (以下都以 Cocoa 为例)。每个线程,包括程序的主线程( main thread )都有与之相应的 run loop 对象。

    runloop 和线程的关系:
    主线程的run loop默认是启动的。

    iOS的应用程序里面,程序启动后会有一个如下的main()函数

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

    重点是UIApplicationMain()函数,这个方法会为main thread设置一个NSRunLoop对象,这就解释了:为什么我们的应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。

    对其它线程来说,run loop默认是没有启动的,如果你需要更多的线程交互则可以手动配置和启动,如果线程只是去执行一个长时间的已确定的任务则不需要。

    在任何一个 Cocoa 程序的线程中,都可以通过以下代码来获取到当前线程的 run loop 。

    NSRunLoop *runloop = [NSRunLoop currentRunLoop];

    参考链接:《Objective-C之run loop详解》。

    [※※※]runloop的mode作用是什么?

    model 主要是用来指定事件在运行循环中的优先级的,分为:

    NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态

    UITrackingRunLoopMode:ScrollView滑动时

    UIInitializationRunLoopMode:启动时

    NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合

    苹果公开提供的 Mode 有两个:

    NSDefaultRunLoopMode(kCFRunLoopDefaultMode)

    NSRunLoopCommonModes(kCFRunLoopCommonModes)

    [※※※※]以+ scheduledTimerWithTimeInterval...
    的方式触发的timer,在滑动页面上的列表时,timer会暂定回调,为什么?如何解决?

    RunLoop只能运行在一种mode下,如果要换mode,当前的loop也需要停下重启成新的。利用这个机制,ScrollView滚动过程中NSDefaultRunLoopMode(kCFRunLoopDefaultMode)的mode会切换到UITrackingRunLoopMode来保证ScrollView的流畅滑动:只能在NSDefaultRunLoopMode模式下处理的事件会影响ScrollView的滑动。

    如果我们把一个NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环中的时候, ScrollView滚动过程中会因为mode的切换,而导致NSTimer将不再被调度。

    同时因为mode还是可定制的,所以:

    Timer计时会被scrollView的滑动影响的问题可以通过将timer添加到>NSRunLoopCommonModes(kCFRunLoopCommonModes)来解决。代码如下:

            // http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)
            // https://github.com/ChenYilong
            
            //将timer添加到NSDefaultRunLoopMode中
            [NSTimer scheduledTimerWithTimeInterval:1.0
                                             target:self
                                           selector:@selector(timerTick:)
                                           userInfo:nil
                                            repeats:YES];
            //然后再添加到NSRunLoopCommonModes里
            NSTimer *timer = [NSTimer timerWithTimeInterval:1.0
                                                     target:self
                                                   selector:@selector(timerTick:)
                                                   userInfo:nil
                                                    repeats:YES];
            [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    

    [※※※※※]猜想runloop内部是如何实现的?

    一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑 是这样的:

    function loop() {
                initialize();
                do {
                    var message = get_next_message();
                    process_message(message);
                } while (message != quit);
            }
    
    

    或使用伪代码来展示下:

            // http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)
            // https://github.com/ChenYilong
            int main(int argc, char * argv[]) {
                //程序一直运行状态
                while (AppIsRunning) {
                    //睡眠状态,等待唤醒事件
                    id whoWakesMe = SleepForWakingUp();
                    //得到唤醒事件
                    id event = GetEvent(whoWakesMe);
                    //开始处理事件
                    HandleEvent(event);
                }
                return 0;
            }
    

    参考链接:
    《深入理解RunLoop》
    摘自博文CFRunLoop,原作者是微博@我就叫Sunny怎么了

    相关文章

      网友评论

          本文标题:RunLoop学习笔记

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