iOS之RunLoop详解
2019-03-12 本文已影响330人
云霄_云霄
1.RunLoop的介绍:
RunLoop即运行循环(跑圈),只不过它这种循环比较高级。一般的 while 循环会导致 CPU 进入忙等待状态,而 RunLoop 则是一种“闲”等待,这部分可以类比 Linux 下的 epoll。当没有事件时,RunLoop 会进入休眠状态,有事件发生时, RunLoop 会去找对应的 Handler 处理事件。RunLoop 可以让线程在需要做事的时候忙起来,不需要的话就让线程休眠。
2.RunLoop的基本作用:
- 保持程序的持续运行
- 处理App中的各种事件(比如触摸事件、定时器事件、Selector事件)
- 节省CPU资源,提高程序性能,该工作的时候工作,该休息就休息
3.iOS程序入口与RunLoop的关系:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
由于main函数里面UIApplicationMain
启动了一个RunLoop,所以UIApplicationMain
函数一直没有返回,则程序并不会马上退出,而是保持持续运行状态,这个默认的RunLoop是跟主线程相关联的。
4.RunLoop对象:
-
NSRunLoop
(OC中Foundation框架下) -
CFRunLoopRef
(C中Core Foundation框架下)
5.RunLoop与线程:
- 每条线程都有唯一一个与之对应的RunLoop对象(字典的方式)。
- 主线程的RunLoop默认已经创建并且开启了,子线程对应的RunLoop需要手动创建并开启。
- RunLoop在第一次获取时创建,在线程结束时销毁。
-
RunLoop与线程的关系如下图
runLoop.jpg
6.获取RunLoop对象:
//Foundation框架下
NSRunLoop * currentRunLoop = [NSRunLoop currentRunLoop]; //获取当前线程的RunLoop对象
NSRunLoop * mainRunLoop = [NSRunLoop mainRunLoop]; //获取主线程的RunLoop对象
//Core Foundation框架下
/*
CFRunLoopRef * currentRunLoop = CFRunLoopGetCurrent() //获取当前线程的RunLoop对象
CFRunLoopRef * mainRunLoop = CFRunLoopGetMain() //获取主线程的RunLoop对象
*/
7.RunLoop的相关类:
-
CFRunLoopRef
--RunLoop本身 -
CFRunLoopModeRef
--RunLoop运行模式(CFRunLoopModeRef代表RunLoop的运行模式,一个RunLoop包含若干个Model,每个Model又包含若干个Source/Timer/Observer,每次RunLoop启动时,智能指定其中一个Model,这个Model被称作CurrentMode,如果需要切换Model,只能退出Loop,在重新指定一个Model进入,这样做主要是为了分隔开不同的Source/Timer/Observer,让其互不影响) -
CFRunLoopSourceRef
--RunLoop的事件(input Source) -
CFRunLoopTimerRef
--(Timer Source) -
CFRunLoopObserverRef
--监听者
CFRunLoopModeRef
的详解:
- 系统默认注册了5个Model
- kCFRunLoopDefaultMode :App的默认model,通常主线程是在这个model下进行
- UITrackingRunLoopMode :界面跟踪model,用于scrolView追踪触摸滑动,保证界面滑动时不受其他model影响
- UIinitializationRunLoopMode :在刚启动App时进入的第一个Mode,启动完成后不在使用
- GSEventReceiveRunLoopMode :接受系统事件的内部mode,通常用不到
- kCFRunLoopCommonModes :这是一个占位mode,不是一个真正的mode
8.RunLoop的运行模式和Timer:
-(void)timer{
//01-创建定时对象
NSTimer * timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
//02-把定时器添加到RunLoop中(切记:如果当前方法在子线程,需要手动开启当前子线程的RunLoop,并且开启,而且开启方法要在添加定时器之后,否则开启了因为没有事件就直接停止了)
//(情况1)模式为默认:NSDefaultRunLoopMode--当滚动UItextView的时候定时器停止工作
//[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//(情况2)界面追踪模式为:UITrackingRunLoopMode--(只有当滚动UItextView的时候定时器才工作)
//[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
//(情况3)界面追踪模式为:NSRunLoopCommonModes(NSDefaultRunLoopMode|UITrackingRunLoopMode)--(不管有没有滚动,定时器一直工作)
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
-(void)run{
NSLog(@"%@",[NSRunLoop currentRunLoop].currentMode);
}
9.CFRunLoopObserverRef
的详解:
- CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变。
- 可以监听的时间点有以下:
kCFRunLoopEntry = (1UL << 0), //即将进入Loop kCFRunLoopBeforeTimers = (1UL << 1), //即将处理Timer kCFRunLoopBeforeSources = (1UL << 2), //即将处理Source kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠 kCFRunLoopAfterWaiting = (1UL << 6), //刚从休眠中唤醒 kCFRunLoopExit = (1UL << 7), //即将退出loop kCFRunLoopAllActivities = 0x0FFFFFFFU //所有的状态
/*
01-创建观察者
参数说明
第一个参数:分配存储空间
第二个参数:要监听的状态
第三个参数:是否持续监听
第四个参数:0
第五个参数:block回调,当runloop状态改变的时候会调用
输出:
2019-03-12 15:13:08.598453+0800 SmallProgram[3374:145215] runloop即将处理Timers事件
2019-03-12 15:13:08.598653+0800 SmallProgram[3374:145215] runloop即将处理Sources事件
2019-03-12 15:13:08.598906+0800 SmallProgram[3374:145215] runloop即将处理Timers事件
2019-03-12 15:13:08.599048+0800 SmallProgram[3374:145215] runloop即将处理Sources事件
2019-03-12 15:13:08.599180+0800 SmallProgram[3374:145215] runloop即将处理Timers事件
2019-03-12 15:13:08.599295+0800 SmallProgram[3374:145215] runloop即将处理Sources事件
2019-03-12 15:13:08.599620+0800 SmallProgram[3374:145215] runloop即将进入到休眠
2019-03-12 15:13:08.693981+0800 SmallProgram[3374:145215] runloop被唤醒
2019-03-12 15:13:08.694216+0800 SmallProgram[3374:145215] runloop即将处理Timers事件
2019-03-12 15:13:08.694340+0800 SmallProgram[3374:145215] runloop即将处理Sources
*/
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;
}
});
/*
//02-监听runLoop的状态
参数说明
第一个参数:runLoop对象
第二个参数:监听者
第三个参数:runLoop在哪种模式下的状态(这里要写C情况下的状态)
kCFRunLoopDefaultMode
*/
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
10.RunLoop的应用:
- (情景一)在tableView头视图上添加定时器轮播的时候,如果想要滑动tableView的时候,也要轮播,则修改runLoop的模式为
NSRunLoopCommonModes
- (情景二)定时器一直是准确运行的来执行对应的操作,则修改runLoop的模式为
NSRunLoopCommonModes
- 设置程序的常驻线程。
-(void)application{
NSThread * thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[thread start];
self.thread = thread;
}
-(void)run{
//01子线程的RunLoop需要手动创建并启动
//02RunLoop启动后会选择运行模式,判断运行模式是否为空
NSRunLoop * runLoop = [NSRunLoop currentRunLoop];
//(方法一)往运行模式中添加timer
//[NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(timeRun) userInfo:nil repeats:YES];
//(方法二)往运行模式中添加source=port|custom|selector
[runLoop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
//将下面方法放到需要子线程来做任务的事件中,比如点击按钮事件
//按钮点击事件让子线程来执行任务run2
[self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:YES];
//这样self.thread会一直不死,来等待处理run2的事件
11.RunLoop的内部自动释放池:
- App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
- 第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
- 第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
-
在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
具体流程
本文参考:
线程、自动释放池、RunLoop的爱恨情仇https://www.jianshu.com/p/8b011b844231