美文网首页
iOS 底层 - 内存管理之定时器

iOS 底层 - 内存管理之定时器

作者: 水中的蓝天 | 来源:发表于2020-04-14 15:30 被阅读0次

    本文源自本人的学习记录整理与理解,其中参考阅读了部分优秀的博客和书籍,尽量以通俗简单的语句转述。引用到的地方如有遗漏或未能一一列举原文出处还望见谅与指出,另文章内容如有不妥之处还望指教,万分感谢 !

    在OC范围内所有用到定时器的最终实现都是把Timer事件添加到RunLoop !。

    OC定时方案:

    NSTimer

    NSTimer是最为常见的一种定时方式,根据官方文档可以知道它是通过添加到Runloop中被触发执行任务的,桥接CFRunLoopTimerRef

    Timers work in conjunction with run loops. Run loops maintain strong references to their timers

    简单应用示例

    - (void)viewDidLoad {
        [super viewDidLoad];
        
     //ios 10 之前
        //方式一:scheduledTimerWithTimeInterval方法内部会启动Runloop,自动开始定时任务
        self.timer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
    
        //方式二:timerWithTimeInterval接口,就需要自己加入runloop
        self.timer = [NSTimer timerWithTimeInterval:5 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
        // 或者 延迟3s开始
        NSTimeInterval timeInterval =  3;
        NSDate *newDate = [NSDate dateWithTimeIntervalSinceReferenceDate:timeInterval];
        self.timer = [[NSTimer alloc] initWithFireDate:newDate interval:5 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
        
    //    ios 10 之后
        __weak typeof(self) weakSelf = self;
        self.timer = [NSTimer timerWithTimeInterval:timeInterval repeats:YES block:^(NSTimer * _Nonnull timer) {
            [weakSelf timerTest];
        }];
        [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
    
        self.timer = [[NSTimer alloc] initWithFireDate:newDate interval:timeInterval repeats:YES block:^(NSTimer * _Nonnull timer) {
            [weakSelf timerTest];
        }];
        [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
    
        self.timer = [NSTimer scheduledTimerWithTimeInterval:5.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
            [weakSelf timerTest];
        }];
    }
    
    - (void)timerTest
    {
        NSLog(@"%s", __func__);
    }
    
    

    使用注意:

    • NSTimer依赖于RunLoop,如果RunLoop的任务过于繁重(运行循环每循环一圈的时间是不确定的,假如1.0秒钟后需要处理定时任务,但恰好时间马上要到的时候RunLoop因为处理其他任务没有结束;那就只能等当前任务处理完了才能处理定时任务,这样时间就变的不准确了),可能会导致NSTimer不准时。所以不能够保证计时精准度。
    • 为了可以在屏幕滚动时还可以正常使用NSTimer, RunLoop的Mode需设置为NSRunLoopCommonMode
    • 在使用IOS10之前 NSTimer使用结束需要手动invalidate且最好不要在targetdealloc函数调用
    • 如果定时器和target相互引用就会引发循环引用,会造成内存泄露。一直泄露最终会导致程序crash ;
      解决办法:
    1. IOS10之后使用block的方式创建定时器可以有效避免循环引用,不需要手动调用invalidate
    2. NSTimer可以使用代理对象--NSProxy,作为中间对象来避免循环引用

    block方式介绍 : The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references ( 计时器的执行主体;在执行时,计时器本身作为参数传递给这个块,以帮助避免循环引用)

    IOS10之前
    [self.timer invalidate];
    self.timer = nil;
    

    CADisplayLink

    CADisplayLink继承自NSObject,是一个基于显示屏幕刷新周期的定时器,保证调用频率和屏幕的刷帧频率一致,60FPS(帧每秒-frames/s);同样是基于RunLoop官方文档

    • 因为同步屏幕刷新频率,屏幕刷新后立即回调,因此很适合跟 UI 相关的定时绘制操作,像进度条FPS等等,这样就无须进行多余运算
    // 1.初始化
    self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLink:)];
    // 2. 设置 - 2桢回调一次,这里非时间,而是以桢为单位
    self.displayLink.frameInterval = 2; //iOS10之前
    self.displayLink.preferredFramesPerSecond = 30; //iOS10及之后
    
    // 3.加入RunLoop
    [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
    //
    // 4.callback
    - (void)displayLink:(CADisplayLink *)displayLink {
        ... ...
    // 5.时间累计:每次回调的间隔时间
        self.accumulator += displayLink.duration * displayLink.frameInterval; //粗略计算,因为可能碰到大量工作等导致间隔时间bu zhu
    // 或者🔥
        if (self.timestamp == 0) {
            self.timestamp = displayLink.timestamp;
        }
        CFTimeInterval now = displayLink.timestamp; // 获取当前的时间戳
        self.accumulator += now - self.timestamp;
        self.timestamp = now;
    // 6.预计下一次时间
        NSTimeInterval next = displayLink.targetTimestamp; //iOS10及之后
    }
    ... ...
    // 7.暂停
    self.displayLink.paused = YES;
    // 8.销毁
    [self.displayLink invalidate];
    self.displayLink = nil;
    

    使用注意:

    • 和NSTimer一样CADisplayLink依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致CADisplayLink不准时所以不能够保证很高的计时精准度
    • 同样 CADisplayLink 会对 target 持有,所以记得进行释放,以免造成内存泄露
      解决办法:可以使用代理对象--NSProxy,作为中间对象来避免循环引用

    GCD : Dispatch Sources、dispatch_after()

    GCD 功能非常强大,今天只了解 Dispatch Sources 中的定时器, 且是不依赖RunLoop

    Dispatch Sources 替换了处理系统相关事件的异步回调函数。配置一个dispatch source,需要指定要监测的事件(DISPATCH_SOURCE_TYPE_TIMER等)、dispatch queue、以及处理事件的代码(blockC函数)。当事件发生时,dispatch source会提交对应的blockC函数到指定的dispatch queue执行

    Dispatch Sources 监听系统内核对象并处理,通过系统级调用;会比NSTimer更精准一些。

    // 1. 创建 dispatch source,并指定检测事件为定时  
    //创建串行队列
    dispatch_queue_t queue = dispatch_queue_create("XYHTimer", DISPATCH_QUEUE_SERIAL);
    
    //创建定时器
    DISPATCH_SOURCE_TYPE_TIMER:定时任务
    handle :是否要监视的底层系统句柄
    mask:指定需要哪些事件的标志掩码。这个参数的解释由类型参数中提供的常量决定。
    queue:队列
    
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    
    // 2. 设置定时器启动时间,间隔,容差
     uint64_t start = 2.0; // 2秒后开始执行
     uint64_t interval = 1.0; // 每隔1秒执行
     start * NSEC_PER_SEC //纳秒
     dispatch_source_set_timer(timer,
                                  dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
                                  interval * NSEC_PER_SEC, 0);
    // 3. 设置回调
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"666 - %@", [NSThread currentThread]);
        });
    dispatch_source_set_cancel_handler(timer, ^{
           //取消定时器时一些操作
        });
    // 4.启动定时器-刚创建的source处于被挂起状态
    dispatch_resume(timer);
    // 5.暂停定时器
    dispatch_suspend(timer);
    // 6.取消定时器
    dispatch_source_cancel(timer);
    timer = nil;
    

    使用注意:

    • 与其他 dispatch objects 一样,dispatch sources 也是引用计数数据类型,在 ARC 中无需手动调用 dispatch_release(timer)

    • dispatch_suspend(timer)timer挂起,若是此时已经在执行 block,继续完成此次 block,并不会立即停止

    • dispatch source在挂起时,直接设置为nil 或者 其它新源都会造成crash,需要在activate的状态下调用dispatch_source_cancel(timer)后再重新创建

    • dispatch_source_set_timer中设置启动时间,dispatch_time_t可通过两个方法生成:dispatch_timedispatch_walltime

      a. `dispatch_time`创建相对时间,基于`mach_absolute_time`,CPU的时钟周期数`ticks`,这个`tricks`在每次手机重启之后,会重新开始计数,而且iPhone锁屏进入休眠之后ticks也会暂停计数,`mach_absolute_time()`不会受系统时间影响,只受设备重启和休眠行为影响。
    
     b. `dispatch_walltime`类似创建绝对时间,当设备休眠时,时间依然再走
    

    所以这两者的区别就是,当设备休眠时,dispatch_time停止运行,dispatch_walltime继续运行,所以如果一个事件处理是在30分钟之后,运行5分钟后,设备休眠20分钟,两个时间对应的事件触发点如下:

    两者的区别.png
    • dispatch_source 可内部持有也可外部持有,内部持有可在事件处理block中进行有条件取消
    dispatch_source_set_event_handler(timer, ^{
            NSLog(@"dis timer fired ^_^");
            if (条件满足) {
                dispatch_source_cancel(timer);
            }
        });
    
    • 可以通过dispatch_set_target_queue(timer, queue)更改事件处理的所在队列,修改 dispatch source 是异步操作,所以不会更改已经在执行的事件

    • dispatch_resumedispatch_suspend调用需一一对应,重复调用dispatch_resumecrash

    • 若是想像 NSTimer 实现 Nonrepeating Timer(不要重复计时),则使用 dispatch_after

    • 可以对该功能进行一个封装:dispatch_source定时器封装

    高精度定时方案:

    以上的几种定时方案都会受限于苹果为了保护电池提高性能采用的策略从而导致有延迟现象,像NSTimer会有50~100毫秒的误差,若的确需要使用更高经度的定时器(误差在0.5毫秒以内),一般在多媒体操作方面可能会需要,苹果官方同样也提供了方法给开发者;可以阅读高精度定时文档

    高精度定时用到的比较少,一般视频或者音频相关的数据流操作中会需要。

    其实现原理:使定时器的线程优先于系统上的其他线程;在无多线程冲突的情况下,定时器的任务会被优先处理;
    但也需要注意:不要创建大量的实时线程,一旦某个线程也要被优先处理,结果就是实时线程都失败

    实现高精度定时的两种方法:

    名词解释

    ULL : unsiged long long 64bit
    在16进制数据后面加上数据类型限制,因为默认的数据类型是int,加数据类型是为了防止数据越界。
    32和64位下long long int总是64位的, long int总是32位的。
    nanos:豪微秒,简称微秒
    abs: 求绝对值

    Don't use a high precision timer unless you really need it. They consume compute cycles and battery. There can only be a limited number of high precsion timers active at once. A high precision timer is "first in line", and not every timer can be first. When too many try, all the timers lose accuracy.

    Example applications that are candidates for high precision timers are games that need to provide precise frame rates, or daemons that are streaming data to hardware with limited buffering, such as audio or video data.

    除非你真的需要,否则不要使用高精度的计时器。它们消耗计算周期和电池。
    一次只能激活有限数量的高精确定时器。高精度定时器是“排在第一位的”,并不是每个定时器都能排在第一位。当太多的尝试,所有的定时器失去准确性。

    • 适合使用高精度计时器的应用程序包括:
      需要提供精确帧率的游戏
      将数据流到具有有限缓冲的硬件(如音频或视频数据)的守护进程。

    • 使用Mach Thread API 把定时器所在的线程,移动到高优先级的线程调度即实时调度类

    #include <mach/mach.h>
    #include <mach/mach_time.h>
    #include <pthread.h>
    
    void move_pthread_to_realtime_scheduling_class(pthread_t pthread)
    {
        mach_timebase_info_data_t timebase_info;
        mach_timebase_info(&timebase_info);
     
        const uint64_t NANOS_PER_MSEC = 1000000ULL;
        double clock2abs = ((double)timebase_info.denom / (double)timebase_info.numer) * NANOS_PER_MSEC;
        
        //设置线程策略
        thread_time_constraint_policy_data_t policy;
        policy.period      = 0; //周期值 0表示没有固定周期
        policy.computation = (uint32_t)(5 * clock2abs); // 5 ms of work
        policy.constraint  = (uint32_t)(10 * clock2abs);//最大时间量
        policy.preemptible = FALSE; //是否可以中断
        
        //开始计时
        int kr = thread_policy_set(
                                   
                       pthread_mach_thread_np(pthread_self()),
                       THREAD_TIME_CONSTRAINT_POLICY,
                       (thread_policy_t)&policy,
                       THREAD_TIME_CONSTRAINT_POLICY_COUNT
                                   
                       );
        
        if (kr != KERN_SUCCESS) {//计时失败就退出
            mach_error("thread_policy_set:", kr);
            exit(1);//退出
        }
        
    }
    
    
    • 使用更精确的计时API mach_wait_until(),如下代码使用mach_wait_until()等待10秒
    #include <mach/mach.h>
    #include <mach/mach_time.h>
    
    static const uint64_t NANOS_PER_USEC = 1000ULL;
    static const uint64_t NANOS_PER_MILLISEC = 1000ULL * NANOS_PER_USEC;
    static const uint64_t NANOS_PER_SEC = 1000ULL * NANOS_PER_MILLISEC;
     
    static mach_timebase_info_data_t timebase_info;
     
    static uint64_t abs_to_nanos(uint64_t abs) {
        return abs * timebase_info.numer  / timebase_info.denom;
    }
     
    static uint64_t nanos_to_abs(uint64_t nanos) {
        return nanos * timebase_info.denom / timebase_info.numer;
    }
     
    
    void example_mach_wait_until(int argc, const char * argv[])
    {
        mach_timebase_info(&timebase_info);
        
        //10秒
        uint64_t time_to_wait = nanos_to_abs(10ULL * NANOS_PER_SEC);
        
        /**
         mach_absolute_time()的意义:
         
        Note: The ability to convert between real time and Mach absolute time is also useful when calling mach_wait_until and thread_policy_set with the THREAD_TIME_CONSTRAINT_POLICY policy.
        注意:在使用THREAD_TIME_CONSTRAINT_POLICY策略调用mach_wait_until和thread_policy_set时,在实时时间和Mach绝对时间之间进行转换的能力也很有用。
         */
        uint64_t now = mach_absolute_time();
        mach_wait_until(now + time_to_wait);
    }
    

    以上就是本人已知的定时器,虽然提供方法各不相同,但它们的内核代码是相同的

    • NSTimer:使用频率较高,需要注意避免NSTimer停止工作问题出现,加入RunLoop的Mode时设置NSRunLoopCommonMode,定时器停止操作需要手动实现invalidate,否则会引起内存泄露;同时要避免出现循环引用的发生
    • CADisplayLink: 如有需要和屏幕显示刷新同步的定时任务,可以选择会省去多余的计算
    • GCD定时的精度比NSTimer高一些,使用时需要将任务添加到队列中即可;应用在对文件资源定期读写操作会是个不错的选择;使用注意⚠️:需要 dispatch_resumedispatch_suspend 配套使用,如需要给dispatch source设置新值或者置空时,必须先调用dispatch_source_cancel(timer)
    • Mach Thread: 使用频率较少,缺点:开发难度大、耗电、CUP消耗大,一般多应用在视频流和音频流处理;

    参考

    盲果冻

    相关文章

      网友评论

          本文标题:iOS 底层 - 内存管理之定时器

          本文链接:https://www.haomeiwen.com/subject/zsiamhtx.html