RunLoop详解
RunLoop运行循环(死循环)
RunLoop模式
- NSDefaultRunLoopMode -- 默认模式,苹果建议该模式放时钟,网络事件
- UITrackingRunLoopMode -- UI模式,只能被触摸事件所唤醒! !
- NSRunLoopCommonModes -- 并不是一个真正的RunLoop的模式! 而是一个占位模式
- UIInitializationRunLoopMode 在刚启动 App 时进入的第一个 Mode,启动完成后就不再使用。
- GSEventReceiveRunLoopMode 接受系统事件的内部 Mode,通常用不到。
主线程一开起来,就会跑一个和主线程对应的RunLoop,那么RunLoop一定是在程序的入口main函数中开启。第三个参数是当前app的单利对象,填写nil相当于填写了一个UIApplication
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
进入UIApplicationMain
UIKIT_EXTERN int UIApplicationMain(int argc, char *argv[], NSString * __nullable principalClassName, NSString * __nullable delegateClassName);
我们发现它返回的是一个int数,那么我们对main函数做一些修改
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"开始");
int re = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
NSLog(@"结束");
return re;
}
}
运行程序,我们发现只会打印开始,并不会打印结束,这说明在UIApplicationMain函数中,开启了一个和主线程相关的RunLoop,导致UIApplicationMain不会返回,一直在运行中,也就保证了程序的持续运行。
我们来看到RunLoop的源码
// 用DefaultMode启动
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
我们发现RunLoop确实是do while通过判断result的值实现的。因此,我们可以把RunLoop看成一个死循环。如果没有RunLoop,UIApplicationMain函数执行完毕之后将直接返回,也就没有程序持续运行一说了
CFRunLoopSourceRef事件源(输入源)
按照函数调用栈可以把Source分为两种
Source0:非基于Port的 用于用户主动触发的事件(点击button 或点击屏幕)
Source1:基于Port的 通过内核和其他线程相互发送消息(与内核相关)
注意:Source1在处理的时候会分发一些操作给Source0去处理
RunLoop退出
- 主线程销毁RunLoop退出
- Mode中有一些Timer 、Source、 Observer,这些保证Mode不为空时保证RunLoop没有空转并且是在运行的,当Mode中为空的时候,RunLoop会立刻退出
我们在启动RunLoop的时候可以设置什么时候停止
[NSRunLoop currentRunLoop]runUntilDate:<#(nonnull NSDate *)#>
[NSRunLoop currentRunLoop]runMode:<#(nonnull NSString *)#> beforeDate:<#(nonnull NSDate *)#>
RunLoop应用
1. 常驻线程
常驻线程的作用:我们知道,当子线程中的任务执行完毕之后就被销毁了,那么如果我们需要开启一个子线程,在程序运行过程中永远都存在,那么我们就会面临一个问题,如何让子线程永远活着,这时就要用到常驻线程:给子线程开启一个RunLoop
注意:子线程执行完操作之后就会立即释放,即使我们使用强引用引用子线程使子线程不被释放(强引用只是引用的对象不被释放,但不能使线程保活),也不能给子线程再次添加操作,或者再次开启。
HKThread *thread = [[HKThread alloc] initWithBlock:^{
_timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
// while (true) {
// //啥都不干,只为线程保活
// }
[[NSRunLoop currentRunLoop]addTimer:_timer forMode:NSDefaultRunLoopMode];
//每条线程上面都有一个runloop,只不过,默认是不开启的
[[NSRunLoop currentRunLoop] run];//这里是一个死循环,下面的这一句代码不走
NSLog(@"来了!!!!");
}];
[thread start];
第二种方法
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 创建子线程并开启
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(show) object:nil];
self.thread = thread;
[thread start];
}
-(void)show
{
// 注意:打印方法一定要在RunLoop创建开始运行之前,如果在RunLoop跑起来之后打印,RunLoop先运行起来,已经在跑圈了就出不来了,进入死循环也就无法执行后面的操作了。
// 但是此时点击Button还是有操作的,因为Button是在RunLoop跑起来之后加入到子线程的,当Button加入到子线程RunLoop就会跑起来
NSLog(@"%s",__func__);
// 1.创建子线程相关的RunLoop,在子线程中创建即可,并且RunLoop中要至少有一个Timer 或 一个Source 保证RunLoop不会因为空转而退出,因此在创建的时候直接加入
// 添加Source [NSMachPort port] 添加一个端口
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
// 添加一个Timer
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//创建监听者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"RunLoop进入");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"RunLoop要处理Timers了");
break;
case kCFRunLoopBeforeSources:
NSLog(@"RunLoop要处理Sources了");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"RunLoop要休息了");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"RunLoop醒来了");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop退出了");
break;
default:
break;
}
});
// 给RunLoop添加监听者
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 2.子线程需要开启RunLoop
[[NSRunLoop currentRunLoop]run];
CFRelease(observer);
}
注意:创建子线程相关的RunLoop,在子线程中创建即可,并且RunLoop中要至少有一个Timer 或 一个Source 保证RunLoop不会因为空转而退出,因此在创建的时候直接加入,如果没有加入Timer或者Source,或者只加入一个监听者,运行程序会崩溃
2. 自动释放池
Timer和Source也是一些变量,需要占用一部分存储空间,所以要释放掉,如果不释放掉,就会一直积累,占用的内存也就越来越大,这显然不是我们想要的。
那么什么时候释放,怎么释放呢?
RunLoop内部有一个自动释放池,当RunLoop开启时,就会自动创建一个自动释放池,当RunLoop在休息之前会释放掉自动释放池的东西,然后重新创建一个新的空的自动释放池,当RunLoop被唤醒重新开始跑圈时,Timer,Source等新的事件就会放到新的自动释放池中,当RunLoop退出的时候也会被释放。
注意:只有主线程的RunLoop会默认启动。也就意味着会自动创建自动释放池,子线程需要在线程调度方法中手动添加自动释放池。
@autorelease{
// 执行代码
}
附: 使用GCD也可是创建计时器,而且更为精确我们来看一下代码
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//创建队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//1.创建一个GCD定时器
/*
第一个参数:表明创建的是一个定时器
第四个参数:队列
*/
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 需要对timer进行强引用,保证其不会被释放掉,才会按时调用block块
// 局部变量,让指针强引用
self.timer = timer;
//2.设置定时器的开始时间,间隔时间,精准度
/*
第1个参数:要给哪个定时器设置
第2个参数:开始时间
第3个参数:间隔时间
第4个参数:精准度 一般为0 在允许范围内增加误差可提高程序的性能
GCD的单位是纳秒 所以要*NSEC_PER_SEC
*/
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
//3.设置定时器要执行的事情
dispatch_source_set_event_handler(timer, ^{
NSLog(@"---%@--",[NSThread currentThread]);
});
// 启动
dispatch_resume(timer);
}
网友评论