洋仔的博客 洋仔的博客
首页
  • 个人心法总结

    • 价值心法
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • iOS基础知识
  • 前端
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 投资体系
  • 毛选
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

洋仔

奋斗的小青年
首页
  • 个人心法总结

    • 价值心法
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • iOS基础知识
  • 前端
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 投资体系
  • 毛选
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 技术文档

  • GitHub技巧

  • Nodejs

  • 博客搭建

  • iOS基础知识

    • iOS底层相关

    • Runloop系列

      • Runloop系列
      • CADisplayLink 与 NSTimer 有什么不同
        • CADisplayLink 与 NSTimer 有什么不同?
        • 一、什么是CADisplayLink
        • 二、NSTimer循环引用原因及解决方案
        • NSTimer创建
        • 循环引用分析
        • 解决方案
          • 方案一:使用更新后的API
          • 方案二: 在当前页面消失的时候释放timer对象
          • 方案三: 中间件方法
          • 方案四: 使用NSProxy类
          • 方案五: 自定义Timer类
          • 方案六:类似系统API,使用block方式
      • 监控runloop卡顿
    • Runtime系列

    • 内存管理系列

    • Block系列

    • 线程系列

    • KVC跟KVO系列以及通知中心

    • UI系列

    • 离屏渲染系列

    • 组件化系列跟架构

    • OC跟webview交互系列

    • 持久化系列

    • APP编译系列

    • APP性能优化系列

    • cocoapods系列

    • swift系列

    • Git系列

    • 网络相关

    • 三方库系列

    • 系统原理

    • 总结系列

    • 算法系列

    • 数据结构系列

  • 前端

  • 技术
  • iOS基础知识
  • Runloop系列
洋仔
2024-09-03
目录

CADisplayLink 与 NSTimer 有什么不同

# CADisplayLink 与 NSTimer 有什么不同?

1.原理不同

CADisplayLink是一个能让我们以和屏幕刷新率同步的频率将特定的内容画到屏幕上的定时器类。CADisplayLink以特定模式注册到runloop后, 每当屏幕显示内容刷新结束的时候,runloop就会向CADisplayLink指定的target发送一次指定的selector消息, CADisplayLink类对应的selector就会被调用一次。

NSTimer以指定的模式注册到runloop后,每当设定的周期时间到达后,runloop会向指定的target发送一次指定的selector消息。

2.周期设置方式不同

iOS设备的屏幕刷新频率(FPS)是60Hz,因此CADisplayLink的selector默认调用周期是每秒60次,这个周期可以通过frameInterval属性设置,CADisplayLink的selector每秒调用次数=60/frameInterval。比如当frameInterval设为2,每秒调用就变成30次。因此,CADisplayLink周期的设置方式略显不便。

NSTimer的selector调用周期可以在初始化时直接设定,相对就灵活的多。

3、精确度不同

iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高。

NSTimer的精确度就显得低了点,比如NSTimer的触发时间到的时候,runloop如果在阻塞状态,触发时间就会推迟到下一个runloop周期。并且NSTimer新增了tolerance属性,让用户可以设置可以容忍的触发的时间的延迟范围。

4、使用场景

CADisplayLink使用场合相对专一,适合做UI的不停重绘,比如自定义动画引擎或者视频播放的渲染。

NSTimer的使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用。

# 一、什么是CADisplayLink

简单地说,它就是一个定时器,每隔几毫秒刷新一次屏幕。

CADisplayLink是一个能让我们以和屏幕刷新率相同的频率将内容画到屏幕上的定时器。我们在应用中创建一个新的CADisplayLink对象,把它添加到一个runloop中,并给它提供一个target和selector在屏幕刷新的时候调用。

一但CADisplayLink以特定的模式注册到runloop之后,每当屏幕需要刷新的时候,runloop就会调用CADisplayLink绑定的target上的selector,这时target可以读到CADisplayLink的每次调用的时间戳,用来准备下一帧显示需要的数据。例如一个视频应用使用时间戳来计算下一帧要显示的视频数据。在UI做动画的过程中,需要通过时间戳来计算UI对象在动画的下一帧要更新的大小等等。

