iOS底层-Runloop(二)
RunLoop的应用
1. 定时器的影响
- 用NSTimer时,一旦页面滚动定时器就会停止(前面已举例)
- 原因:默认定时器在kCFRunLoopDefaultMode模式下,一点滚动就会切换到UITrackingRunLoopMode模式下,所以定时器滚动时无效
- 解决办法:使用NSRunLoopCommonModes模式
- 除了上面的解决办法,还可以使用GCD
- 上面说过,一般情况下GCD不依赖于Runloop,它有自己的逻辑处理,跟Runloop无关
-
代码举例:
#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
2. 常驻线程
- 常驻线程:让一个线程永远不死
-
示例代码
#import "ViewController.h" #import "ZHThread.h" @interface ViewController () @property (nonatomic,strong) ZHThread *thread; @end /* 通过继承NSThread并且从写delloc监听线程,可以看到,开启线程后执行完任务立马死掉 注意:不能简单搞个属性强引用,因为线程一旦执行完毕,即使不释放掉,当前的线程也处于消亡状态,从新开启([self.thread start])会崩溃 当然不通过start调用,通过performselector调用虽然不会崩溃,但是不会调用run2,因为线程处于消亡状态 //解决办法:线程中添加runloop 好处:可以随时让拿到这个线程让他做一些事情.没有做事情之前,这个runloop处于休眠状态,一旦调用该线程处理事件,就回唤醒runloop去执行该线程的事件 使用:搞一个常驻线程监控联网状态 原理: 1.创建一个子线程并开启线程 2.在开启方法(run)中添加一个runloop,并开启runloop 3.runloop会一直跑圈不会执行完毕,阻止了开启方法的执行完毕即卡住子线程不让他执行完毕 4.一旦子线程中有事件,runloop会被唤醒,让改事件在该线程中执行,一旦执行完毕,继续跑圈卡住该线程 如果用一个死循环代替runloop是不可以的,因为死循环一直在处理这个事件,不会停下来先去执行触发事件 */ @implementation ViewController - (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===="); //往里面添加source(mode中啥都没有,runloop没用) [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode]; //启动runloop [[NSRunLoop currentRunLoop] run]; NSLog(@"=====runloop退出======"); } //子线程调用 -(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]; } }
手动控制一个线程的生命周期
- 上面的示例代码是线程在整个应用程序中都不死,那么,如果我们想手动控制一个线程的生命周期呢?
- 先使用上面代码,在控制器中也实现delloc,然后实现导航控制器的跳转,我们会发现,以下问题
- back到上一个控制器时,当前控制器不会销毁:说明上面代码有循环引用
- self.thread也不会销毁,那么是不是因为与控制器循环引用了呢?
- 代码分析结果:
- 经过分析发现 NSThread的initWithTarget:方法会捕获target,导致循环引用
-
那么改成这么创建
self.thread = [[ZHThread alloc]initWithBlock:^{ NSLog(@"=====run===="); //往里面添加source(mode中啥都没有,runloop没用) [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode]; //启动runloop [[NSRunLoop currentRunLoop] run]; NSLog(@"===runloop退出======"); }];
- 这么创建,会发现控制器delloc了,但是self.thread仍然没有销毁
- 因为self.thread内部有runloop,一直处于休眠状态,那么self.thread就必须一直处理事情,所以self.thread不死
- 那么接下来需要实现手动退出runloop
- 实现手动退出runloop
-
添加一个退出按钮事件
//手动控制runloop退出 - (IBAction)stop { [self performSelector:@selector(stop) onThread:self.thread withObject:self waitUntilDone:NO]; } //必须是创建的那个线程的runloop,因此,这个方法必须在那个线程中(self.thread)调用 //停止当前的Runloop,否则,线程永远不死 -(void)stopThread{ CFRunLoopStop(CFRunLoopGetCurrent()); NSLog(@"=====%@", [NSThread currentThread]); }
- 点击手动退出后,然后在点击back到上一界面,发现self.thread仍然没有销毁,那是为什么呢?
- 经过排查发现,这个方法有问题:
[[NSRunLoop currentRunLoop] run];
- 这个方法本质是一个死循环,会一直调用run,专门用来开启一个永不销毁的线程!!!即一旦开启runloop,就不可以控制runloop的退出
- 经过排查发现,这个方法有问题:
-
那么将这个方法替换成下面的方法
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
- 换成这个方法后,点击touch界面,处理一次子线程事件后发现,还没有点击退出按钮,runloop直接退出了,但是back上一个控制器,self.thread销毁了
- 这个方法,只会启动一次runloop,当前线程只要执行一次事件就会自动退出当前的runloop(即只会处理一次事件,就会退出runloop)
- 因此,下面我们只要实现使用这个方法一直开启runloop,而且能够随时控制它退出,就达到目的了
-
因此想要线程手动控制,而且不死那就改成这个
- (void)viewDidLoad { [super viewDidLoad]; __weak typeof(self) weakself = self; self.thread = [[ZHThread alloc]initWithBlock:^{ NSLog(@"=====run===="); //往里面添加source(mode中啥都没有,runloop没用) [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode]; //启动runloop //搞一个循环,一直循环开启runloop,但是可以控制退出 while (!weakself.isstop) { //这个方法,每次掉用就会启动一次runloop,当前线程只要执行一次事件就会自动退出当前的runloop [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; } NSLog(@"===runloop退出======"); }]; //执行线程 [self.thread start]; }
- 用一个属性来控制runloop是否开启
-
stopThread方法设置isstop
//停止当前的Runloop,否则,线程永远不死 -(void)stopThread{ self.isstop = YES; CFRunLoopStop(CFRunLoopGetCurrent()); NSLog(@"=====%@", [NSThread currentThread]); }
-
-
最终代码如下:
#import "ZHViewController.h" #import "ZHThread.h" @interface ZHViewController () @property (nonatomic,strong) ZHThread *thread; @property (nonatomic, assign) BOOL isstop; @end @implementation ZHViewController - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = UIColor.whiteColor; __weak typeof(self) weakself = self; //initWithTarget这个会捕获target导致循环引用 // self.thread = [[ZHThread alloc] initWithTarget:self selector:@selector(run) object:nil]; self.thread = [[ZHThread alloc]initWithBlock:^{ NSLog(@"=====run===="); //run:默认为NSDefaultRunLoopMode模式,时间是永远,下面三行代码等价 //往里面添加source(mode中啥都没有,runloop没用) [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode]; //启动runloop //注意:!!!!这个方法本质是一个死循环,会一直调用run,专门用来开启一个永不销毁的线程!!!即一旦开启runloop,就不可以控制runloop的退出 //[[NSRunLoop currentRunLoop] run]; //搞一个循环,一直循环开启runloop,但是可以控制退出 while (!weakself.isstop) { //这个方法,每次掉用就会启动一次runloop,当前线程只要执行一次事件就会自动退出当前的runloop [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; } NSLog(@"===runloop退出======"); }]; //执行线程 [self.thread start]; } -(void)test{ NSLog(@"==test===%@", [NSThread currentThread]); } //停止当前的Runloop,否则,线程永远不死 -(void)stopThread{ self.isstop = YES; CFRunLoopStop(CFRunLoopGetCurrent()); NSLog(@"=====%@", [NSThread currentThread]); self.thread = nil; } //手动控制runloop退出 - (IBAction)stop { if(!self.thread) return; [self performSelector:@selector(stopThread) onThread:self.thread withObject:self waitUntilDone:NO]; } //随时使用子线程调用 -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ if(!self.thread) return; [self performSelector:@selector(test) onThread:self.thread withObject:self waitUntilDone:NO]; NSLog(@"=====%s",__func__); } //一旦在这调用,程序就会崩溃???? - (void)dealloc{ NSLog(@"=====%s",__func__); [self stop]; } @end
- 问题:
- 如果先点击stop退出runloop,在back到上一个控制器,则当前控制器、self.thread都销毁,没有问题
- 如果不点击stop退出runloop,直接back到上一个控制器,当前控制器指向delloc,但是程序会崩溃在
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
这一句,为什么呢? - 控制器的delloc里面已经执行stop了呀? 难道跟手动点击stop有什么区别呢?
- 经过代码分析,发现是这个方法的问题:
performSelector:onThread:withObject:waitUntilDone:
- 最后一个参数:
waitUntilDone
的问题 - 如果传的是YES: 代表子线程的performSelector中方法执行完之后再往下执行主线程。
- 如果是NO:直接走主线程,不管子线程
- 由于上面代码中设置的是NO,因此,delloc会先走完,即先走完stop,然后在执行stopThread
- 由于已经delloc了,即当前控制器已经被释放了,然后再去执行停止runloop,因此会崩溃,报EXC_BAD_ACCESS 内存错误
- 只要把参数设置为YES即可
-
举例:
//手动控制runloop退出 - (IBAction)stop { /** YES: //先执行stopThread =stoprun====<ZHThread: 0x600002208140>{number = 4, name = (null)} //再打印 123456 NO: //先打印 123456 //再执行stopThread =stoprun====<ZHThread: 0x600002208140>{number = 4, name = (null)} */ [self performSelector:@selector(stopThread) onThread:self.thread withObject:self waitUntilDone:YES]; NSLog(@"123456"); }
- 最后一个参数:
- 改为YES以后,点击back回到上一VC,会发现不崩溃了,控制器delloc了,但是ZHThread仍然没有delloc,为什么呢???
- 在while循环内部打断点,然后点击back,会发现仍然会走while循环内部
- 但是在stopThread方法中已经将
self.isstop = YES;
了啊? - 打印weakself发现,此时为nil了,因此
!weakself.isstop
还是为YES,所以还会走while循环内部,那么久又开启了runloop,所以ZHThread仍然没有销毁。 -
解决办法,将while循环条件改为如下:
//搞一个循环,一直循环开启runloop,但是可以控制退出 //必须判断weakself是否为nil!!!! while ( weakself && !weakself.isstop) { //这个方法,每次掉用就会启动一次runloop,当前线程只要执行一次事件就会自动退出当前的runloop [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; }
- 经过测试,完美解决手动控制子线程生命周期
- 问题:
-
封装一个手动控制子线程生命周期的API
#import <Foundation/Foundation.h> @interface ZHPermenantThread : NSObject /** 开启一个子线程 */ -(void)run; /** 销毁一个子线程 */ -(void)stop; /** 利用子线程执行一个任务 @param task 任务内容 */ -(void)excuteTask:(void (^)(void))task; @end #import "ZHPermenantThread.h" /*********ZHThread*********/ @interface ZHThread : NSThread @end @implementation ZHThread - (void)dealloc { NSLog(@"=====%s",__func__); } @end /*********ZHPermenantThread**********/ @interface ZHPermenantThread () @property (nonatomic,strong) ZHThread *innerThread; @property (nonatomic, assign) BOOL isstop; @end @implementation ZHPermenantThread - (instancetype)init { self = [super init]; if (self) { self.isstop = NO; __weak typeof(self) weakself = self; self.innerThread = [[ZHThread alloc] initWithBlock:^{ [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode]; while ( weakself && !weakself.isstop) { //这个方法,每次掉用就会启动一次runloop,当前线程只要执行一次事件就会自动退出当前的runloop [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; } }]; } return self; } -(void)run{ if(!self.innerThread) return; [self.innerThread start]; } -(void)stop{ if(!self.innerThread) return; [self performSelector:@selector(__stop) onThread:self.innerThread withObject:self waitUntilDone:YES]; } -(void)excuteTask:(void (^)(void))task{ if(!self.innerThread || !task) return; //执行任务没有必要卡主主线程:NO [self performSelector:@selector(__excuteTask:) onThread:self.innerThread withObject:task waitUntilDone:NO]; } #pragma mark - private methods //停止当前的Runloop,否则,线程永远不死 -(void)__stop{ self.isstop = YES; CFRunLoopStop(CFRunLoopGetCurrent()); NSLog(@"=stoprun====%@", [NSThread currentThread]); self.innerThread = nil; } -(void)__excuteTask:(void (^)(void))task{ task(); } - (void)dealloc{ //当前对象销毁,innerThread也要销毁 [self stop]; NSLog(@"=====%s",__func__); } @end //使用 #import "ZHViewController.h" #import "ZHPermenantThread.h" @interface ZHViewController () @property (nonatomic,strong) ZHPermenantThread *thread; @end @implementation ZHViewController - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = UIColor.whiteColor; self.thread = [[ZHPermenantThread alloc] init]; [self.thread run]; } //手动控制runloop退出 - (IBAction)stop { [self.thread stop]; } //随时使用子线程调用 -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ [self.thread excuteTask:^{ NSLog(@"==执行任务===%@", [NSThread currentThread]); }]; } - (void)dealloc{ NSLog(@"=====%s",__func__); } @end
-
也可以用C语言方式实现(更简单)
- (instancetype)init { self = [super init]; if (self) { self.isstop = NO; self.innerThread = [[ZHThread alloc] initWithBlock:^{ NSLog(@"runloop--run"); //创建上下文 CFRunLoopSourceContext text = {0}; //结构体初始化 //创建一个source CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &text); //添加source CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode); //销毁sorce CFRelease(source); // while ( weakself && !weakself.isstop) { // //启动runloop // //第三个参数:true:代表执行完source后就会退出当前runloop, // CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true); // } //false:执行完任务不退出当前runloop,等价于上面的while循环 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false); NSLog(@"runloop--stop"); }]; } return self; }
-
performSelector的使用
//延迟3秒设置图片并且值只在NSDefaultRunLoopMode模式下,也就是说,在滚动的时候,不显示图片,滚动停止,显示图片
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"123"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]
自动释放池
- 所有对象都放到这个池子中去,到池子释放的时候给池子中的每一个对象release
- 自动释放池什么时候释放?(看源码得到)
- runloop休眠之前会释放一次,因为休眠可能会很长,如果不释放,会堆积很多
- 释放后会同时创建一个新的释放池,处理新一轮事件
- 因此,在创建runloop的代码外层最好包装一层自动释放池
- 比如上面的全局定时器代码
RunLoop面试题
- 什么是RunLoop?
- 从字面意思看:运行循环/跑圈
- 其实它内部就是do-while循环,在这个循环内部不断地处理各种任务(比如:source/Timer/Observer)
- 一个线程对应一个runloop,主线程的runloop默认已经启动,子线程的runloop需要自己手动启动(调用run方法)
- RunLoop只能选择一个Mode启动,如果当Mode中没有任何source(source0/source1),Timer,那么就直接退出RunLoop
- 自动释放池什么时候释放?
- 通过Observer监听RunLoop的状态,一旦监听到RunLoop即将进入休眠等待状态,就释放自动释放池(KCFRunLoopBeforeWaiting)
- 在开发中如何使用RunLoop?什么应用场景?
- 开启一个常驻线程(让一个子线程不进入消亡状态,等待其他的线程发来消息,处理其他事件)
在子线程中开启一个定时器 在子线程中进行一些长期监控 - 可以控制定时器在特定模式下运行
- 可以让某些事件(行为/任务)在特定模式下执行
- 可以添加Observer监听runloop的状态,比如监听点击事件的处理(在所有点击事件之前做一些事情)
- 开启一个常驻线程(让一个子线程不进入消亡状态,等待其他的线程发来消息,处理其他事件)
- 自动释放池与RunLoop
kCFRunLoopEntry;
// 创建一个自动释放池kCFRunLoopBeforeWaiting;
// 销毁自动释放池,创建一个新的自动释放池kCFRunLoopExit;
// 销毁自动释放池
IOS程序启动与运转
从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资源。