Runloop作用
- 1.保证当前线程不退出。
- 2.监听事件:触摸事件、时钟事件和网络事件。
- 3.节约资源:有事件时,处理事件。没有事件,处于休眠状态。
ps:事件产生到有结果的过程:
硬件设备接收信号 > 电信号转化成模拟信号 > 操作系统接收信号 > 找到响应的应用程序 > 找到具体的某个类的某个方法执行。
时钟事件和Runloop关系
案列一:
- (void)viewDidLoad {
[super viewDidLoad];
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
}
- (void)timerMethod {
static int a = 0;
NSLog(@"%d---%@",a,[NSThread currentThread]);
}
该方法已经将时钟事件添加到当前Runloop中,无需程序员操作什么。只是此时Runloop模式是默认模式。
案列二:
- (void)viewDidLoad {
[super viewDidLoad];
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:timer forMode: NSRunLoopCommonModes];
}
- (void)timerMethod {
static int a = 0;
NSLog(@"%d---%@",a,[NSThread currentThread]);
}
该方法返回一个NSTimer对象,通过加入到Runloop的占位模式下,开启定时服务。
Runloop的常见模式
- NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
默认模式,APP主线程是在该模式下运行。 - UITrackingRunLoopMode
UI模式,ScrollView滑动时的模式,其优先级高于默认模式。 - UIInitializationRunLoopMode
启动 App 时第进入的第一个 Mode,启动完成后就不再使用。 - NSRunLoopCommonModes(kCFRunLoopCommonModes)
占位模式,包含默认模式和UI模式。 - 更多模式
苹果公开提供的 Mode 有三个:
- NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
- UITrackingRunLoopMode
- NSRunLoopCommonModes(kCFRunLoopCommonModes)
note:Runloop在同一段时间只能并且必须在一种特定的Mode下运行。更换mode时,需要停止当前Loop,然后重启新Loop。
理解ibireme 深入理解RunLoopRunLoop 的 Mode:
个人理解_commonModes和_commonModeItems关系:
以Timer在默认模式和UI模式(即占位模式)下为例。主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为”Common”属性,就是说这两个Mode已经被添加到_commonModes中,并且Timer这个item已经被加到_commonModeItems中。当滑动Scrollview时,Runloop会退出,重新指定Mode为UI模式,再次开启Runloop,RunLoop 会自动将 _commonModeItems 里的Timer 同步到具有 “Common” 标记的所有Mode里。
问题探索
问题一:
为哈苹果建议时钟事件添加到Runloop的默认模式下,而不放到UI模式中呢?
举个例子:
- (void)viewDidLoad {
[super viewDidLoad];
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:UITrackingRunLoopMode];
}
- (void)timerMethod {
sleep(1.0);
static int a = 0;
NSLog(@"%d---%@",a,[NSThread currentThread]);
}
如果时钟事件添加到UI模式下,在timer的回调方法中添加一个耗时操作,会阻塞主线程,出现UI卡顿。
问题二:
开启一条子线程,执行放到RunLoop中的timer定时器,定时器的seletor为什么不执行?
代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
NSThread *thread = [[NSThread alloc]initWithBlock:^{
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
}];
[thread start];
}
- (void)timerMethod{
static NSInteger count = 0;
NSLog(@"count = %ld",count ++);
}
原因:子线程死掉,NSthread负责开辟一条线程,CPU负责调度线程,线程中任务一旦执行完成就会释放,若想保住子线程,开启一个死循环。
代码修改如下:
- (void)viewDidLoad {
[super viewDidLoad];
NSThread *thread = [[NSThread alloc]initWithBlock:^{
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
while (true) {
}
}];
[thread start];
}
- (void)timerMethod{
static NSInteger count = 0;
NSLog(@"count = %ld",count ++);
}
虽然开启一个死循环保住了子线程,此时的timerMethod方法依然没有执行,为什么?
原因:虽然在子线程中把timer放到RunLoop中,也保住了子线程,但是死循环中并没有操作什么,需要的是把子线程的RunLoop并没有从Event队列(消息队列)中取出处理。
代码再次修改如下:
- (void)viewDidLoad {
[super viewDidLoad];
NSThread *thread = [[NSThread alloc]initWithBlock:^{
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop]run];
}];
[thread start];
}
- (void)timerMethod{
static NSInteger count = 0;
NSLog(@"count = %ld",count ++);
}
如果需要手动停止这个线程?
代码三次修改如下:
- (void)viewDidLoad {
[super viewDidLoad];
isFinished = YES;
NSThread *thread = [[NSThread alloc]initWithBlock:^{
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
while (isFinished) {
[[NSRunLoop currentRunLoop]runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.0001]];
}
}];
[thread start];
}
- (void)timerMethod{
static NSInteger count = 0;
NSLog(@"count = %ld",count ++);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
isFinished = NO;
}
上述代码中为什么Runloop模式都是NSDefaultRunLoopMode?
原因:在子线程中使用默认模式,就可以,没有必要使用占位模式,子线程中使用默认模式并不会阻塞主线程。
问题三:
每一条线程都有一个runloop这种说法对吗?
不对,原因:创建了一个线程,RunLoop并未创建,但是当你第一次获取时,就会创建一个RunLoop, 再次获取时,拿到的只是第一次获取的RunLoop。(有点类似于懒加载)当线程退出([NSThread exit], RunLoop释放。
问题四:
主线程退出,对子线程有影响吗?
没有,主线程也是一条线程,主线程死掉,子线程依然可以正常运行。
问题五:
主线程为什么只有一个?
原因:线程之间访问资源存在资源抢夺的问题,假如存在两条线程就需要对同一个资源进行加锁,这样APP的流畅性就会降低,所以只会开辟一条主线程。
问题扩展一:
NSTimer定时器时间不精确原因?
一个循环中如果RunLoop没有被识别(这个时间大概在50-100ms)或者说当前RunLoop在执行一个长的call out(例如执行某个循环操作)则NSTimer可能就会存在误差,RunLoop在下一次循环中继续检查并根据情况确定是否执行。
问题扩展二:
performSelector:withObject:afterDelay:本质?
performSelector:withObject:afterDelay:执行的本质还是通过创建一个NSTimer然后加入到当前线程RunLoop。(类似的还有performSelector:onThread:withObject:afterDelay:,只是它会在另一个线程的RunLoop中创建一个Timer),所以此方法事实上在任务执行完之前会对触发对象形成引用,任务执行完进行释放(注意:performSelector: withObject:等方法则等同于直接调用,原理与此不同)。
相应的方法还有:
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
针对下面的几个方法,则是创建了一个source0事件。
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
扩展问题来自: iOS刨根问底-深入理解RunLoop
触摸事件与Runloop
触摸事件又叫事件源(输入源),对应于coreFoundation框架中的CFRunLoopSourceRef。
事件源分类
- source0:非基于Port的,用于用户主动触发的事件。诸如UIEvent(触摸,滑动等),performSelector这种需要手动触发的操作。
- source1:基于Port的系统内核事件,可以通过内核和其他线程相互发送消息 。
问题六:
线程之间怎么进行通讯?
- (void)viewDidLoad {
[super viewDidLoad];
isFinished = YES;
}
- (void)timerMethod{
static NSInteger count = 0;
NSLog(@"count = %ld",count ++);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSThread *thread = [[NSThread alloc]initWithBlock:^{
while (isFinished) {
[[NSRunLoop currentRunLoop]run];
}
}];
[thread start];
[self performSelector:@selector(otherMethod) onThread:thread withObject:nil waitUntilDone:NO];
}
- (void)otherMethod {
for (NSInteger i = 0; i < 10; i++) {
NSLog(@"i = %ld currentThread = %@",i,[NSThread currentThread]);
}
isFinished = NO;
}
线程之间通过Source事件进行通讯。
CFRunloopObserverRef与Runloop
CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变。
可以监听的状态有:
/* Run Loop Observer Activities */
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
};
问题七
autoreleasepool什么时候释放?
盗用一张图,看一下Runloop内部逻辑:
上图地址
- 即将进入Loop,其会调用 _objc_autoreleasePoolPush() 创建自动释放池。
- BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。
ps:问题七答案可以在ibireme 深入理解RunLoop找到。
问题八
AFNetworking为什么要有一个常驻线程?
使用NSURLConnection有几种选择:
A.在主线程调异步接口
若直接在主线程调用异步接口,会有个Runloop相关的问题:当在主线程调用 [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES] 时,请求发出,侦听任务会加入到主线程的 Runloop 下,RunloopMode 会默认为 NSDefaultRunLoopMode。这表明只有当前线程的Runloop 处于 NSDefaultRunLoopMode 时,这个任务才会被执行。但当用户滚动 tableview 或 scrollview 时,主线程的 Runloop 是处于 NSEventTrackingRunLoopMode 模式下的,不会执行 NSDefaultRunLoopMode 的任务,所以会出现一个问题,请求发出后,如果用户一直在操作UI上下滑动屏幕,那在滑动结束前是不会执行回调函数的,只有在滑动结束,RunloopMode 切回 NSDefaultRunLoopMode,才会执行回调函数。苹果一直把动画效果性能放在第一位,估计这也是苹果提升UI动画性能的手段之一。
所以若要在主线程使用 NSURLConnection 异步接口,需要手动把 RunloopMode 设为 NSRunLoopCommonModes。这个 mode 意思是无论当前 Runloop 处于什么状态,都执行这个任务。
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[connection start];
B.在子线程调同步接口
若在子线程调用同步接口,一条线程只能处理一个请求,因为请求一发出去线程就阻塞住等待回调,需要给每个请求新建一个线程,这是很浪费的,这种方式唯一的好处应该是易于控制请求并发的数量。
C.在子线程调异步接口
子线程调用异步接口,子线程需要有 Runloop 去接收异步回调事件,这里也可以每个请求都新建一条带有 Runloop 的线程去侦听回调,但这一点好处都没有,既然是异步回调,除了处理回调内容,其他时间线程都是空闲可利用的,所有请求共用一个响应的线程就够了。
AFNetworking 用的就是第三种方式,创建了一条常驻线程专门处理所有请求的回调事件,这个模型跟 nodejs 有点类似。网络请求回调处理完,组装好数据后再给上层调用者回调,这时候回调是抛回主线程的,因为主线程是最安全的,使用者可能会在回调中更新UI,在子线程更新UI会导致各种问题,一般使用者也可以不需要关心线程问题。
答案来自这里:AFNetworking2.0的源码解析
问题九
怎么创建一个常驻线程?
Runloop应用
推荐文章:
ibireme 深入理解RunLoop
iOS线下分享《RunLoop》by 孙源@sunnyxx
黑幕背后的Autorelease by sunnyxx
iOS Runloop实践(常驻线程)
iOS刨根问底-深入理解RunLoop
Runloop应用举例
iOS RunLoop入门小结