RunLoop的基础知识

  RunLoop是多线程开发中非常重要的一个知识点,比较抽象。一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。但是,在实际开发过程中,我们可能需要这样一个机制,让线程能随时处理事件但并不会退出。也就是说,在线程没有处理消息时,可以进入休眠状态,以避免资源占用。而在有消息到来时,可以立刻被唤醒。这种机制就是RunLoop。

一、RunLoop的基本概念

  
  1、RunLoop的作用

  RunLoop从字面意思上理解,就是指运行循环,意思就是持续运行。程序运行以后之所以不会马上退出,就是因为它内部开启了一个RunLoop来保证程序的持续运行。RunLoop的基本作用如下:

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

  我们在学习C语言的时候就已经知道,程序的入口函数是main(),在iOS开发中,程序的入口函数同样是main()函数:

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

  在上面的代码中,具体来说就是return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));这行代码,它内部开启了一个RunLoop(其实就是一个死循环)来保证程序一直处于运行状态。按住command键,点击UIApplicationMain()函数进入相关的头文件,可以看到它的返回类型是一个int类型:


Snip20170214_7.png

  为此,我们可以对上面的程序进行相应的改写,在UIApplicationMain()函数前后加入打印信息,以此来验证程序开启了RunLoop从而不会退出:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSLog(@"执行main函数");
        int num = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        NSLog(@"num = %d", num);
        return num;
    }
}

  运行程序,注意看一下控制台的打印消息:


验证UIApplicationMain()函数中开启了运行循环.gif

  从上面的GIF图中可以看到,程序运行以后确实执行了main()函数。但是,它只打印了UIApplicationMain()函数前面的"执行main函数"语句,并没有打印它后面num变量的值。这个num的值永远都不会打印,如果它被打印了,就说明num的值被返回了,也就是说这个main()函数执行完了,程序就退出了。

  UIApplicationMain()函数之所以一直没有返回,是因为它内部开启了一个RunLoop,而这个RunLoop是跟主线程先关的,它能保证主线程不会挂掉。在iOS开发中有两套API是可以用来访问RunLoop对象的:

1、Foundation框架(NSRunLoop),基于OC语言;
2、Core Foundation框架(CFRunLoopRef),基于C语言。

  其中,NSRunLoop是基于CFRunLoopRef做了一层OC包装。因此,要学好RunLoop相关的知识,应该多研究CFRunLoopRef的API。

  2、RunLoop和线程之间的关系

1、RunLoop和线程是一一对应的,即一条线程对应一个RunLoop对象;
2、主线程的RunLoop由系统创建并默认已启动,子线程的RunLoop需要手动创建;
3、RunLoop在第一次获取时创建,在线程结束时被销毁

  3、如何获得RunLoop对象

[NSRunLoop currentRunLoop]; // 在Foundation框架中获取当前线程的RunLoop
[NSRunLoop mainRunLoop];  // 在Foundation框架中获取主线程的RunLoop对象
CGRunLoopGetCurrent(); // 在Core Foundation框架中获取当前线程的RunLoop
CFRunLoopGetMain(); // 在Core Foundation框架中获取主线程的RunLoop

  理论上讲,在同一个项目中,使用[NSRunLoop currentRunLoop];和CGRunLoopGetCurrent();获取到当前线程的RunLoop,它们的内存地址应该是一样的。但是,因为它们属于不同的框架,获得RunLoop的内存地址还是不同的。不过,可以通过getCFRunLoop将它们转换成一样的(主线程RunLoop的转换也是一样的):

[NSRunLoop currentRunLoop].getCFRunLoop;  // 将Foundation框架下的RunLoop转换为Core Foundation框架下的RunLoop

  在前面我们说过,主线程的RunLoop有系统创建并默认已启动,而子线程的RunLoop需要手动创建。那么,子线程的RunLoop该如何创建呢?下面就简单的举一个实例:

