转载: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.Foundation
NSRunLoop2.Core Foundation
CFRunLoopRef
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在第一次获取时创建,在线程结束时销毁
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、CFRunLoopObserverRefCFRunLoopObserverRef是观察者,能够监听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的使用场合:
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(除Observer以外)主动给他发消息,然后Observer被动的接收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的状态,比如监听点击事件的处理(在所有点击事件之前做一些事情)