iOS-RunLoop,为手机省电,节省CPU资源,程序离不开的机制

RunLoop是什么?基本操作是什么?

1、RunLoop的作用

RunLoop可以:

  • 保持程序的持续运行

  • 处理App中的各种事件(比如触摸事件、定时器事件、Selector事件)

  • 节省CPU资源,提高程序性能:该做事时做事,该休息时休息

学到这里,你就知道了RUnLoop的作用了吧。看看程序里的例子:

程序中的main函数里面:

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

在 UIApplicationMain 里面就开启了一个RunLoop,这个默认启动的RunLoop是跟主线程相关联的。它就可以处理我们上面说的那些事情,说白了就是让CUP有时间休息,没事的时候帮我们省电。

下面我们看看怎么访问它:

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

1.FoundationNSRunLoop

2.Core FoundationCFRunLoopRef

2.1、两者的关系:

NSRunLoop和CFRunLoopRef都代表着RunLoop对象

NSRunLoop是基于CFRunLoopRef的一层OC包装,所以要了解RunLoop内部结构,需要多研究CFRunLoopRef层面的API(Core Foundation层面)

2.2、如何获得RunLoop对象

Foundation

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

Core Foundation

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

3、RunLoop和线程的关系

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

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

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

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这有个iOS交流群:642363427,不管你是小白还是大牛欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术!

4、RunLoop的结构

如图所示:

 
image

一个RunLoop包含若干个Mode,

而每个Mode又包含若干个Source、Timer、Observer

对应的类是:

CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

每个RunLoop启动时,只能指定一种Model,并且切换Mode时,只能先退出RunLoop,这样是为了分隔开不同组的Source、Timer、Observer。

RunLoop有5种Mode:

系统默认注册了5个Mode:

NSDefaultRunLoopMode:App的默认Mode,通常主线程是在这个Mode下运行,可以把这个理解为一个”过滤器“,我们可以只对自己关心的事件进行监视。

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

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

GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到

NSRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode

5、RunLoop的内部类

每个Mode又包含若干个Source、Timer、Observer,他们对应的类如下:

5.1、CFRunLoopTimerRef

  • CFRunLoopTimerRef是基于时间的触发器

  • CFRunLoopTimerRef基本上说的就是NSTimer,它受RunLoop的Mode影响

  • GCD的定时器不受RunLoop的Mode影响

5.2、CFRunLoopSourceRef

  • CFRunLoopSourceRef是事件源(输入源)

  • 按照官方文档,Source的分类

    • Port-Based Sources
    • Custom Input Sources
    • Cocoa Perform Selector Sources
  • 按照函数调用栈,Source的分类

    • Source0:非基于Port的, 用于用户主动触发事件
    • Source1:基于Port的,通过内核和其他线程相互发送消息

5.3、CFRunLoopObserverRef

CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变

可以监听的时间点有以下几个

 
image
  • 添加Observer
// 创建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    NSLog(@"----监听到RunLoop状态发生改变---%zd", activity);
});
// 添加观察者:监听RunLoop的状态
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 释放Observer
CFRelease(observer);

RunLoop的使用

下来是Run Loop的使用场合:

  1. 使用port或是自定义的input source来和其他线程进行通信
  2. 在线程(非主线程)中使用timer
  3. 使用 performSelector…系列(如performSelectorOnThread, …)
  4. 使用线程执行周期性工作
  • run loop不需要创建,在线程中只需要调用[NSRunLoop currentRunLoop]就可以得到

  • 假设我们想要等待某个异步方法的回调。比如connection。如果我们的线程中没有启动run loop,是不会有效果的(因为线程已经运行完毕,正常退出了)。

  • 你不需要在任何情况下都去启动一个线程的 run loop。比 如,你使用线程来处理一个预先定义的长时间运行的任务时,你应该避免启动 run loop。Run loop 在你要和线程有更多的交互时才需要,比如以下情况:

 使用端口或自定义输入源来和其他线程通信

 使用线程的定时器

 Cocoa 中使用任何 performSelector...的方法

 使线程周期性工作

如果你决定在程序中使用 run loop,那么它的配置和启动都很简单。和所有线程 编程一样,你需要计划好在辅助线程退出线程的情形。让线程自然退出往往比强制关闭它更好。关于更多介绍如何配置和退出一个 run loop,参阅”使用 Run Loop 对象” 的介绍。

终于学好了关于RunLoop的基本概念,

我们知道了,RunLoop接收到两种事件就会去调用相应的方法处理事件,两种事件分别是输入源(input source)和定时源 (timer source),换句话说,RunLoop就是所有要监视的输入源和定时源以及要通知的 run loop 注册观察 者的集合。

所以,我们要知道

Run loop 入口

Run loop 何时处理一个定时器

Run loop 何时处理一个输入源

Run loop 何时进入睡眠状态

Run loop 何时被唤醒,但在唤醒之前要处理的事件

Run loop 终止

例子

给子线程添加RunLoop

NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(show) object:nil];
    [thread start];

- (void)show
{
    [NSRunLoop currentRunLoop]; // 只要调用currentRunLoop方法, 系统就会自动创建一个RunLoop, 添加到当前线程中
}

常驻线程

有这么一个需求,我们要在子线程中没接收一个事件就调用一次方法。但是子线程在完成任务后就销毁,全局变量强引用?试试

//
//  ViewController.m
//  NSThreadTest
//
//  Created by 薛银亮 on 14/8/10.
//  Copyright (c) 2014年 薛银亮. All rights reserved.
//
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, strong)NSThread *thread;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(run) object:@"xyl"];
    self.thread = thread;
    [thread start];
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self performSelector:@selector(test) onThread:self.thread withObject:@"xyl" waitUntilDone:YES];
}
-(void)run{
    NSLog(@"runrunrunrun");
}
-(void)test{
    NSLog(@"testtesttest");
}
@end