- (void)viewDidLoad {
    [super viewDidLoad];

    // 创建子线程
    [[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start];
}

- (void)test {

    // 获取当前子线程的RunLoop
    NSLog(@"子线程的RunLoop:%p", [NSRunLoop currentRunLoop]);  
}

  像上面那样,先开一条子线程,然后在selector监听的方法中获取当前线程的RunLoop即可。

二、RunLoop中几个重要类的简单介绍和使用

  
  1、和RunLoop相关的类

  在学习RunLoop的过程中,我们主要是基于Core Foundation框架的。在Core Foundation框架下,和RunLoop相关的类主要是下面这5个:

CFRunLoopRef —— RunLoop本身
CFRunLoopModeRef —— 运行模式
CFRunLoopTimerRef —— 定时器事件
CFRunLoopSourceRef —— 输入源(或者事件源)
CFRunLoopObserverRef —— 监听者

  上面这5个类中,最重要的是CFRunLoopModeRef。一个RunLoop包含若干个Mode,而每个Mode中又包含若干个Timer、Source和Observer。每次RunLoop启动时,只能指定其中一个Mode,而这个Mode就被称为currentMode。如果要切换当前Mode,只能退出当前循环,再重新指定一个Mode进入。这样可以有效分隔不同组的Timer/Source/Observer,让它们之间互不干扰。另外,需要注意的是,运行模式中至少要有一个Timer或者一个Source(Observer在Mode中无实际意义,只是用来监听RunLoop的状态)。下面这张图是苹果官方给出的RunLoop结构示意图:


Structure of a run loop and its sources

  关于上面这张图,在Xcode自带的Documentation and API Reference中可以搜到。为了便于理解和记忆,上面那张图可以精简为下面这张图:


RunLoop中的运行模式.png

  关于运行模式,系统默认已经注册了5种,其中只有前面3种是和我们有一定关系的,后面两种运行模式在实际开发过程中很少用到:

kCFRunLoopDefaultMode:应用默认的Mode,通常主线程是在这个Mode下运行;
UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他 Mode 影响;
kCFRunLoopCommonModes: 占位Mode,它不是一种真正的Mode;
UIInitializationRunLoopMode: 在刚启动应用时进入的第一个 Mode,启动完成后就不再使用,和我们关系不大;
GSEventReceiveRunLoopMode: 接受系统事件的内部Mode,通常用不到。

  2、RunLoop运行模式的简单使用举例

  在上面的内容中,我们已经介绍过,RunLoop中有多个运行模式,但是在程序启动以后,只能选择其中的一种。并且,在该运行模式中,至少需要一个Timer或者一个Source:

- (void)viewDidLoad {
    [super viewDidLoad];
    // NSTimer
    [self timer];
}

// MARK:- 定时器
- (void)timer {
    NSLog(@"start---");
    // 创建一个定时器
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:NO];
    /**
     * timerWithTimeInterval:<#(NSTimeInterval)#> target:<#(nonnull id)#> selector:<#(nonnull SEL)#> userInfo:<#(nullable id)#> repeats:<#(BOOL)#>
     * 第一个参数 : 表示要在多少秒之后执行后面方法选择器中的方法;
     * 第二个参数 : 表示目标对象;
     * 第三个参数 : 表示方法选择器;
     * 第四个参数 : 表示用户信息,可以传nil;
     * 第五个参数 : 表示是否需要重复执行方法选择器中的方法。
     */

    // 把定时器添加到RunLoop中,并选择运行模式
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    /**
     * addTimer:<#(nonnull NSTimer *)#> forMode:<#(nonnull NSRunLoopMode)#>
     * 第一个参数 : 表示定时器;
     * 第二个参数 : 表示运行模式。
     */
}

- (void)test {
    // 打印当前线程和运行模式
    NSLog(@"当前线程:%@---运行模式%@", [NSThread currentThread], [NSRunLoop currentRunLoop].currentMode);
    /**
     * 5中基本的运行模式:
     * NSDefaultRunLoopMode/kCFRunLoopDefaultMode---默认的Mode,通常主线程在这个Mode中运行;
     * NSRunLoopCommonModes/kCFRunLoopCommonModes---占位Mode,不是真正的Mode;相当于既添加了默认Mode,又添加了界面追踪Mode;
     * UITrackingRunLoopMode---界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其它Mode影响;
     * UIInitializationRunLoopMode---在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用;
     * GSEventReceiveRunLoopMode---接受系统事件的内部 Mode,通常用不到。
     */
}

  除了可以通过+ timerWithTimeInterval: target: selector: userInfo: repeats:这个方法创建定时器之外,还可以使用+ scheduledTimerWithTimeInterval: target: selector: userInfo: repeats:方法来创建定时器。因此,同样可以使用下面这种方式来选择运行模式:

- (void)viewDidLoad {
    [super viewDidLoad];
    // NSTimer
    [self timer];
}

// MARK:- 定时器
- (void)timer {
    NSLog(@"start---");
    // 创建定时器另外一种方式
    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:NO];
}

- (void)test {
    // 打印当前线程和运行模式
    NSLog(@"当前线程:%@---运行模式%@", [NSThread currentThread], [NSRunLoop currentRunLoop].currentMode);
}

  使用+ scheduledTimerWithTimeInterval: target: selector: userInfo: repeats:方法来创建定时器时,不需要手动将它添加到RunLoop中,因为系统内部已经自动添加了,并且设置运行模式为默认模式。不过,需要注意的是,如果是在子线程用这种方式创建定时器,它是不工作的:

