• iOS 开发之RunLoop


    概念

    RunLoop 就像她的名字一样,就是跑环,就是一个死循环。是一个可以随时休眠,随时唤醒的死循环。

    那么一个手机App为什么会一直运行?而且在接受到用户点击的时候,会做出反应?这些都离不开RunLoop。

    iOS App启动的时候,就会自动启动一个RunLoop。一直在循环监听着用户的各种操作,并作出反应。每个线程都有一个RunLoop,但是,只有主线程的RunLoop是默认开启的。可以这样理解:

    1. RunLoop 是iOS消息机制的处理模式

    NSRunLoop的主要作用:控制NSrunLoop里面的线程的执行和休眠,在有事做的时候,使当前的RunLoop控制线程工作,没事做的时候让当前的NS RunLoop控制线程休息。

    2.NSRunLoop就是一只在循环检测,从线程start到线程的end,检测inputSource(点击,双击等操作)同步事件,检测timeSource(计时器)同步操作。检测到输入源会执行处理函数,首先会产生通知,corefunction向线程添加runloop observers来监听事件,意在监听事件发生时来做处理。

    iOS main函数中的RunLoop

    int main(int argc, char * argv[]) {
        @autoreleasepool {
            //一旦程序启动会开启一个RunLoop 一直循环监听用户的点击事件 触摸事件 定时器事件等 并且一直不会返回。
          保证程序一直运行,直到程序结束。这个默认的RunLoop就是跟主线程相关的。
    int rs = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); return rs; } }

    iOS中RunLoop对象。

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

    一套是 Foundation -> NSRunLoop oc封装的

    一套是 Core Foundtion ->CFRunLoopRef  C语言调用

    但是 这两套API 是等价的,NSRunLoop就是基于CFRunLoopRef的一层OC包装。所以要研究NSRunLoop还是要研究CFRunLoopRef 。

    RunLoop和线程的关系

    1. 每条线程都有唯一的一个与之对应的RunLoop对象

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

    3. RunLoop在第一次获取时创建,在线程结束的时销毁。

    RunLoop中的相关类

    1.CFRunLoopRef

    2.CFRunLoopModeRef RunLoop模式

    3.CFRunLoopSourceRef 事件源 输入源

    4.CFRunLoopTimerRef

    5.CFRunLoopObserverRef

    CFRunLoopModeRef 代表RunLoop的运行模式 系统提供了5中运行模式:

    default模式:几乎包括所有输入源(除NSConnection) NSDefaultRunLoopMode模式

    connection模式:处理NSConnection事件,属于系统内部,用户基本不用

    UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。

    UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。

    NSRunLoopCommonModes: 包含了多种模式:default, modal, 和tracking modes。

    一个RunLoop包含若干个Mode 每个Model又包含若干个Source/Timer/Observer

    每次RunLoop启动时,只能指定其中一个Mode,这个mode被称作currentMode

    如果需要切换Mode 只能tuichuLoop,再重新指定一个Mode进入。

    CFRunLoopTimerRef

    CFRunLoopTimerRef 就是基于时间的触发器

    基本上说的就是NSTimer,他会收到RunLoop的mode的影响

    GCD的定时器不会受到RunLoop的Mode的影响。

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        //这种方式启动的定时器 会自动加入到系统创建的RunLoop中
        //[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];
        /*
         如果是NSDefaultRunLoopMode 这样虽然定时器会工作,但是当拖动UITextView 的时候定时器就不在工作了,因为RunLoop有四种模式,它会在这四种模式中来回切换,当UITextView拖动时,RunLoop会进入UITrackingRunLoopMode模式,这时,就不再执行其他模式中的timer事件。当不再拖动时,会再次进入NSDefaultRunLoopMode模式,进行定时器事件。
         如果换成 UITrackingRunLoopMode 模式,只有在UI拖动时,才会执行定时器事件。
         
         那么如果你需要在UI拖动时不影响定时器事件的执行,我们可以使用NSRunLoopCommonModes 这其实不是一种模式,而是一种模式集合,包括UITrackingRunLoopMode 和 NSDefaultRunLoopMode。
         
         NSTimer 计时不准确就是因为RunLoop在各种模式中自动切换进行的原因。GCD的计时是比较准确的
         */
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
        
    }
    - (void)show {
        
        NSLog(@"show -----");
    }

    GCD Timer

    - (void)GCDTimer {
        //第一步 创建队列
        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
        //第二步 创建一个GCD定时器
        /*
         第一个参数 表明创建的是一个定时器
         第四个参数 表示事件运行在哪个线程中
         */
        dispatch_source_t sourceTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0,0, queue);
        self.timer = sourceTimer;
        //设置时间间隔
        /*
         第一个参数 定时器
         第二个参事 定时器 开始时间
         第三个参数 从现在开始间隔时间
         第四个参数 精准度 GCD 的单位是纳秒
         */
        dispatch_source_set_timer(sourceTimer, DISPATCH_TIME_NOW, 2.0 *NSEC_PER_SEC, 0*NSEC_PER_SEC);
        //设置事件
        dispatch_source_set_event_handler(sourceTimer, ^{
           //要执行的任务
            NSLog(@"GCDTimer");
        });
        //启动定时器
        dispatch_resume(sourceTimer);
    }

    CFRunLoopSourceRef (事件源 输入源)

    分类

    Source0: 不是基于端口的 用户主动触发的事件。

    Source1: 基于端口的 通过内核和其他线程相互发送消息

    RunLoop 的消息类型

    根据上图我们可以将消息分为二种类型,第一种类型又可以细分为三种,此三种共同点就是它们都是异步执行的

    port ->source1

    监听程序的Mach ports,Mach ports是一个比较底层的东西,可以简单的理解为:内核通过port这种方式将信息发送,而mach则监听内核发来的port信息,然后将其整理,打包发给runloop。

    Customer:->source0

    很明显,由开发人员自己发送。不仅仅是发送,过程的话相当复杂,苹果也提供了一个CFRunLoopSource来帮助处理。由于很少用到,可以简单说下核心,但是对帮助我们理解runloop却很有帮助:
    1.定义输入源(数据结构)
    2.将输入源添加到runloop,那么这样就有了接受者,即为R1
    3.协调输入源的客户端(单独线程),专门监听消息,然后将消息打包成runloop能够处理的样式,即第一步定义的输入源。它类似Mach的功能
    4.谁来发送消息的问题?上面的machport是由内核发送的。自定义的当然要我们自己发送了。。。首先必须是另一个线程来发送(当然如果只是测试的话可以和第三步在同一个线程),先发送消息给输入源,然后唤醒R1,因为R1一般处于休眠状态,然后R1根据输入源来做相应的处理

    Selector Sources

    NSObject类提供了很多方法供我们使用,这些方法是添加到runloop的,所以如果没有开启runloop的话,不会运行(不过有个坑,请看下面介绍)。

    /// 主线程
    performSelectorOnMainThread:withObject:waitUntilDone:
    performSelectorOnMainThread:withObject:waitUntilDone:modes:
    /// 指定线程
    performSelector:onThread:withObject:waitUntilDone:
    performSelector:onThread:withObject:waitUntilDone:modes:
    /// 针对当前线程
    performSelector:withObject:afterDelay:         
    performSelector:withObject:afterDelay:inModes:
    /// 取消,在当前线程,和上面两个方法对应
    cancelPreviousPerformRequestsWithTarget:
    cancelPreviousPerformRequestsWithTarget:selector:object:

    下面提供的方法是在指定的线程运行aSelector,一般情况下aSelector会添加到指定线程的runloop。但,如果调用线程和指定线程为同一线程,且wait参数设为YES,那么aSelector会直接在指定线程运行,不再添加到runloop。

    performSelectorOnMainThread:withObject:waitUntilDone:
    performSelectorOnMainThread:withObject:waitUntilDone:modes:
    
    performSelector:onThread:withObject:waitUntilDone:
    performSelector:onThread:withObject:waitUntilDone:modes:

    其实这也很好理解,假设这种情况也添加到指定线程的runloop,我们可以这样反向理解:1,当前线程runloop还没有开启,那么aSelector就不会被执行,然而你却一直在等待,造成线程卡死。2,当前线程runloop已经开启,那么调用performSelector这个方法的位置肯定是处于runloop的callout方法里面,在这里等待runloop再callout从而调用aSelector方法完成,显然也是死等待,线程卡死。。。

    还有一些performSelector方法,是不会添加到runloop的,而是直接执行,可以按照上面的特殊情况进行理解。方法列举如下:

    - (id)performSelector:(SEL)aSelector;
    - (id)performSelector:(SEL)aSelector withObject:(id)object;
    - (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;

    看到这里,是否感觉有些乱???只要记住没有延迟或者等待的都不会添加到runloop,有延迟或者等待的还有排除上面提到的特殊情况

    CFRunLoopObservers 观察者

    首先它并不属于事件源(不会影响runloop的生命周期),它比较特殊,用于观察runloop自身的一些状态的,有以下几种:

    1.进入RunLoop  kCFRunLoopEntry

    2.RunLoop即将执行定时器  kCFRunLoopBeforeTimers

    3.RunLoop即将执行输入源  kCFRunLoopBeforeSources

    4.RunLoop即将休眠  kCFRunLoopBeforeWaiting

    5.RunLoop被唤醒 在处理完唤醒它的事件之前  kCFRunLoopAfterWaiting

    6.退出  kCFRunLoopExit

    //给RunLoop添加一个监听者
    - (void)observer {
        //创建监听者
        /**
         param1: 给observer分配存储空间
         param2: 需要监听的状态类型:kCFRunLoopAllActivities监听所有状态
         param3: 是否每次都需要监听,如果NO则一次之后就被销毁,不再监听,类似定时器的是否重复
         param4: 监听的优先级,一般传0
         param5: 监听到的状态改变之后的回调
         return: 观察对象
         */
        CFRunLoopObserverRef observer =  CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
            switch (activity) {
                case kCFRunLoopEntry:
                    NSLog(@"RunLoop进入");
                    break;
                case kCFRunLoopBeforeTimers:
                    NSLog(@"即将处理timer");
                    break;
                case kCFRunLoopBeforeSources:
                    NSLog(@"即将处理input Sources");
                    break;
                case kCFRunLoopBeforeWaiting:
                    NSLog(@"即将睡眠");
                    break;
                case kCFRunLoopAfterWaiting:
                    NSLog(@"从睡眠中唤醒,处理完唤醒源之前");
                    break;
                case kCFRunLoopExit:
                    NSLog(@"退出");
                    break;
                default:
                    break;
            
            }
        });
        
        /*
         *第一个参数 RunLoop
         *第二个参数 监听者
         *第三个参数 要监听RunLoop在哪种运行模式下的状态
         */
        [NSTimer scheduledTimerWithTimeInterval:3 target:self selector:@selector(doFireTimer) userInfo:nil repeats:NO];
        CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
        
    }
    
    - (void)doFireTimer {
        NSLog(@"---fire---");
    }
  • 相关阅读:
    ASP.NET请求管道、应用程序生命周期、整体运行机制
    PHP面试总结
    ASP.NET MVC源码分析系列
    SQL中的重要语句
    Nicescroll滚动条插件的用法
    Nunit2.5.10快速上手(笔记)
    ucore 源码剖析
    《ucore lab8》实验报告
    《ucore lab7》实验报告
    《ucore lab6》实验报告
  • 原文地址:https://www.cnblogs.com/huanying2000/p/6583140.html
Copyright © 2020-2023  润新知