• NSTimer运行机制和线程问题


    A、首先要理解NSTimer运行机制和Runloop之间的关系:

    1.IOS的Run Loops机制

    Run Loops是线程的基础部份,任何线程,包括主结程,都包含了一个run loop对象,Cocoa和CoreFoundation层都有对应的Run Loop实现。

    Run loop 对线程的作用,就是用来控制当有事件需要处理的时候,让线程快速响应,而当没有工作的时候,线程改为休息。

    本质上Run Loop是一个While死循环。不停地监听事件以及处理事件。我们可以自己写一个While循环来做到这点,但是苹果的封装显然会更好。比如可以有不同的运行模式、不同的接收源和定时源,不工作的时候休息等。

    在一个应用的主函数中:

    1 int main(int argc, char * argv[])
    2 {
    3     @autoreleasepool {
    4         return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    5     }
    6 }

    UIApplicationMain()函数就会启动一个主线程,并且自动为它设置一个Run Loop对象,但对除此以外,其它的线程需要明确去设置和启动。

    2.适用场景


    只有创建一个附属线程的时候,才需要明确去运行一个Run Loop.而且是在确定需要的时候才去设置和启动它,否则就必要了。

    例如,开一个线程去执行一个明确的长时间的任务,就没有必要。起用Run Loop主要还是为了跟创建的线程可以有更多的交互。

    而使用Run Loop一个明显的好处就是:节约计算资源,同时也就节约用电了。因为在没有触发的时候线程是处于休眠状态的,不会消耗CPU资源。

    3.结构


    一个Run Loop可以接收的事件类型有两种:一种是输入(Input Source);一种是时间资源(time source).前者异步传递事件的,通常消息是来自其它线程或应用发送的;而后者是同步事件的,比如:定时计划,或者定时重复的工作。


    runloop.jpg

    4.观察者


    Run Loop对象的循环过程中可以添加观察者对象。整个Run Loop 在运行过程中发生的事件具体如下:

    1. 通知观察者Run Loop开始了;
    2. 通知观察者任何预设好的时间已经触发;
    3. 通知观察者任何接入端口的输入源准备被触发;
    4. 触发任何非端口方式接入的输入源;
    5. 如果有任何一个基于端口方式的输入淅准备被触发,立即运行被时间,并且跳到9步;
    6. 通知观察者线程准备休息;
    7. 让线程休息,直到以下任何一个事件发生:
      1. 任何一个基于端口的输入淅有事件发生;
      2. 任何一个计时器触发;
      3. 该Run Loop设置的过期时间过时了;
      4. 该Run Loop被明确地唤醒;
    8. 通知观察者线程被唤醒;
    9. 运行待触发的事件:
      1. 如果一个用户自定义的计时器被触发,运行该计时器的事件,并重新运行Run Loop.跳转到第2步;
      2. 如果一个输入源被触发,传递该事件;
      3. 如果该Run Loop被明确唤醒并且没有超时,重新运行Run Loop循环。跳转到第2步;
    10. 通知观察者该Run Loop循环已经退出。

    输入源(Input Source)


    输入源主要有三种:

    • 基于端口的输入源
    • 自定义输入源
    • selector源

    其中selector源就是常用的“performSelector...”方法。

    定时源


    定时源adpujiu是常用的NSTimer,定时器类;基机制也是基于Run Loop运行的,只是在指定的间隔时间发送消息给需要处理的回调方法。
    两种方法:

    1 NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:4.0 targer:self selector:@selector(fireTimer:) userInfo:nil repeats:YES];
    2 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

    以及

    1 [NSTimer scheduledTimerWithTimerInterval:10
    2                                    terget:self
    3                                    selector:@selector(fireTimer:)
    4                                    userInfo:nil
    5                                    repeats:YES];

    不过需要注意的是,如果Run Loop没有监视定时器相关模式,那么定时器将不会运行。

    如果定时器开始时,Run Loop正在处理前面的事件,那么它会等Run Loop处理完了才开始。如果Run Loop不再运行,那么定时器也永远不再启动了。

    使用方式


    启动方式

    • 无条件启动;
    • 设置超时时间启动,如RunUntilDate方法;
    • 指定某种模式下启动,如RunMode:beforDate:方法;

    能出方法

    • 启动时设定好设定超时时间;
    • 显式的停止Run Loop(调用CFRunLoopStop函数);

    启动模式

    可用模式有 5 种,一般常用的都是default;

    • default
    • connection
    • modal
    • event tracking
    • common modes

    例子


    在新线程中,注册一个Run Loop的观察者,监听每次循环过程的所有事件;同时启动一个定时器;

    从日记中可以看到整个Run Loop 的过程,包括启动和结束、每次定时器唤醒时前后的事件、没有任何任务进入休眠的状态。

    由此可以看出,Run Loop能更加精细地跟整个线程的运行过程交互。

     1 - (void)viewDidLoad {
     2     [super viewDidLoad];
     3     // 在新线程中运行:
     4     [self performSelectorInBackground:@selector(testRunLoop) withObject:ni;];
     5 }
     6 - (void)testRunloop {
     7    // 获取当前线程的Run Loop
     8     NSRunLoop *myRunLoop = [NSRunLoop currentRunLoop];
     9     // 创建一个Run Loop 观察者对象;观察事件为每次循环的所有活动;
    10     CFRunLoopObserverContext context = {0, (__bridge void *)(self), NULL, NULL, NULL};
    11     CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
    12     kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);
    13     if (observer)
    14     {
    15         // 将Cocoa的NSRunLoop类型转换成Core Foundation的CFRunLoopRef类型
    16         CFRunLoopRef cfLoop = [myRunLoop getCFRunLoop];
    17         // 添加观察才对象到该Run Loop 上
    18         CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
    19     }
    20     // 创建定时器,每0.1秒触发
    21     [NSTimer scheduledTimerWithTimeInterval:0.1 target:self
    22     selector:@selector(doFireTimer:) userInfo:nil repeats:YES];
    23     // 重复启动Run Loop 5次
    24     NSInteger loopCount = 5;
    25     do {
    26     //启动 Run Loop 开始循环,直到指定的时间才结束,这里就是持续1秒;
    27     //当调用RunUnitDate方法时,观察者检测到循环已经启动,开始根据循环的各个阶段的事件,调用上面注册的myRunLoopObserver回调函数。
    28     [myRunLoop runUntiDate:[NSDate dateWithTimIntervalSinceNow:1]];
    29     // 运行完之后,会再一次调用回调函数,状态是KFRunLoopExit,表示循环结束。
    30     loopCount--;
    31     } while(loopCount);
    32     NSlog(@"The End.");
    33 }
    34 - (void)doFireTimer:(NSTimer *)timer
    35 {
    36     NSLog(@"fire timer");
    37 }
    38 // Run loop观察者的回调函数:
    39 void myRunLoopObserver(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    40     switch (activity) {
    41         case kCFRunLoopEntry:
    42             NSLog(@"run loop entry");
    43             break;
    44         case kCFRunLoopBeforeTimers:
    45             NSLog(@"run loop before timers");
    46             break;
    47         case kCFRunLoopBeforeSources:
    48             NSLog(@"run loop before sources");
    49             break;
    50         case kCFRunLoopBeforeWaiting:
    51             NSLog(@"run loop before waiting");
    52             break;
    53         case kCFRunLoopAfterWaiting:
    54             NSLog(@"run loop after waiting");
    55             break;
    56         case kCFRunLoopExit:
    57             NSLog(@"run loop exit");
    58             break;
    59         default:
    60             break;
    61     }
    62 }
    代码

    B、理解run loop后,才能彻底理解NSTimer的实现原理,也就是说NSTimer实际上依赖run loop实现的。

    先看看NSTimer的两个常用方法:

    1 + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo; //生成timer但不执行
    2 
    3 + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo; //生成timer并且纳入当前线程的run loop来执行

    NSRunLoop与timer有关方法为:

    - (void)addTimer:(NSTimer *)timer forMode:(NSString *)mode; //在run loop上注册timer

    主线程已经有run loop,所以NSTimer一般在主线程上运行都不必再调用addTimer:。但在非主线程上运行必须配置run loop,该线程的main方法示例代码如下:

    - (void)main
    
    {
    
      NSTimer *myTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timer:) userInfo:nil repeats:YES];
    
      NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    
      [runLoop addTimer:myTimer forMode:NSDefaultRunLoopMode]; //实际上这步是不需要,scheduledTimerWithTimeInterval已经纳入当前线程运行。如果使用timerWithTimeInterval则需要
    
      while (condition)
    
        [runLoop run];
    
    }

    实际上这个线程无法退出,因为有timer事件需要处理,[runLoop run]会一直无法返回。解决办法就是设置一个截止时间:

    [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10.0]]; //每隔10秒检查下线程循环条件,当然时间值可以根据实际情况来定。 

    我们通常在主线程中使用NSTimer,有个实际遇到的问题需要注意。当滑动界面时,系统为了更好地处理UI事件和滚动显示,主线程runloop会暂时停止处理一些其它事件,这时主线程中运行的NSTimer就会被暂停。解决办法就是改变NSTimer运行的mode(mode可以看成事件类型),不使用缺省的NSDefaultRunLoopMode,而是改用NSRunLoopCommonModes,这样主线程就会继续处理NSTimer事件了。具体代码如下:

    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timer:) userInfo:nil repeats:YES];
    
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

    大家可以参看博文http://bluevt.org/?p=209,加深理解NSTimer和NSRunLoop的关系。

    以前博文中提到延迟调用的方法,其实就是在当前线程的run loop上注册timer来实现定时运行的。所以如果是在非主线程上使用,一定要有一个run loop。

    1 - (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes;
    2 
    3 - (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay;
  • 相关阅读:
    R语言 which() 、 which.min() 、 which.max() 函数
    R rep() 函数
    R语言 一个向量的值分派给另一个向量
    R语言 sample抽样函数
    超参数 hyperparameters
    随机游走模型(Random Walk)
    随机数
    Lambda 函数与表达式
    static
    变量的申明定义
  • 原文地址:https://www.cnblogs.com/tig666666/p/5465378.html
Copyright © 2020-2023  润新知