- (void)viewDidLoad {
    [super viewDidLoad];

    // 创建子线程
    [NSThread detachNewThreadSelector:@selector(timer) toTarget:self withObject:nil];
}

// MARK:- 定时器
- (void)timer {
    NSLog(@"start---");
    // 创建定时器另外一种方式
    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:NO];
}

- (void)test {
    // 打印当前线程和运行模式
    NSLog(@"当前线程:%@---运行模式%@", [NSThread currentThread], [NSRunLoop currentRunLoop].currentMode);
}

  为什么不工作呢?这主要是和线程有关。我们说过,主线程的RunLoop默认已创建,但是子线程的RunLoop需要手动创建。而自动将定时器添加到RunLoop中,并设置运行模式为默认模式的前提条件是,RunLoop必须存在!但是,在子线程中,这个RunLoop并不存在。因此,它也就不工作了。要想让上面那个定时器工作,必须手动创建与子线程对应的RunLoop,然后再手动开启它:

- (void)viewDidLoad {
    [super viewDidLoad];

    // 创建子线程
    [NSThread detachNewThreadSelector:@selector(timer) toTarget:self withObject:nil];
}

// MARK:- 定时器
- (void)timer {
    NSLog(@"start---");
    // 创建当前子线程对应的RunLoop
    NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
    // 创建定时器
    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:NO];
    // 开启子线程的RunLoop
    [currentRunLoop run];
    /**
     * 用下面这两个方法开启子线程的RunLoop也是可以的:
     * runUntilDate:<#(nonnull NSDate *)#> : 后面的参数表示设定一个RunLoop退出的时间;
     * runMode:<#(nonnull NSRunLoopMode)#> beforeDate:<#(nonnull NSDate *)#> : 第一个参数表示运行模式,第二个参数表示退出时间
     */
}

- (void)test {
    // 打印当前线程和运行模式
    NSLog(@"当前线程:%@---运行模式%@", [NSThread currentThread], [NSRunLoop currentRunLoop].currentMode);
}

  NSTimer的运行效果会受运行模式的影响,它有时候会不工作。也就是说,它有可能不精准。我们都知道,GCD定时器是绝对精准的,不会受RunLoop运行模式的影响。而且GCD定时器使用起来更加简单,Xcode默认为我们提供了一个和定时器相关的代码段,只需要敲dispatch_source_t,它就自动出来了:

- (void)viewDidLoad {
    [super viewDidLoad];

    // GCD定时器
    [self gcdTimer];
}

// MARK:- GCD中的定时器
- (void)gcdTimer {

    NSLog(@"start---");
    // 创建GCD中的定时器
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
    /**
     * dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, <#dispatchQueue#>);
     * 第一个参数 : 表示Source的类型(DISPATCH_SOURCE_TYPE_TIMER表示它是一个定时器);
     * 第二个参数 : 表示一些描述信息,比如说线程的id;
     * 第三个参数 : 表示更加详细的描述信息;
     * 第四个参数 : 表示一个队列,它决定了GCD定时器中的任务在哪个线程中执行
     */

    // 设置GCD中定时器的起止时间
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    /**
     * dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, <#intervalInSeconds#> * NSEC_PER_SEC, <#leewayInSeconds#> * NSEC_PER_SEC);
     * 第一个参数 : 表示定时器对象;
     * 第二个参数 : 表示定时器的起始时间(DISPATCH_TIME_NOW是指从现在开始);
     * 第三个参数 : 表示间隔时间(NSEC_PER_SEC的值是10的9次方,主要是用来将纳秒转换为秒。GCD中的时间单位是纳秒);
     * 第四个参数 : 表示精准度,也就是允许的误差是多少(传一个0表示绝对精准)。
     */

    // 通过一个block代码块来封装一些操作
    dispatch_source_set_event_handler(timer, ^{
        // 打印当前线程
        NSLog(@"当前线程为:%@", [NSThread currentThread]);
    });
    /**
     * dispatch_source_set_event_handler(timer, ^{
     <#code to be executed when timer fires#>
     });
     * block参数 : 主要是用来封装任务的。
     */

    // 启动定时器
    dispatch_resume(timer);

    // 给timer一个强引用
    self.timer = timer;
}

  需要特别注意的是,在上面的代码中,timer是一个局部变量,我们设定2.0秒之后再执行后面的block代码块,而block代码块非常的特殊,很有可能在我们执行它之前,这个timer就被释放掉了。因此,为了保证这个block代码块和timer能够成功执行,必须在类扩展中给timer一个强引用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值