其他
CADisplayLink前世今生
本文字数:19803字
预计阅读时间:50分钟
用最通俗的语言,描述最难懂的技术
前情描述
code review
的时候说到了CADisplayLink
的一些变化,感触颇深,提到了接口的一些变动,现在就自己的一些理解加上网上文档的查阅对该对象进行以下的说明:测试环境
编译环境:Xcode 13.1 运行设备:iPhone X,iOS 14.7.1,iPhone 13 Pro,iOS 15.5
前情描述
CADisplayLink
的timestamp
属性来检索系统上一帧的时间,比如某个电影程序可能会使用时间戳来计算下一个即将显示的视频帧;一个执行动画的程序可能会使用timestamp
来确定可见对象下一帧出现的位置和方式。duration
提供了在最大每秒帧数(maximumFramesPerSecond
)下帧之间时间量,要计算实际的的帧持续时间,使用targetTimestamp - timestamp
。你可以在应用程序中使用这个值来计算显示的实际帧率,系统显示下一帧的大致时间,并调整绘制行为,以便可以及时显示。preferredFramesPerSecond
来定义每秒的帧数。CADisplayLink
完成显示时,调用invalidate
将它从运行循环中移除,并将其与目标解除关联。CADisplayLink接口
Copyright (c) 2009-2021, Apple Inc.
All rights reserved. */
#import <QuartzCore/CABase.h>
#import <QuartzCore/CAFrameRateRange.h>
#import <Foundation/NSObject.h>
@class NSString, NSRunLoop;
NS_ASSUME_NONNULL_BEGIN
/** 与显示器的垂直同步信号(vsync:vertical synchronization)绑定的定时器的类. **/
API_AVAILABLE(ios(3.1), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macos)
@interface CADisplayLink : NSObject
{
@private
void *_impl;
}
/* 为目的显示创建一个新的CADisplayLink的实例对象,它将在目标对象上调用一个名为sel的签名方法 */
+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
/* 将定时器添加到运行循环和某一模式下;除非暂停,直到移除前它会被垂直同步信号每一次触发
* 也只能被添加到一个运行循环中;但是它可以被添加到多个模式中;当它被添加到一个运行循环
* 时将隐式引用
*/
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;
/* 将定时器从运行循环的某个模式下移除;当它被从最后一个注册的模式中移除的时候它会被
* 隐式的释放
*/
- (void)removeFromRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;
/* 将定时器从所有的运行循环模式中移除(如果目标定时器被隐式引用将被殷式释放)
* 紧接着释放目标对象 */
- (void)invalidate;
/* 获取上一次selector被执行的时间戳 */
@property(readonly, nonatomic) CFTimeInterval timestamp;
/* 获取当前设备的屏幕刷新时间间隔 */
@property(readonly, nonatomic) CFTimeInterval duration;
/* 即将渲染的下一帧的时间戳 */
@property(readonly, nonatomic) CFTimeInterval targetTimestamp
API_AVAILABLE(ios(10.0), watchos(3.0), tvos(10.0));
/* 当为true时,该定时器被暂停触发,初始状态为false */
@property(getter=isPaused, nonatomic) BOOL paused;
/* 定义每次显示定时器触发时必须经过多少个显示帧;默认值时1,这意味着每一个显示帧都会触发
* 目标对象的方法签名。将时间间隔设为2,就会导致每隔一帧触发一次目标对象的签名方法
* 当设置小于1的时候就是未被定义
*/
@property(nonatomic) NSInteger frameInterval
API_DEPRECATED("preferredFramesPerSecond", ios(3.1, 10.0),
watchos(2.0, 3.0), tvos(9.0, 10.0));
/* 定义定时器回调所需的回调率,单位时帧/秒,如果设置为0,定时器将以硬件的刷新频率被触发
* 定时器将在硬件的刷新频率下做最大能力的回调
*/
@property(nonatomic) NSInteger preferredFramesPerSecond
API_DEPRECATED_WITH_REPLACEMENT ("preferredFrameRateRange",
ios(10.0, API_TO_BE_DEPRECATED),
watchos(3.0, API_TO_BE_DEPRECATED),
tvos(10.0, API_TO_BE_DEPRECATED));
/* 定义定时器所需要的回调速率的范围,但是时帧/秒。如果该范围包含相同的最小和最大的帧率
该属性将与preferredFramesPerSecond相同,否则实际的回调将被动态的调整以适配动画源*/
@property(nonatomic) CAFrameRateRange preferredFrameRateRange
API_AVAILABLE(ios(15.0), watchos(8.0), tvos(15.0));
@end
如何使用
// ViewController.m
// CADisplayLinkTest
//
// Created by Augus on 2022/3/26.
//
#import "ViewController.h"
static CGFloat const kImageViewWidth = 100.0f;
@interface ViewController ()
@property (nonatomic, strong) CADisplayLink *displayLink;
@property (nonatomic, strong) UIImageView *imageView;
@property (nonatomic, assign) CGFloat dynamicImageViewY;
@property (nonatomic, strong) UIButton *startButton;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = UIColor.lightGrayColor;
_dynamicImageViewY = 0;
[self createImageView];
[self createAnimationButton];
[self createDisplayLink];
}
- (void)createAnimationButton {
_startButton = [UIButton buttonWithType:UIButtonTypeCustom];
_startButton.frame = CGRectMake(200, 200, 100, 100);
[_startButton setTitle:@"start" forState:UIControlStateNormal];
[_startButton addTarget:self action:@selector(pauseAnimation) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:_startButton];
}
- (void)createImageView {
_imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, kImageViewWidth, kImageViewWidth)];
_imageView.image = [UIImage imageNamed:@"kobe0"];
[self.view addSubview:_imageView];
}
/// 创建定时器实例
- (void)createDisplayLink {
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(startAnimation:)];
_displayLink.paused = YES;
_displayLink.frameInterval = 2;
NSLog(@"0--targetTimestamp:%f,timestamp:%f", _displayLink.targetTimestamp,_displayLink.timestamp);
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop]
forMode:NSRunLoopCommonModes];
NSLog(@"1--targetTimestamp:%f,timestamp:%f", _displayLink.targetTimestamp,_displayLink.timestamp);
}
/// 定时器的回调方法
/// @param sender 定时器的实例对象
- (void)startAnimation:(CADisplayLink *)sender {
NSLog(@"2--targetTimestamp:%f,timestamp:%f", sender.targetTimestamp,sender.timestamp);
_dynamicImageViewY++;
if (_dynamicImageViewY == self.view.frame.size.height - kImageViewWidth) {
_dynamicImageViewY = 0;
}
self.imageView.frame = CGRectMake(0, _dynamicImageViewY, kImageViewWidth, kImageViewWidth);
}
/// 暂停动画
- (void)pauseAnimation{
_displayLink.paused = !self.displayLink.paused;
if (_displayLink.paused) {
[_startButton setTitle:@"start" forState:UIControlStateNormal];
} else {
[_startButton setTitle:@"pause" forState:UIControlStateNormal];
}
}
/// 销毁计数器
- (void)stopDisplayLink {
if (_displayLink) {
[_displayLink invalidate];
_displayLink = nil;
}
}
@end
属性说明
timestamp
selector
被执行过一次这个值才会被取到,同理targetTimestamp
,这个属性是用来比较当前图层时间与上一次selector
执行时间只差,从而来计算本次UI应该发生的改变的进度(例如视图做移动效果)。double actualFramesPerSecond = 1 / (_displaylink.targetTimestamp - _displaylink.timestamp);
duration
selector
触发一次才可以取值。值的一提的是,当前iOS设备的刷新频率大都是60HZ。也就是说每16.7ms刷新一次。作用也与timestamp
相同,都可以用于辅助计算。不过需要说明的一点是,如果CPU
过于繁忙,duration
的值是会浮动的。selector
触发的时间间隔是time = frameInterVal * duration
。必须注意的是,selector
执行所需要的时间一定要小于其触发间隔,否则会造成掉帧情况,也就是卡顿情况。举例说明
frameInterval
值为1的时候我们需要保证的是CADisplayLink
调用的target
的函数计算时间不应该大于16.7ms否则就会出现的丢帧现象。mac
应用中我们使用的不是CADisplayLink
而是CVDisplayLink
它是基于C接口的用起来配置有些麻烦,但是用起来还是很简单的。CPU
的空闲程度。如果CPU
忙于其它计算,就没法保证以60HZ执行屏幕的绘制动作,导致跳过若干次调用回调方法的机会,跳过次数取决CPU
的忙碌程度;执行回调方法所用的时间。如果执行回调时间大于重绘每帧的间隔时间,就会导致跳过若干次回调调用机会,这取决于执行时间长短。
preferredFrameRateRange
Info.plist
添加以下字段:CADisplayLink
的回调和 CAAnimation
动画启用全范围刷新率。总结
优势
UI
不断刷新的事件,过渡相对流畅,无卡顿感。劣势
由于和屏幕的刷新频率绑定,如果 CPU
不堪重负影响了刷新频率,那么目标对象的selector
也会受到影响;selector
触发的时间间隔只能是duration
的整数倍;selector
事件如果大于其触发间隔就会造成掉帧现象;不能被继承。
CADisplayLink低配版之NSTimer
NSTimer
。NSTimer
对于iOS开发者并不陌生,因为特性极其接近,所以很多人会混淆,比如他们的初始化,以及销毁和相同的痛点。NSTimer
的一些接口和需要注意的点:Copyright (c) 1994-2019, Apple Inc. All rights reserved.
*/
#import <Foundation/NSObject.h>
#import <Foundation/NSDate.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSTimer : NSObject
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
/// Creates and returns a new NSTimer object initialized with the specified block object. This timer needs to be scheduled on a run loop (via -[NSRunLoop addTimer:]) before it will fire.
/// - parameter: timeInterval The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
/// - parameter: repeats If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
/// - parameter: 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
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
/// Creates and returns a new NSTimer object initialized with the specified block object and schedules it on the current run loop in the default mode.
/// - parameter: ti The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
/// - parameter: repeats If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
/// - parameter: 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
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
/// Initializes a new NSTimer object using the block as the main body of execution for the timer. This timer needs to be scheduled on a run loop (via -[NSRunLoop addTimer:]) before it will fire.
/// - parameter: fireDate The time at which the timer should first fire.
/// - parameter: interval The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
/// - parameter: repeats If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
/// - parameter: 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
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep NS_DESIGNATED_INITIALIZER;
- (void)fire;
@property (copy) NSDate *fireDate;
@property (readonly) NSTimeInterval timeInterval;
// Setting a tolerance for a timer allows it to fire later than the scheduled fire date, improving the ability of the system to optimize for increased power savings and responsiveness. The timer may fire at any time between its scheduled fire date and the scheduled fire date plus the tolerance. The timer will not fire before the scheduled fire date. For repeating timers, the next fire date is calculated from the original fire date regardless of tolerance applied at individual fire times, to avoid drift. The default value is zero, which means no additional tolerance is applied. The system reserves the right to apply a small amount of tolerance to certain timers regardless of the value of this property.
// As the user of the timer, you will have the best idea of what an appropriate tolerance for a timer may be. A general rule of thumb, though, is to set the tolerance to at least 10% of the interval, for a repeating timer. Even a small amount of tolerance will have a significant positive impact on the power usage of your application. The system may put a maximum value of the tolerance.
@property NSTimeInterval tolerance API_AVAILABLE(macos(10.9), ios(7.0), watchos(2.0), tvos(9.0));
- (void)invalidate;
@property (readonly, getter=isValid) BOOL valid;
@property (nullable, readonly, retain) id userInfo;
@end
NS_ASSUME_NONNULL_END
初始化
NSTimer
有7个方法可以为我们提供实例,分3类:以 timer
开头的三个类方法;以 schedule
的三个类方法;以及一个 init
开头的指定构造初始化方法。
timer
开头的两个类方法是灵活度最高的两个方法。这两个方法的不同点在于绑定事件的方式。一个使用NSInvocation
进行转发消息,一个使用target/selector
模式绑定事件。总之就是绑定timer
的触发事件。CADisplayLink
一样我们也需要将它加入到RunLoop
中,之后就可以触发绑定事件了。NSTimer
就一定要加入到RunLopp
中才可以触发绑定事件,你可能会说schedule
开头那两个类方法就不用添加RunLoop
,其实是系统为你将timer
添加到了currentRunLoop
中下的defaultModel
。属性
fireDate
timer
的事件的触发时间,通常我们使用这个属性来做计时器的暂停与恢复:_timer.fireDate = [NSDate distantFuture];
///恢复计时器
_timer.fireDate = [NSDate distantPast];
timeInterval
timer
的事件的触发间隔。tolerance
NSTimer
事件的触发事件是不准确的,完全取决于当前Runloop
处理的时间。如果当前Runloop
在处理复杂运算,则timer
执行时间将会被推迟,直到复杂运算结束后立即执行触发事件,之后再按照初始设置的节奏去执行。当设置tolerance
之后在允许范围内的延迟可以触发事件,超过的则不触发。关于tolerance
的设置,苹果有这么一段介绍:tolerance
的值是timeInterval
的十分之一。valid
timer
是否有效。userInfo
实例方法
fire
fire
并不是立即激活定时器,而是立即执行一次定时器绑定的方法。当加入到RunLoop
中timer
不需要激活即可按照设定的时间触发事件。fire
只是相当于手动让timer
触发一次事件。如果timer
设置的repeat=NO
,则fire
之后timer
立即失效。如果timer
的repeat=YES
,则到了之前设置的时间它依旧会按部就班的触发事件。fire
只是单独触发了一次事件,并不影响原timer
的节奏。invalid
NSTimer
使用的时候如果不注意的话,是会造成内存泄漏的。原因是我们生成实例的时候,会对控制器retain
一下。如果不对其进行管理则ViewController
的永远不会引用计数为零,进而造成内存泄漏。timer
的时候,进行下面操作,而且在合适的时机进行调用:_timer = nil;
timer
实例的时候repeat=NO
,那当触发事件结束后,系统也会自动调用invalid
一次。block
的方法。共同的问题
push
进入一个新的控制器B,然后在B控制器运行一个CADisplayLink
和NSTimer
,进行分别打印各自的方法。@property (nonatomic, strong) GTControllableCThread *augusThread;
@property (nonatomic, strong) NSTimer *augusTimer;
@property (nonatomic, strong) CADisplayLink *augusDisplayLink;
@end
@implementation GTFourController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.title = @"Four";
self.view.backgroundColor = UIColor.whiteColor;
self.augusTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(testTimer) userInfo:nil repeats:YES];
self.augusDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(testLink)];
[self.augusDisplayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)testTimer {
NSLog(@"%s",__func__);
}
- (void)testLink {
NSLog(@"%s",__func__);
}
@end
问题分析
NSTimer
举例(CADisplayLink
是一样的)。_augusTimer
是一个强引用,然后我们的NSTimer
内部对传入的self
控制器B的实例也是一个强引用。_augusTimer
属性的修饰符换成weak
就行了吧?这样以来控制器B对_augusTimer
会是一个弱饮用打破循环。NSTimer
内部会有一个强引用属性target
,它被框架设置为强引用,所以无论你穿入的是什么引用,都不会影响内部的引用问题解决
target
,让中间target
对控制器B是一个弱引用即可,为什么不能对NSTimer
呢?因为框架是闭源的,我们无法修改内部的属性。代码实现
+ (instancetype)proxyWithTarget:(id)target;
@property (nonatomic, weak) id target;
@end
#import "GTProxyTarget.h"
@implementation GTProxyTarget
+ (instancetype)proxyWithTarget:(id)target {
GTProxyTarget *proxy = [GTProxyTarget alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
@end
// 然后控制器B代码修改为
self.augusTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:[GTProxyTarget proxyWithTarget:self] selector:@selector(testTimer) userInfo:nil repeats:YES];
timer
绑定方法不会执行,应该如何修改,为什么?[super viewDidLoad];
// Do any additional setup after loading the view.
// [self createNSTimerOfRunLoop];
// [self createNSTimerOfRunLoopMode];
}
- (void)createNSTimerOfRunLoop {
_timer = [NSTimer timerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer run of runLoop");
}];
// 关于runLoop
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addTimer:self->_timer forMode:NSRunLoopCommonModes];
});
}
- (void)createNSTimerOfRunLoopMode {
_timer = [NSTimer timerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer run of mode");
}];
// 关于runLoop mode
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addTimer:self->_timer forMode:UITrackingRunLoopMode];
[runLoop run];
});
}
其一,子线程的运行循环是默认关闭的,需要手动开启; 其二,运行循环是通过不同的 mode
来处理不同的输入源。
mode
?CADisplayLink
和NSTimer
的底层驱动又是什么?关于RunLoop
RunLoop
Input Source
)提供异步事件,通常是来自另外的线程或者是不同的应用程序的消息;定时器源(Timer Source
)在一个预定的时间或者重复的时间间隔内发生。当事件来到时,这两种类型的源都使用同一种特殊的应用程序进行消息处理。CoreFoundation
里面关于RunLoop
有五个类:CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
CFRunLoopModeRef
接口并没有暴露,只是通过CFRunLoopRef
的接口进行了封装,关系如下:RunLoop
包好若干个Mode
,每个Mode
又包含若干个Souce/Timer/Observer
。每次调用RunLoop
的主函数时,只能指定一个Mode
运行,这个Mode
称作CurrentMode
,如果需要切换Mode
,只能退出当前Loop
,再重新指定一个新的Mode
进入。这样做就是为了分隔开不同组的Source
/Timer
/Observer
,让其互不影响。CFRunLoopSourceRef
souce0
和source1
。source0
:只包含一个回调函数指针,并不能主动触发事件,你要先调用CFRunLoopSourceSignal(source)
来将这个source
标记为待处理,然后手动调用CFRunLoopWakeUp(runloop)
来唤醒RunLoop
,让其处理这个事件;source1
:包含一个mach_port
和一个回调函数指针,被用于通过内核和其他线程相互发送消息。能主动唤醒RunLoop
线程。
CFRunLoopTimerRef
NSTimer
是toll-free bridged
的,可以混用。其包含一个时间长度和一个回调函数指针。当其加入到RunLoop
时,RunLoop
会注册对应的时间点,当时间点到时,RunLoop
会被唤醒以执行那个回调。CFRunLoopObserverRef
Observer
都包含了一个回调函数指针,当RunLoop
的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};
Source
/Timer
/Observer
被统称为mode item
,一个item
可以被同时加入多个mode
。但一个item
被重复加入同一个mode
时是不会有效果的。如果一个mode
中一个item
都没有,则RunLoop
会直接退出,不进入循环。NSTimer & CADisplayLink
NStimer
本质就是CFRunLoopTimerRef
,它们之间是toll-free bridged
的。一个NSTimer
注册到RunLoop
后,RunLoop
会为其重复的时间点注册好事件。例如13:05, 13:10, 13:15这几个时间点。RunLoop
为了节省资源,并不会在非常准确的时间点回调这个Timer
。Timer
有个属性叫做Tolerance
(允许误差时间),标识了当时间点到后,容许有多少最大误差。NSTimer
是用XNU
内核的mk_timer
驱动的,timer
的部分源码,以下是CFRunLoop.c
的宏定义。#define USE_DISPATCH_SOURCE_FOR_TIMERS 1
#define USE_MK_TIMER_TOO 1
#else
#define USE_DISPATCH_SOURCE_FOR_TIMERS 0
#define USE_MK_TIMER_TOO 1
#endif
CADisplayLink
是一个和屏幕刷新率一致的定时器,和NSTimer
并不一样,其内部实际是操作了一个Source1
。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和NSTimer
相似),造成界面卡顿的感觉。CFRunLoopMode
Mode
kCFRunLoopDefaultMode | App 的默认Mode ,通常主线程是在这个Mode 下运行的 |
UITrackingRunLoopMode | Mode ,用于ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode 影响 |
UIInitializationRunLoopMode | App 时第进入的第一个Mode ,启动完成后就不再使用。 |
GSEventReceiveRunLoopMode | Mode ,通常用不到 |
kCFRunLoopCommonModes | Mode ,没有实际作用。 |
Mode
有两个:kCFRunLoopDefaultMode (NSDefaultRunLoopMode)
和UITrackingRunLoopMode
。Mode Name
来操作其对应的Mode
。Common
标记的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes)
,其本质就是UITrackingRunLoopMode | kCFRunLoopDefaultMode
。PerformSelecter
NSObject
的performSelecter:afterDelay:
后,实际上其内部会创建一个Timer
并添加到当前线程的RunLoop
中。所以如果当前线程没有RunLoop
,则这个方法会失效。performSelector:onThread:
时,实际上其会创建一个Timer
加到对应的线程去,同样的,如果对应线程没有RunLoop
该方法也会失效。现在的CADiplayLink有什么不同?
ProMotion
设备的渲染事件的驱动方式发生了变化,由之前的跟屏幕的Vsync
绑定更新为了由UIKit
内部的一个Source0
信号驱动回调,初始化,添加等方式跟之前版本一致,接下来我们就一一验证。DisplayLink
驱动方式的变化
CADisplayLink
回调方法上设置断点,分别在iPhone 7 iOS 14和iPhone 13 Pro iOS 15设备运行。在iPhone 7 iOS 14上, CADisplayLink
是通过source 1 mach_port
直接接受VSync
信号驱动的:
在iPhone 13 Pro iOS 15上, CADisplayLink
是由一个UIKit
的Source0
驱动:在iOS 15上, CADisplayLink
第一次创建并添加到RunLoop
的时候,会注册一个source1
信号,这和iOS 14保持一致:
其中 callout
回调对应符号同样为IODispatchCalloutFromCFMessage
:
Vsync
信号确实会唤醒一次RunLoop
,只是这次唤醒不一定触发DisplayLink
的回调,这就说明IODispatchCalloutFromCFMessage
行为和iOS 14相比发生了某种变化。源码验证
QuarcCore
动态库的二进制文件,使用ida
打开,在左侧方法列表搜索CA::Display::TimerDisplayLink::
定位到CA::Display::DisplayLink::dispatch_items
。up
)的调用函数,点击确认:CA::Display::TimerDisplayLink::callback
的实现:CA::Display::DisplayShmemInfo::power_state
返回了1才会去触发CA::DisplayLink::dispatch_items
,否则后续不会触发,而这个方法也就是不同系统差异的所在。如何解决
CADisplayLink
中回调中确认duration
参数,得到当前屏幕的实时的刷新率,并修改preferredFrameRateRange
进行跟踪:// CAFrameRateRange.minimum 传最小值10.0,preferred 传0.0,让该CADisplayLink只用于监控当前的系统帧率,而不影响帧率的动态选择
_displayLink.preferredFrameRateRange = CAFrameRateRangeMake(10.0, currentFPS, 0.0);
参考文档
https://developer.apple.com/documentation/quartzcore/cadisplaylink?language=objc https://blog.ibireme.com/2015/05/18/runloop/ https://developer.apple.com/documentation/quartzcore/optimizing_promotion_refresh_rates_for_iphone_13_pro_and_ipad_pro?language=objc https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html https://blog.csdn.net/ByteDanceTech/article/details/123437098