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的结构
如图所示:
一个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的状态改变
可以监听的时间点有以下几个
- 添加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的使用场合:
- 使用port或是自定义的input source来和其他线程进行通信
- 在线程(非主线程)中使用timer
- 使用 performSelector…系列(如performSelectorOnThread, …)
- 使用线程执行周期性工作
-
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的状态,比如监听点击事件的处理(在所有点击事件之前做一些事情)
-