在添加进runloop的时候我们应该选用高一些的优先级,来保证动画的平滑。可以设想一下,我们在动画的过程中,runloop被添加进来了一个高优先级的任务,那么,下一次的调用就会被暂停转而先去执行高优先级的任务,然后在接着执行CADisplayLink的调用,从而造成动画过程的卡顿,使动画不流畅。

# 二、NSTimer循环引用原因及解决方案

# NSTimer创建

第一种创建方式

self.timer = [NSTimer timerWithTimeInterval:1
                                     target:self
                                   selector:@selector(fireHome)
                                   userInfo:nil
                                    repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer
                                  forMode:NSDefaultRunLoopMode];
1
2
3
4
5
6
7
  • 如果在主线程里创建,需要修改下Mode为NSRunLoopCommonModes,不然,当滚动事件发生时,会导致NSTimer不执行,主线程的RunLoop是默认开启的,所以不需要[[NSRunLoop currentRunLoop] run]。
  • 如果在子线程里创建,且当前线程里无滚动事件,则不需要修改Mode,子线程的RunLoop默认不开启的,需要手动加入Runloop:

第二种创建方式

self.timer = [NSTimer scheduledTimerWithTimeInterval:1
                                              target:self
                                            selector:@selector(fireHome)
                                            userInfo:nil
                                             repeats:YES];
1
2
3
4
5

# 循环引用分析

说到循环引用,大家都会想到有weakself替换self去解决,比如上面的写法改为

__weak typeof(self) weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1
                                     target:weakSelf
                                   selector:@selector(fireHome)
                                   userInfo:nil
                                    repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer
                                  forMode:NSDefaultRunLoopMode];
1
2
3
4
5
6
7
8

同事: 为什么timer都弱引用了target,还是释放不了

  • self 强持有 timer 我们都能直接看出来,那么timer是什么时候强持有 self的呢?
  • 截屏2021-10-08 下午4.33.01.png

看苹果官方文档可知:target方法中,timer对self对象进行了强持有,因此造成了循环引用。

  • 但是当我们按照惯例用weakSelf去打破强引用的时候,发现weakSelf没有打破循环引用,timer仍然在运行。

  • 即 self -> timer -> weakSelf -> self。

来看看__weak typeof(self) weakSelf = self;做了什么

869753-26f8fb55fde27308.webp

从上图可知,weakSelf和self两个指针地址不同但内存空间地址相同,也就是两个对象同时持有同一个内存空间.相当于NStimer间接的持有了self,所以weakSelf并没有打破循环关系

** 同事: 不使用属性或者成员变量切断当前类对timer的强引用,可以吧

NSTimer *timer = [NSTimer timerWithTimeInterval:1
                                     target:weakSelf
                                   selector:@selector(fireHome)
                                   userInfo:nil
                                    repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer
                                  forMode:NSDefaultRunLoopMode];
1
2
3
4
5
6
7

timer还是释放不了,原因如下

1335883-872d4c69c289a08f.webp

主线程的Runloop在程序运行期间是不会销毁的,它比self的生命周期都长,也就是runloop引用着timer,timer就不会销毁,timer引用着target,target也不会销毁.从runloop引用着timer这个思路来想,要打破循环,只能从timer引用target这个层面来打破

# 解决方案

通过上面的分析,我们知道需要打破的地方就是timer对self的强引用

截屏2021-10-08 下午5.28.56.png

# 方案一:使用更新后的API

在iOS 10以后系统,苹果针对NSTimer进行了优化,使用Block回调方式,解决了循环引用问题。

  [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
       // Do some things
    }];
1
2
3

# 方案二: 在当前页面消失的时候释放timer对象

使用(void)didMoveToParentViewController:(UIViewController *)parent方法,在这个方法里清掉定时器,就会释放Timer对象,也就解决了强引用。即调用dealloc方法了。

