本人小白,欢迎各位大佬补充指点
先说说我是咋理解runloop的吧?然后在专业的讲讲
本人理解:
- 从C语言的角度来看,整个app就是调用了一个main函数,只是这个main函数一直不返回值.
- main函数内部:
常识:执行任务需要开启一个线程,因为任务必须在线程下执行
一般的main函数执行流程:开启一个线程->添加许多任务(即写了很多代码)->任务完成(代码执行完毕)->线程关闭->返回main函数值程序结束 - runloop啥作用?
Runloop就是一个死循环,把这个死循环放在上面开启的线程中,那么线程就永远不会结束.因此main永远不返回.但是这个死循环比较牛逼,下面讲讲他的牛逼之处:
一般死循环特点:
1>程序一旦进入死循环,一直循环执行,永不停歇,并且执行速度非常快
2>只执行这段代码,其他任何代码都不会执行,也就是说,只专注在这跑圈,不管外界发生任何事
Runloop特点:
程序一旦进入runloop,当外界没事的时候,他就慢慢跑圈,(至于多慢我就不知道了),甚至休眠,一旦有事,他就快速跑到该事件,去执行该事件,执行完毕后再接着休眠
下面专业系统的讲一下:
RunLoop的传说
- 什么是runloop?
字面理解:运行循环/跑圈 - 基本作用
1>保持程序的持续运行
打开app它一直不死,就是因为runloop
2>处理App中的各种事件(比如触摸事件、定时器事件、Selector事件)
3>节省CPU资源,提高程序性能:该做事时做事,该休息时休息
能让我们的主线程,有事情就做事情,没事情就休息,比如点击事件:点击前主线程在休息,当点击事件处理完毕后,让主线程再次休息… - 如果没有runloop
int main(int argc, char * argv[]) {
NSLog(@"execute main function");
return 0;
}
第3行后程序就结束了
如果有了runloop大致运行代码逻辑如下:
由于main函数里面( UIApplicationMain())启动了个RunLoop,所以程序并不会马上退出,保持持续运行状态
main函数中的RunLoop
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
代码的UIApplicationMain函数内部就启动了一个RunLoop
所以UIApplicationMain函数一直没有返回,保持了程序的持续运行
这个默认启动的RunLoop是跟主线程相关联的
既然runloop那么牛逼,那么我们就来认识他一下:
RunLoop对象
iOS中有2套API来访问和使用RunLoop:
Foundation(OC):NSRunLoop
Core Foundation(C):CFRunLoopRef
该框架有个特点:所有的类都已CF开头,以Ref(reference:引用)结尾
NSRunLoop和CFRunLoopRef都代表着RunLoop对象
NSRunLoop是基于CFRunLoopRef的一层OC包装,所以要了解RunLoop内部结构,需要多研究CFRunLoopRef层面的API(Core Foundation层面)
RunLoop与线程关系
上面说到,runloop通常是用来卡住线程的,那么他们必然有关系啦
- 每条线程都有唯一的一个与之对应的RunLoop对象
主线程的RunLoop已经自动创建好了,并且主动启动了,子线程的RunLoop需要主动创建,然后手动启动 - RunLoop在第一次获取时创建,在线程结束时销毁
获得RunLoop对象
- Foundation
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象 - Core Foundation
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
上面讲了3条理论,下面代码证明一下:
- (void)viewDidLoad {
[super viewDidLoad];
//当前线程就是主线程
NSLog(@"%p--%p",[NSRunLoop mainRunLoop],[NSRunLoop currentRunLoop]);
//创建一个线程
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[thread start];
}
-(void)run{
/*
[NSRunLoop currentRunLoop]:这就是在子线程中创建了一个runloop,不是通过alloc/init,而且该创建是懒加载的
通过分析runloop(C语言)源码过程如下:
创建runloop函数只有一个参数t:线程(这也说明一个runloop只有一个线程)
_CFRunLoopGet0(pthread_t t) {
1.如果t==kNilPthreadT(0的意思),那么t就等于主线程
__CFRunLoops:本身是一个可变字典,用于存储所有的runloop,key值为线程,value为runloop
__CFRunLoops :{
mainthread : mainloop,
thread1:loop1,
...
}
2.如果发现__CFRunLoops为空值
2.1先创一个可变字典(就是__CFRunLoops)
2.2根据主线程创建一个主runloop,即mainloop
2.3将mainloop存入可变字典,key值为主线程
3.根据传进来的线程到字典中拿到相应的runloop
4.没有创建一个newloop->将newloop存入可变字典->返回这个newloop
5.有就直接返回loop
}
*/
NSLog(@"%p",[NSRunLoop currentRunLoop]);
}
RunLoop的相关类
学到对象,那么当然就有学到一些该对象相关的类了
Core Foundation中关于RunLoop的5个类
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
这几个类的关系图如下:
分析:
RunLoop就像一个汽车,要跑起来,那么就要装一些必备的东西
RunLoop里面有很多Mode对象->每个Mode对象里面又有三种集合分别存放:Source/Observer/Timer对象->有了这些Mode以及子内容runloop才能一直跑圈/休息/有事件触发
注意:runloop中的Mode中必须有source或者timer才能一直运行,否则跑一圈就停了
下面分别讲一讲这几个类:
CFRunLoopModeRef
- CFRunLoopModeRef代表RunLoop的运行模式
- 一个 RunLoop 包含若干个 Mode,每个Mode又包含若干个Source/Timer/Observer
- 每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode
[NSRunLoop currentRunLoop] runMode: beforeDate:
- 如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入
这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响 -
系统默认注册了5个Mode:(前两种最重要)
kCFRunLoopDefaultMode: App的默认Mode,通常主线程是在这个Mode下运行
UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
分析:当我们滚动图片时就会切换到这个模式,跟踪你的滚动,一旦停止滚动就会切换到Default模式
举例:当我们滚动的时候,其他事件没反应,比如定时器,当滚动的时候定时器没反应,因为它在Default模式中
UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode
CFRunLoopTimerRef
- CFRunLoopTimerRef是基于时间的触发器,基本上说的就是NSTimer
- 它受runloop的Mode影响
- GCD的定时器不受runLoop的Mode影响
GCD定时器实现不一样
代码举例:
- (void)viewDidLoad {
[super viewDidLoad];
//向sb中拖入一个textView
//情况一:没有滚动textView时,1秒打印一次,一旦滚动textView,定时器无用了,停止滚动,定时器有起作用了
// [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
//上面一段代码做了很多事情,等价代码如下:
//timer添加到defaultMode里面去,mode再添加到runloop中去,runloop在启动时候再指定这个mode->拿出timer来用
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
//也就说只能在Defaultmode下好使
// [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//情况二:如果想要一滚动timer就有用,一停止滚动timer就失效,那么模式就用UITrackingRunLoopMode
// [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
//情况三:滚动不影响定时器
//定时器只会跑在标记为CommonModes的模式下(或者用GCD定时器)
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
//通过打印runloop,我们发现kCFRunLoopDefaultMode/UITrackingRunLoopMode这两种模式都被标记为了commonmodes标签了,因此就可以做到了
NSLog(@"%@",[NSRunLoop currentRunLoop]);
}
- (void)test {
NSLog(@"----");
}
CFRunLoopSourceRef
- 系统定的我们用不着
- CFRunLoopSourceRef是事件源(输入源)
- 我们的一些触发事件,都是由source触发的
1>以前的分法
Port-Based Sources
Custom Input Sources
Cocoa Perform Selector Sources(处理performselector调用)
2>根据函数调用栈分法
Source0:非基于Port
Source1:基于Port的,通过内核和其他线程通信,接收/分发系统事件
CFRunLoopObserverRef
- CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变
可以监听的时间点有以下几个:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),//即将进入loop,左移动0->2的0次放= 1
kCFRunLoopBeforeTimers = (1UL << 1),//即将处理Timer …2
kCFRunLoopBeforeSources = (1UL << 2),//即将处理source...4
kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠 …32
kCFRunLoopAfterWaiting = (1UL << 6),//刚刚从休眠中唤醒...64
kCFRunLoopExit = (1UL << 7),//即将推出loop 128
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
代码举例
// 创建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"----监听到RunLoop状态发生改变---%zd", activity);
});
//添加观察者到当前线程的RunLoop的当前Mode中,监听runloop的状态
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 释放Observer
CFRelease(observer);
/*
//需要自己管理内存
CF的内存管理(Core Foundation)
1.凡是带有Create、Copy、Retain等字眼的函数,创建出来的对象,都需要在最后做一次release
* 比如CFRunLoopObserverCreate
2.release函数:CFRelease(对象);
*/
RunLoop的逻辑流程图
RunLoop的应用
- performSelector的使用
//延迟3秒设置图片并且值只在NSDefaultRunLoopMode模式下,也就是说,在滚动的时候,不显示图片,滚动停止,显示图片
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"123"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]
- 常驻线程
#import "ViewController.h"
#import "ZHThread.h"
@interface ViewController ()
@property (nonatomic,strong) ZHThread *thread;
@end
@implementation ViewController
/*
常驻线程:让一个线程永远不死
通过继承NSThread并且从写delloc监听线程,可以看到,开启线程后执行完任务立马死掉
注意:不能简单搞个属性强引用,因为线程一旦执行完毕,即使不释放掉,当前的线程也处于消亡状态,从新开启([self.thread start])会崩溃
当然不通过start调用,通过performselector调用虽然不会崩溃,但是不会调用run2,因为线程处于消亡状态
//解决办法:线程中添加runloop
好处:可以随时让拿到这个线程让他做一些事情.没有做事情之前,这个runloop处于休眠状态,一旦调用该线程处理事件,就回唤醒runloop去执行该线程的事件
使用:搞一个常驻线程监控联网状态
原理:
1.创建一个子线程并开启线程
2.在开启方法(run)中添加一个runloop,并开启runloop
3.runloop会一直跑圈不会执行完毕,阻止了开启方法的执行完毕即卡住子线程不让他执行完毕
4.一旦子线程中有事件,runloop会被唤醒,让改事件在该线程中执行,一旦执行完毕,继续跑圈卡住该线程
如果用一个死循环代替runloop是不可以的,因为死循环一直在处理这个事件,不会停下来先去执行触发事件
*/
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[ZHThread alloc] initWithTarget:self selector:@selector(run) object:nil];
//执行线程
[self.thread start];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[super touchesBegan:touches withEvent:event];
//随时拿到这个子线程执行事件
[self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:nil];
}
-(void)run{
NSLog(@"=====run====");
//run:默认为NSDefaultRunLoopMode模式,时间是永远,下面三行代码等价
//往里面添加source(mode中啥都没有,runloop没用)
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
//启动runloop
[[NSRunLoop currentRunLoop] run];
// [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
// [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
NSLog(@"永远不会调用======");
}
-(void)run2{
NSLog(@"=====run2====");
}
上面代码用图解如下:
- 在子线程搞个定时器一直调用
//创建子线程初始化调用这个方法(将上面viewdidload方法中run改为runx即可)
-(void)runx{
@autoreleasepool {
//创建定时器
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run2) userInfo:nil repeats:YES];
//将定时器添加到runloop
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
//或者
// [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run2) userInfo:nil repeats:YES];
//放在主线程中就不会用到这句,因为主线程一直在run,不用手动run
// [[NSRunLoop currentRunLoop] run];
}
}
- 自动释放池
- 将所有对象都放到这个池子中去,到池子释放的时候给池子中的每一个对象release
- 自动释放池什么时候释放?(看源码得到)
runloop休眠之前会释放一次,因为休眠可能会很长,如果不释放,会堆积很多
释放后会同时创建一个新的释放池,处理新一轮事件
因此,在创建runloop的代码外层最好包装一层自动释放池
比如上面的代码
GCD的定时器使用
- GCD的定时器不受runLoop的Mode影响(滚动界面时,定时器不受影响)
GCD定时器实现不一样 - GCD比NSTimer准确
代码举例:
#import "ViewController.h"
@interface ViewController ()
/** 定时器(这里不用带*,因为dispatch_source_t就是个类,内部已经包含了*) */
@property (nonatomic, strong) dispatch_source_t timer;
@end
@implementation ViewController
int count = 0;
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// 获得队列
dispatch_queue_t queue = dispatch_get_main_queue();
// 创建一个定时器(dispatch_source_t本质还是个OC对象)
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 设置定时器的各种属性(几时开始任务,每隔多长时间执行一次)
// GCD的时间参数,一般是纳秒(1秒 == 10的9次方纳秒)NSEC_PER_SEC = 1s
// dispatch_time(DISPATCH_TIME_NOW, 3.0 * NSEC_PER_SEC) 比当前时间晚3秒
//当前时间直接写:DISPATCH_TIME_NOW
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC));
uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
dispatch_source_set_timer(self.timer, start, interval, 0);
// 设置回调
dispatch_source_set_event_handler(self.timer, ^{
NSLog(@"------------%@", [NSThread currentThread]);
count++;
if (count == 4) {
// 取消定时器
dispatch_cancel(self.timer);
self.timer = nil;
}
});
// 启动定时器
dispatch_resume(self.timer);
}
@end
IOS程序启动与运转
注:这段是copy谁的我忘了,总结的很好,借用一下哈
从Xcode的线程函数调用栈(注意看看)可以看到一些方法调用顺序。
(主线程开启)start->(加载framework,动态静态链接库,启动图片,Info.plist,pch等)->main函数->UIApplicationMain函数:{
- 初始化UIApplication单例对象
- 初始化AppDelegate对象,并设为UIApplication对象的代理
- 检查Info.plist设置的xib文件是否有效,如果有则解冻Nib文件并设置outlets,创建显示key window、rootViewController、与rootViewController关联的根view(没有关联则看rootViewController同名的xib),否则launch之后由程序员手动加载。
- 建立一个主事件循环,其中包含UIApplication的Runloop来开始处理事件。
}
UIApplication:
- 通过window管理视图;
- 发送Runloop封装好的control消息给target;
- 处理URL,应用图标警告,联网状态,状态栏,远程事件等。
AppDelegate:
管理UIApplication生命周期和应用的五种状态(notRunning/inactive/active/background/suspend)。
Key Window:
- 显示view;
- 管理rootViewcontroller生命周期;
- 发送UIApplication传来的事件消息给view。
rootViewController: - 管理view(view生命周期;view的数据源/代理;view与superView之间事件响应nextResponder的“备胎”);
- 界面跳转与传值;
- 状态栏,屏幕旋转。
view:
- 通过作为CALayer的代理,管理layer的渲染(顺序大概是先更新约束,再layout再display)和动画(默认layer的属性可动画,view默认禁止,在UIView的block分类方法里才打开动画)。layer是RGBA纹理,通过和mask位图(含alpha属性)关联将合成后的layer纹理填充在像素点内,GPU每1/60秒将计算出的纹理display在像素点中。
- 布局子控件(屏幕旋转或者子视图布局变动时,view会重新布局)。
- 事件响应:event和guesture。
runloop:
- (要让马儿跑)通过do-while死循环让程序持续运行:接收用户输入,调度处理事件时间。
- (要让马儿少吃草)通过mach_msg()让runloop没事时进入trap状态,节省CPU资源。
RunLoop面试题
- 什么是RunLoop?
1>从字面意思看:运行循环/跑圈
2>其实它内部就是do-while循环,在这个循环内部不断地处理各种任务(比如:source/Timer/Observer)
3>一个线程对应一个runloop,主线程的runloop默认已经启动,子线程的runloop需要自己手动启动(调用run方法)
4>RunLoop只能选择一个Mode启动,如果当Mode中没有任何source(source0/source1),Timer,那么就直接退出RunLoop - 自动释放池什么时候释放?
通过Observer监听RunLoop的状态,一旦监听到RunLoop即将进入休眠等待状态,就释放自动释放池(KCFRunLoopBeforeWaiting) - 在开发中如何使用RunLoop?什么应用场景?
1>开启一个常驻线程(让一个子线程不进入消亡状态,等待其他的线程发来消息,处理其他事件)
在子线程中开启一个定时器
在子线程中进行一些长期监控
2>可以控制定时器在特定模式下运行
3>可以让某些事件(行为/任务)在特定模式下执行
4>可以添加Observer监听runloop的状态,比如监听点击事件的处理(在所有点击事件之前做一些事情)