RunLoop

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 *)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 *)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的源码解析

问题九

怎么创建一个常驻线程?

参考ibireme 深入理解RunLoop

Runloop应用

iOS实时卡顿监控

推荐文章:

ibireme 深入理解RunLoop
iOS线下分享《RunLoop》by 孙源@sunnyxx
黑幕背后的Autorelease by sunnyxx
iOS Runloop实践(常驻线程)
iOS刨根问底-深入理解RunLoop
Runloop应用举例
iOS RunLoop入门小结

你可能感兴趣的:(RunLoop)