//生命周期  移除childVC的时候
- (void)didMoveToParentViewController:(UIViewController *)parent {
    if (parent == nil) {
        [self.myTimer invalidate];
        self.myTimer = nil;
    }
}
1
2
3
4
5
6
7

缺点: 针对PresentVC不适用

# 方案三: 中间件方法

//定义个中间件属性
@property (nonatomic, strong) id target;

  _target = [NSObject new];
  class_addMethod([_target class], @selector(testTimer), (IMP)timerIMP, "v@:");
    //这里换成_target  不用self了。  这就没有了循环引用了
  self.myTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:_target selector:@selector(testTimer) userInfo:nil repeats:YES];
 [[NSRunLoop currentRunLoop] addTimer:self.myTimer forMode:NSDefaultRunLoopMode];

void timerIMP(id self, SEL _cmd) {
    NSLog(@"Do some things");
}

//停止Timer
- (void)dealloc {
    [self.myTimer invalidate];
    self.myTimer = nil;

    NSLog(@"Timer dealloc");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 方案四: 使用NSProxy类

新建一个类TimerProxy,继承NSProxy,设置一个属性

//注意这里要使用weak
@property (nonatomic, weak) id target;
1
2

实现方法转发

/** 方法签名 */
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}
/** 消息转发 */
- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:self.target];
}

在你需要的地方,然后导入TimerProxy头文件使用
@property (nonatomic, strong) TimerProxy *timerProxy;
    _timerProxy = [TimerProxy alloc];//注意这里只有alloc方法
    //self弱引用所以可以被释放
    _timerProxy.target = self;
    self.myTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:_timerProxy selector:@selector(testTimer) userInfo:nil repeats:YES];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 方案五: 自定义Timer类

  1. 自定义一个类CustomTimer,定义两个属性,包括timer和target,timer就是真实的NSTimer对象,这里的target不在是timer的target,而是我们外面真正实现业务的对象

    @property (nonatomic, strong) NSTimer *timer;
    //target使用weak修饰
    @property (nonatomic, weak) id target;
    
    1
    2
    3

2.self作为timer的target,实现timer回调方法handler:,同时创建一个NSInvocation对象,用于外部真正target的方法调用,invocation通过userInfo参数传递

- (instancetype)initWithTimeInterval:(NSTimeInterval)interval
                                   target:(id)target
                                 selector:(SEL)selector
                                  repeats:(BOOL)repeat
{
    self = [super init];
    if (self) {
        NSMethodSignature *methodSignature = [target methodSignatureForSelector:selector];
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
        invocation.selector = selector;
        invocation.target = target;
        self.target = target;
        self.timer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(handler:) userInfo:invocation repeats:repeat];
    }
    return self;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

3.处理回调 在handler:中判断弱引用属性target是否为空,不为空则通过invocation调用target方法,如果为空则释放timer

- (void)handler:(NSTimer *)timer
{
    NSInvocation *invocation = [timer userInfo];
    if (self.target) {
        [invocation invoke];
    }else{
        [self invalidate];
    }
}

- (void)invalidate
{
    [self.timer invalidate];
    self.timer = nil;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 方案六:类似系统API,使用block方式

@implementation NSTimer (BlockTimer)

+ (NSTimer *)bt_timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void(^)(NSTimer *timer))block{

    return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(handler:) userInfo:[block copy] repeats:repeats];
}

+ (void)handler:(NSTimer *)timer{

    void (^block)(NSTimer *timer) = timer.userInfo;
    if (block) {
        block(timer);
    }
}
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
编辑 (opens new window)
上次更新: 2024/10/23, 23:26:17
Runloop系列
监控runloop卡顿

← Runloop系列 监控runloop卡顿→

最近更新
01
数组
10-25
02
数组双指针系列之对撞指针
10-25
03
数组双指针系列之快慢指针
10-25
更多文章>
Theme by Vdoing | Copyright © 2019-2024 Evan Xu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式