RunLoop原理总结-iOS
1、简介
运行循环,保持程序的持续运行,main函数中就默认启动了一个RunLoop,如果没有RunLoop程序会立即退出,有了RunLoop就可以实现在有操作的时候去执行,没有操作的时候停下来休息,节省CPU资源,提高性能。
官方文档对RunLoop的讲解
2、RunLoop的创建
NSRunLoop是基于CFRunLoopRef的封装,CFRunLoopRef是在CoreFoundation框架内的,是开源的。
__CFRunLoop结构体:
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread; //对应的线程
uint32_t _winthread;
CFMutableSetRef _commonModes; //多个commonModes
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode; //当前运行的mode
CFMutableSetRef _modes; //多个modes
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFTypeRef _counterpart;
};
核心方法是__CFRunloopRun(),截取其中的一部分,可以看到__CFRunLoopRun内部其实是一个do while循环。它在休眠时几乎不占用系统资源,这是由系统内核负责实现的
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内部运行原理
可以看到RunLoop在循环中,当接收到Input sources或者Timer sources时会交给对应的处理方去处理。当没有事件消息传入的时候,RunLoop就休息了
3、NSTimer的创建
在创建计时器NSTimer的时候我们通常会用到RunLoop(GCD创建的计时器是不基于runloop的),会把timer对象加到currentRunLoop中,并指定mode为NSDefaultRunLoopMode
self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerRun) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
RunLoopMode有
- NSDefalutRunLoopMode 默认状态.空闲状态
- UITrackingRunLoopMode 处理UI事件的mode,用于界面控件(ScrollView,tableView等等)追踪触摸滑动,保证界面滑动时不受其他Mode影响
- UIInitializationRunLoopMode 刚启动App时进入的第一个Mode,启动完成后不再使用
- NSRunLoopCommonModes 占位用的Mode,不是真正的Mode,默认在Defalut和Tracking这两种模式下
- GSEventReceiveRunLoopMode 接受系统事件的内部 Mode,通常用不到
但是在用户滑动ScrollView时,RunLoop会转到UITrackingRunLoopMode,这个时候timer就不会再继续运行,我们可以理解为runloop维护了一张map,以mode为key,以某种结构为value,我们命名为NSRunLoopState。
在这个NSRunLoopState结构中,需要维护Input source和Timer source等事件源。
当runloop切换到某种mode时,可以获取到对应的NSRunLoopState接口,并对该结构中的事件源进行处理,不能再处理其他mode中的事件源。
为了解决这个问题,我们有两种方式
1、设置计时器的runloopMode为NSRunLoopCommonModes
2、开启线程去处理timer
4、监听runloop的状态改变
CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//创建监听者
/*
第一个参数 CFAllocatorRef allocator:分配存储空间 CFAllocatorGetDefault()默认分配
第二个参数 CFOptionFlags activities:要监听的状态 kCFRunLoopAllActivities 监听所有状态
第三个参数 Boolean repeats:YES:持续监听 NO:不持续
第四个参数 CFIndex order:优先级,一般填0即可
第五个参数 :回调 两个参数observer:监听者 activity:监听的事件
*/
/*
所有事件
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source
kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6),// 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7),// 即将退出RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
*/
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添加监听者
/*
第一个参数 CFRunLoopRef rl:要监听哪个RunLoop,这里监听的是主线程的RunLoop
第二个参数 CFRunLoopObserverRef observer 监听者
第三个参数 CFStringRef mode 要监听RunLoop在哪种运行模式下的状态
*/
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
}
5、RunLoop和线程
只有主线程是默认开始RunLoop的,其他线程需要手动去开启RunLoop
我们先看如下代码:
dispatch_async(dispatch_queue_create("runloop.test", DISPATCH_QUEUE_CONCURRENT), ^{
[self performSelector:@selector(log) withObject:self afterDelay:2];
});
-(void)log {
NSLog(@"执行了log");
}
并不会打印“执行了log”,这是因为子线程默认不开启runloop,子线程中的任务执行完毕后,子线程就会被销毁
我们把他改成
dispatch_async(dispatch_queue_create("runloop.test", DISPATCH_QUEUE_CONCURRENT), ^{
[self performSelector:@selector(log) withObject:self afterDelay:2];
[[NSRunLoop currentRunLoop] run];
});
给子线程手动开启runloop,发现打印了“执行了log”,通过RunLoop可以实现线程保活、线程常驻
6、RunLoop和自动释放池
Timer和Source也是一些变量,需要占用一部分存储空间,所以要释放掉,如果不释放掉,就会一直积累,占用的内存也就越来越大。
那么什么时候释放,怎么释放呢?
RunLoop内部有一个自动释放池,当RunLoop开启时,就会自动创建一个自动释放池,当RunLoop在休息之前会释放掉自动释放池的东西,然后重新创建一个新的空的自动释放池,当RunLoop被唤醒重新开始跑圈时,Timer,Source等新的事件就会放到新的自动释放池中,当RunLoop退出的时候也会被释放。
注意:只有主线程的RunLoop会默认启动。也就意味着会自动创建自动释放池,子线程需要在线程调度方法中手动添加自动释放池。