结果令人感到遗憾:线程只能执行一个函数run,然后就死亡了。就算用全局的变量引用着,这个线程也只是存在于内存中,同样是死亡状态,不能持续的执行。

  • 想在子线程中不断执行任务,必须保证子线不处于死亡状态

  • 但是子线程执行完一次任务就进入死亡状态

  • 那我们可以把线程停留在进入死亡状态之前,这里可以用RunLoop

    • 我们可以在线程初始化的时候执行的方法中给他创建一个运行时RunLoop,这是他就可以不断接收source,也就是这样
-(void)run{
    NSLog(@"runrunrunrun");
    [[NSRunLoop currentRunLoop]addPort:[[NSPort alloc]init] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop]run];
}
  • 注意RunLoop:

启动前内部必须要有至少一个item,虽然Obsever也是item的一种,但是只会等待Timer和Source ,Timer是因为有回调,Source是会接收事件,所以当RunLoop里面有Timer或者Source的时候,RunLoop会等待里面的item(除Obsever以外)主动给他发消息,然后Oberver被动的接收RunLoop发送过来的消息,亦即是说,能主动给RunLoop发消息的item会让RunLoop跑起来并且不退出。

    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

 //1.将NSTimer添加在Default模式, 定时器只会运行在Default Mode下, 当拖拽时Mode切换为Tracking模式所以没反应
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

// 2.将NSTimer添加在Tracking模式, , 定时器只会运行在Tracking Mode下,当停止时Mode切换为Default模式所以没反应
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

// 3.将NSTimer添加为被标记为Common的模式, Default和Tracking都被标记为了Common, 所以都有反应
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

// 4.scheduled创建的定时器默认添加在Default模式, 所以不用手动添加, 但是后期也可以修改
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 修改模式
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

注意:GCD的定时器不受RunLoop的影响,因为RunLoop底层是使用GCD实现timer的

  • GCD定时器

    有这么一个需求,需要这么一个定时器,误差几乎为0的定时器,但是无论是NSTimer还是CGDisplayLink都会有误差,而且误差都比较大,这是我们可以用GCD来实现定时器,实际上,上面已经说了,RunLoop底层也是调用GCD的source来实现NSTimer的,只是NSTimer还受mode的影响,下面来看看怎么用GCD实现

//  获取队列
    dispatch_queue_t queue = dispatch_get_main_queue();
//  创建定时器
    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//  设置定时器属性(什么时候开始,间隔多大)
//  定义开始时间
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
//  定义时间间隔
    uint64_t interver = (uint64_t)(1.0 * NSEC_PER_SEC);
//  设置开始时间和时间间隔
    dispatch_source_set_timer(self.timer, start,interver, 0);
//  设置回调
    dispatch_source_set_event_handler(self.timer, ^{
        NSLog(@"==================") ;
    });
//      dispatch_cancel(self.timer);
//      self.timer = nil;
//  取消定时器
//  启动定时器
    dispatch_resume(self.timer);

线程除了处理输入源,Run Loops也会生成关于Run Loop行为的通知(notification)。Run Loop观察者(Run-Loop Observers)可以收到这些通知,并在线程上面使用他们来作额为的处理,我们可以像下面这样添加一个观察者给RunLoop

添加RunLoop监听

// 创建Observer
// 第一个参数:用于分配该observer对象的内存
// 第二个参数:用以设置该observer所要关注的的事件
// 第三个参数:用于标识该observer是在第一次进入run loop时执行, 还是每次进入run loop处理时均执行
// 第四个参数:用于设置该observer的优先级
// 第五个参数: observer监听到事件时的回调block
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    switch(activity)
    {
        case kCFRunLoopEntry:
            NSLog(@"即将进入loop");
            break;
        case kCFRunLoopBeforeTimers:
            NSLog(@"即将处理timers");
            break;
        case kCFRunLoopBeforeSources:
            NSLog(@"即将处理sources");
            break;
        case kCFRunLoopBeforeWaiting:
            NSLog(@"即将进入休眠");
            break;
        case kCFRunLoopAfterWaiting:
            NSLog(@"刚从休眠中唤醒");
            break;
        case kCFRunLoopExit:
            NSLog(@"即将退出loop");
            break;
        default:
            break;
    }
});

将上面的监听添加到观察者


    /*
     第一个参数: 给哪个RunLoop添加监听
     第二个参数: 需要添加的Observer对象
     第三个参数: 在哪种模式下监听
     */
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopDefaultMode);

    // 释放observer
    CFRelease(observer);

RunLoop面试题

  • 什么是RunLoop?

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

    • 一个线程对应一个RunLoop,主线程的RunLoop默认已经启动,子线程的RunLoop得手动启动(调用run方法)

    • RunLoop只能选择一个Mode启动,如果当前Mode中没有任何Source(Sources0、Sources1)、Timer,那么就直接退出RunLoop

    • 自动释放池什么时候释放?

    • 通过Observer监听RunLoop的状态

  • 在开发中如何使用RunLoop?什么应用场景?

    • 开启一个常驻线程(让一个子线程不进入消亡状态,等待其他线程发来消息,处理其他事件)

    • 在子线程中开启一个定时器

    • 在子线程中进行一些长期监控

    • 可以控制定时器在特定模式下执行

    • 可以让某些事件(行为、任务)在特定模式下执行

    • 可以添加Observer监听RunLoop的状态,比如监听点击事件的处理(在所有点击事件之前做一些事情)

posted @ 2020-10-23 13:49  iOS__峰公众号iOSVNL  阅读(274)  评论(0编辑  收藏  举报