什么是runloop?
1、运行循环,run loop 奔跑的循环
2、在程序运行过程中循环做一些事情
runloop的应用范畴?
1、定时器、PerformSelector
2、GCD Async Main Queue
3、事件响应、手势识别、界面刷新
4、网络请求
5、AutoReleaseTool
runloop的基本作用?
来看看这段代码~思考下:以下代码的输出是什么?
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"来了");
nil 就相当于UIApplication
int a = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
NSLog(@"come here");
return a;
}
}
想好了吗?揭晓答案了哦~答案是“来了”,你猜对了吗?why?因为UIApplicationMain 有个死循环(runloop循环),
如果以上直接返回0话(没有runloop的情况),程序就会自动闪退
所以runloop的作用是
- 保证去程序不退出(保持程序的持续运行)
- 负责处理App中的各种事件,触摸,时钟,网络事件
- 节省cpu 的资源,提高cpu性能:如果没有事件发生,会让程序进入休眠状态
Runloop 对象的获取
RunLoop对象 OS中有2套API来访问和使⽤用RunLoop
Foundation:NSRunLoop
Core Foundation:CFRunLoopRef
NSRunLoop和CFRunLoopRef都代表着RunLoop对象
NSRunLoop是基于CFRunLoopRef的⼀一层OC包装 CFRunLoopRef是开源的
https://opensource.apple.com/tarballs/CF/
Foundation
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
Core Foundation
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
Runloop 的相关类
Core Foundation中关于RunLoop的5个类(CFRunLoopRef 是开源的)
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
Source0
触摸事件处理理
performSelector:onThread:
Source1
基于Port的线程间通信
系统事件捕捉
Timers
NSTimer performSelector:withObject:afterDelay:
Observers
⽤用于监听RunLoop的状态
UI刷新(BeforeWaiting)
Autorelease pool(BeforeWaiting)
typedef struct __CFRunLoop * CFRunLoopRef;
struct __CFRunLoop {
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutabluSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
};
CFRunLoopModeRef 代表Rrunloop的运行模式,一个Runloo包含如干个Mode,每个Mode又包含若干个source0、source1、timer、observer,但是runloop启动只能选择其中一个mode,作为currentmode,runloop启动时只能选择一个mode 作为currentmode。当没有任何Source0/Source1/Timer/Observer,RunLoop会⽴立⻢马退出
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoop {
CFStringRef _name;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _times;
};
Runloop 在实际开发中的应用
1.控制线程生命周期(线程保活)
2.解决NSTimer在滑动时停止工作的问题
3.监控应用卡顿
4.性能优化
Runloop 的五种模式
1.UITrackingRunLoopMode UI模式
2.NSDefaultRunLoopMode 默认模式
3.NSRunLoopCommonModes 占位模式相当于 UI模式+ 默认模式
4.UIInitializationRunLoopMode:在刚启动App时进入的第一个Mode,启动完成后不再使用
5.GSEventReceiveRunLoopMode:接受系统事件的内部Mode,通常用不到
有这样一个场景 :界面上有个textview,当滚动textview,viewdidload 里的timer就不工作了
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeMethod) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
我们来解释下原因,请看下图
此时的timer在默认模式下,苹果的一贯注重用户体验,所以当滚动textview (用户操作UI)的时候,runloop 会优先处理UI模式下的source。如果这时吧计时器加入到UI模式下,timeMethod 会正常进行,但当用户停止操作UI,那么UI模式下的runloop 就会进行休眠,所以timeMethod也会停止运行。此时我们可以吧timer加入到UI模式+默认模式。这时苹果为了优化,产生了一个NSRunLoopCommonModes 占位模式,但是它并不属于Runloop的五大模式之一。
Runloop 与线程的关系
1. 每条线程都有唯一的与之对应的runloop对象
2.Runloop保存在一个全局的Dictionary里,线程作为key,Runloop作为value
3.线程刚创建的时候是没有runloop对象的,Runloop会在第一次获取他时创建,在main.m 获取当前runloop,要是有的话直接获取,没有的话会自动创建一个runloop的对象
4、runloop会在线程结束时销毁
5、主线程的run loop默认是启动的。子线程默认是没有runloop的
iOS的应用程序里面,程序启动后会有一个如下的main()函数
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
重点是UIApplicationMain()函数,这个方法会为main thread设置一个NSRunLoop对象,这就解释了:为什么我们的应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。
-
对其它线程来说,run loop默认是没有启动的,如果你需要更多的线程交互则可以手动配置和启动,如果线程只是去执行一个长时间的已确定的任务则不需要。
-
在任何一个 Cocoa 程序的线程中,都可以通过以下代码来获取到当前线程的 run loop 。
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
RunLoop && 多线程
- (void)viewDidLoad {
[super viewDidLoad];
NSThread *thread = [[NSThread alloc]initWithBlock:^{
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeMethod) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
NSLog(@"来了1");
}];
[thread start];
}
- (void)timeMethod {
NSLog(@"来了2");
[NSThread sleepForTimeInterval:1];
NSLog(@"%@",[NSThread currentThread]);
}
2018-05-16 10:05:46.915347+0800 测试完[841:26856] 来了1
原因:线程走了,想要保证线程的命,就要让线程有执行不完的任务,线程就不会释放了~
- (void)viewDidLoad {
[super viewDidLoad];
NSThread *thread = [[NSThread alloc]initWithBlock:^{
NSTime*timer= [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeMethod) userInfo:nilrepeats:YES];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
//这个循环只能保证线程的命,但是timeMethod 还是不能调用
// while(true)
//{
//从事件队列中取出事件来处理!!(但是平没有开源这个,但是封装了一个NSRunLoop 来处理这个事件)
// }
//RunLoop -- 一条线程上面的RunLoop模式是不循环的
//CFRunLoop -- currentRunLoop () 第一次获取RunLoop的时候,创建RunLoop
[[NSRunLoop currentRunLoop]run];//死循环
NSLog(@"来了1");
}];
[thread start];
}
- (void)timeMethod {
NSLog(@"来了2");
[NSThread sleepForTimeInterval:1];
NSLog(@"%@",[NSThread currentThread]);
}
打印结果如下:
2018-05-11 10:06:44.594863+0800 RunloopTest[1300:42091] 来了2
2018-05-11 10:06:45.599998+0800 RunloopTest[1300:42091] <NSThread: 0x60400047df80>{number = 4, name = (null)}
主线程与子线程做的事情都是一样的,唯一的区别是:UI界面在主线上面的;App启动的第一条线程
苹果为啥要吧UI界面放在主线程?为了安全+效率
三,CFRunloop 的一个用法
#import "ViewController.h"
typedef void(^RunloopBlock)(void);
@interface ViewController ()
@property(strong,nonatomic)NSMutableArray *tasks;
@end
@implementation ViewController
//这里在cell 上加载图片的耗时操作没写
/*分析卡顿的原因:
所有的Cell的加载都在主线程的一次Runloop循环中,UI渲染也属于Runloop的事情,但是一次渲染18张图片,渲染太多。导致卡顿
解决思路:一次Runloop循环,只加载一张图片
步骤:
1.观察(observer)Runloop的循环
2.一次Runloop循环,加载一张图片
|-Cell加载图片的方法放到数组里
|-Runloop循环 一次,就从数组取出一个图片加载
*/
- (void)viewDidLoad {
[self addRunloopObserver];
self.tasks = NSMutableArray.array;
[self addTask:^{
}];
}
- (void)addTask:(RunloopBlock)task{
//保存任务到数组
[self.tasks addObject:task];
if (self.tasks.count == 0) {
return;
}
if (self.tasks.count>18) {
[self.tasks removeObjectAtIndex:0];
}
}
- (void)addRunloopObserver{
//获取当前的runloop
CFRunLoopRef runloop = CFRunLoopGetCurrent();
//定义上下文
/*
typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
} CFRunLoopObserverContext;
*/
CFRunLoopObserverContext context = {
0,
(__bridge void *)self,
&CFRetain,
&CFRelease,
NULL
};
//创建观察者
//c 中create new copy 堆区开辟内存空间。需要释放
CFRunLoopObserverRef runLoopObserver = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, 0, &callback, &context);
CFRunLoopAddObserver(runloop, runLoopObserver, kCFRunLoopCommonModes);
//释放
CFRelease(runloop);
}
void callback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
ViewController *vc = (__bridge ViewController *)info;
if (vc.tasks.count == 0) {
return;
}
RunloopBlock task = vc.tasks.firstObject;
task();
[vc.tasks removeObjectAtIndex:0];
};