iOS 开发之RunLoop

概念

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

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

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

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

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

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

RunLoop和我们平常的开发息息相关 有很大的联系 我们使用的定时器 PerformSelector() GCD 事件响应 手势识别 界面刷新 网络请求 和AutoreleasePool 这些东西的底层 都和它相关。

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---");
}

 

posted @ 2017-03-19 23:58  幻影-2000  阅读(183)  评论(0编辑  收藏  举报