小米电池休眠自动解除(小米电池休眠自动解除打开后盖)

  • 生活
  • 2023-04-19 14:20
背景

Apple在今年推出了支持ProMotion屏幕的iPhone设备,让App在iPhone13Pro和iPhone13ProMax上的最大刷新帧率可到达120Hz,极大优化了应用滑动/动画的流畅度体验。

ProMotion并不是一个新的概念,早在2017年,Apple推出的第二代iPadPro便搭载了这一刷新率最高可达120Hz的屏幕。在iPad上,高刷新率默认对所有App启用。而也许是出于能耗的考虑,在iPhone上,Apple并未将这个能力自动对所有App启用,而是需要开发者手动添加配置项来进行适配。

近期有消息指出iOS15.4beta修正了这一行为(https://www.macrumors.com/2022/01/27/ios-15-4-apps-120-hz-promotion/),经过笔者验证额外的配置项依然是需要的,并且本文内容依然适用。

本文介绍了在iPhone上对ProMotion动态帧率的适配时观察到的现象和遇到的问题,尝试推测了背后的原理,并探讨了解决问题的可能思路,最终基于调研结果在国际化短视频业务上线优化方案,取得了核心业务指标的收益。

什么是帧率

在深入探究ProMotion屏幕所带来的变化之前,我们先回顾一个似乎耳熟能详的概念:

什么是帧率?

众所周知,显示器并不能显示真正动态的画面,所有动画效果都是靠高速播放一帧帧静态画面欺骗人类视觉所造成的假象。那么帧率最基本的定义便是屏幕内容的变化频率,是一个物理意义上的指标。这种变化频率又由以下两个值共同决定:

刷新帧率:由屏幕硬件规格控制,传统显示设备一般为59.94Hz,决定了帧率的上限。渲染帧率:由CPU->GPU渲染管线的执行速率控制,决定了帧率的下限。

理想情况下,渲染帧率和刷新帧率最好完全匹配,或者渲染帧率是刷新帧率的整数倍,这样实际展现的内容不会出现任何异常。但现实中二者往往会出现不匹配的情况,卡顿就是其中之一:

卡顿

当CPU->GPU的渲染管线遇到瓶颈,导致某一帧的渲染耗时大于屏幕的刷新间隔时,上一帧画面会在屏幕上多停留数帧的时间。当这个滞留时间过长,用户感知到画面更新的延迟,这称为卡顿。这也是iOS开发过程中会遇到的主要性能问题之一。

实际帧率

帧率并不等同于刷新率,它和所展示的内容息息相关:

展示静态画面时,理想情况只需要进行一次渲染,尽管屏幕仍然以60Hz或者更高的频率进行刷新,每次刷新所展示的内容(FrameBuffer)也未改变,用户感知到的实际帧率依然接近0。展示固定帧率的元素,例如24FPS的电影视频时,用户感知到的实际帧率自然也是24FPS左右。展示超高帧率的内容,例如CS:GO不锁帧跑>200FPS,但由于显示设备刷新率限制,用户感知到的帧率依然不会超过硬件帧率的上限。什么是动态刷新率

ProMotion本质上是对Adaptive-Sync显示标准的一种实现。

Ref:https://en.***.org/wiki/Variable_refresh_rate

根据Apple官方文档显示,ProMotion屏幕支持的刷新率是可变的。

具体来说,对iPhone而言:

TheiPhone13ProandiPhone13ProMaxProMotiondisplayscanpresentcontentonthedisplayusingthefollowingrefreshratesandtimings:

120Hz(8ms),80Hz(12ms),60Hz(16ms),48Hz(20ms),40Hz(25ms),30Hz(33ms),24Hz(41ms),20Hz(50ms),16Hz(62ms),15Hz(66ms),12Hz(83ms),10Hz(100ms)

而对iPadPro来说:

TheiPadPro’sProMotiondisplaycanpresentcontentonthedisplayusingthefollowingrefreshratesandtimings:

120Hz(8ms),60Hz(16ms),40Hz(25ms),30Hz(33ms),24Hz(41ms)

这其实是Apple对VESA定制的Adaptive-Sync技术标准的一种实现,在游戏业界已经实装多年,类似的实现还有AMD的FreeSync和Nivida的G-Sync。这种新的显示技术有着以下优点:

减少可感知的卡顿

对于固定刷新率的屏幕而言,当某一帧的渲染耗时出现异常,在VSync信号到来之后才完成渲染,那么当前内容便会滞留在屏幕上,这一帧需要再等一次VSync信号才能被渲染展示给用户。

而Adaptive-Sync技术可以避免这一点,在该帧渲染结束后尽快进行展示,从而减少显示卡顿时长:

减少移动设备的屏幕功耗

在搭载了固定刷新率屏幕的设备上,当显示静态内容或者帧率较低(例如视频)的内容时,GPU的渲染频率比实际频率刷新率会更低。但是固定刷新率的屏幕依然会已最高速率进行刷新,重复展示之前的内容,造成了额外的电量消耗。

ProMotion屏幕在这种情况下可以主动降低刷新率,减少屏幕功耗,这对于移动设备来说尤其重要。

动态刷新率的表现形式

TheiPhone13Pro,theiPhone13ProMax,andtheiPadProProMotiondisplaysarecapableofdynamicallyswitchingbetween:

Fasterrefreshratesupto120Hz

Slowerrefreshratesdownto24Hzor10Hz

已知,ProMotion屏幕的刷新帧率并不固定,系统会实时地根据当前显示内容的类型和状态来动态切换屏幕的刷新帧率。为了更好地理解这种动态帧率的表现形式,笔者分别在

iPhoneXR-无ProMotioniPhone13Pro-有ProMotion默认锁频

上对一些典型渲染场景进行了测试,发现搭载了ProMotion屏幕的设备上运行App时,不同的场景下的各种统计口径的帧率指标确实展示出了有趣的变化。

具体而言,笔者分别在以下几种场景:

测试场景静态页面

静态的UIView,无动画/视频等元素

2.滑动中的页面

包含静态Cell的UITableView,仅观察滑动中的表现

3.CoreAnimation默认刷新率动画

显示基于CABasicAnimation实现的简单位移动画

4.CoreAnimation120Hz高刷新率动画

仅在ProMotion设备上测试,基于CABasicAnimation实现的简单位移动画,同时解锁了CADisableMinimumFrameDurationOnPhone和preferredFrameRateRange帧率限制。(关于此限制下文会有具体介绍)

5.Metal渲染30Hz/60Hz视频

使用基于MTKView进行渲染的播放器,播放源帧率分别为30Hz/60Hz的视频文件

并使用以下几种统计口径的帧率指标进行测试:

测试指标CADisplayLink计算帧率

iOS中主要的帧率统计手段。

根据CADisplayLink.h头文件中描述,CADisplayLink是一个”Classrepresentingatimerboundtothedisplayvsync“。在回调中比较当前帧/前一帧的时间戳,可以计算出上一帧的渲染耗时(ts),其倒数(1/ts)即为当前的实时帧率。

2.XcodeGPUReport帧率

Xcode->ShowDebugNavigator->FPS中显示的帧率。这个只能统计当前应用直接通过OpenGLES或者Metal进行绘制的帧率,例如游戏渲染/视频播放,无法统计CoreAnimation的帧率(众所周知,后者通过backboardd进行绘制)。

3.InstrumentsCoreAnimationFPS

Instruments中CoreAnimationFPS工具所显示的帧率。这个统计的是CoreAnimation的帧率,即RenderServerbackboardd绘制的频率。目前该工具有BUG无法显示高于60FPS的帧率。

4.InstrumentsDisplay/VSync信号频率

Instruments中Display工具所显示的Surface/VSync信号时间戳。如下图所示:

Display:指对应显示器的单个Surface上屏持续的时间,对应CPU-GPU管线的渲染频率VSync:指垂直同步信号时间戳,对应屏幕硬件的刷新频率

在60Hz屏幕上,iOS设备默认采用双缓冲刷新机制,也就是前帧缓存和后帧缓存。GPU总是在后帧缓存上进行当前帧的绘制。当VSync信号到来时,交换前后帧缓存的指针(SwapFrameBuffer),屏幕刷新显示新的内容。

而当屏幕以120Hz显示内容时,iOS会切换成三缓冲刷新机制(见上图中三种颜色的Surface),这减少渲染管线的压力,但同时会增加一定的渲染上屏延迟。

Metal应用可以通过设置-[CAMetalLayersetMaximumDrawableCount:]为2来在120Hz屏幕上强制启用双缓冲机制,避免这种延迟。

如果屏幕显示内容未发生变化,Surface则不会发生交换,一个Surface的Display可能持续数个VSync间隔,但多余的VSync信号依然代表着硬件层额外的屏幕刷新,造成额外的电量消耗。

非ProMotion设备

首先让我们看看传统的固定刷新率的设备的情况。

VSync信号间隔固定为16.67ms

XR的屏幕刷新率为固定的60Hz,这一点对应的具体指标是VSync信号的间隔,而在任何场景下,XR的VSync信号的间隔均为固定的16.67ms。

此外,在显示静态内容时,由于视图LayerTree无变化,CoreAnimation不会有提交新的事务提交,backboardd不会进行刷新,所以对应这一帧的Surface也长时间(数十秒)未被交换下去,CoreAnimationFPS的值显示为0。

但由于VSync信号仍然以60Hz的频率持续触发,屏幕此时正在不停重复展示同样的FrameBuffer,消耗了额外的电量。

CADisplayLink基本完全跟随VSync信号

根据过去对iOS系统的认知,我们知道CADisplayLink是由VSync信号驱动的:

默认配置的CADisplayLink的回调应该与VSync信号基本同时。

这一点在XR上得到了验证,用Instruments记录一次主线程发生的卡顿,得到:

其中:

第一行runloop记录每次RunLoopAfterWaiting->BeforeWaiting的间隔第二行tick记录默认配置的CADisplayLink回调间的间隔最下面则是硬件Display/VSync事件时序图

可以观察到下述现象,符合我们之前的对DisplayLink的认识:

没有卡顿的情况下,VSync信号和RunLoop的唤醒&CADisplayLink回调的触发严格一一对应。RunLoop卡顿,无法处理Source1信号,DisplayLink回调被延迟到卡顿结束时。在此过程中VSync信号间隔始终保持不变。ProMotion设备

下面看看ProMotion设备的测试结果。

VSync信号间隔可变

在ProMotion屏幕上VSync信号间隔是可变的,具体而言:

显示静态内容时,屏幕降频,最低以10Hz的频率进行刷新显示CoreAnimation动画时,系统会适配动画的帧率设置改变刷新率

*通过preferredFrameRateRange可以设置hint请求高刷,但并不一定生效,详见下文“动态帧率的应用场景”部分。

显示滑动中内容时,刷新率在80Hz左右波动,并且跟随滑动速度变化而变化。快滑时刷新率升高,慢滑时降低。显示视频时,刷新率和视频帧率维持一致

可以看到VSync信号间隔能主动跟随显示内容的渲染帧率的改变而改变。

减少卡顿造成的显示延迟

在主线程发生卡顿导致滑动中某一帧渲染耗时过长时,系统会改变这一帧所对应的VSync信号间隔(下图Surface5),减小从渲染到展示的延时,从而减缓用户感知到的卡顿时长。

DisplayLink不完全跟随VSync信号

如图是一张滑动中场景的CADisplayLink回调和Display/VSync事件对照记录。和之前不同的是,再ProMotion设备上DisplayLink和VSync信号之间没有表现出明显的跟随关系:

具体而言:

第三个箭头所指向的DisplayLink的回调并不及时。在这之前主线程的卡顿已经结束,并且额外执行了两次RunLoop,但直到第三次才调用了DisplayLink的回调。不仅仅是时机不匹配,也存在收到VSync但不触发DisplayLink回调的情况(并且主线程处于空闲状态),例如上图中的❓处。解除DisplayLink的帧数限制

我们知道,在iOS15上Apple对第三方应用的显示帧率默认做了限制。第三方应用需要在Info.plist中添加<key>CADisableMinimumFrameDurationOnPhone</key><true/>字段才可以解锁120Hz的刷新率。

于此同时,在iOS15中,CADisplayLink等动画相关API也新增了一个用于配置偏好帧率的属性:

/*Definestherangeofdesiredcallbackrateinframes-per-secondforthisdisplaylink.Iftherangecontainsthesameminimumandmaximumframerate,thispropertyisidenticalaspreferredFramesPerSecond.Otherwise,theactualcallbackratewillbedynamicallyadjustedtobetteralignwithotheranimationsources.*/@property(nonatomic)CAFrameRateRangepreferredFrameRateRangeAPI_AVAILABLE(ios(15.0),watchos(8.0),tvos(15.0));

为了进一步探究新设备上DisplayLink和VSync信号之间的关系,笔者将测试App的CoreAnimation的帧率限制解除,并配置对应的API,分别在不同的场景重新进行测试:

显示动态内容的场景动画场景

展示一个速度中等的位移动画,得到下图:

可以很直观地发现,DisplayLink解锁帧率后的屏幕刷新率基本稳定在120Hz。并且VSync和DisplayLink的关系似乎又重新一一对应了起来。

但是,将动画速度减慢,笔者发现这种对应关系发生了变化:

可以观察到在播放慢速动画时,DisplayLink的频率依然是配置的120Hz,但是实际的屏幕刷新率却只有30Hz。

滑动场景

让我们换一种场景再次进行测试,快速滑动视图,在Instruments中得到下图:

可以发现,DisplayLink解锁帧率后,屏幕刷新率同样基本稳定在120Hz,仅在丢帧时有降频。

需要注意的是笔者在CADisplayLink的回调中除了调用os_signpost上报log外无任何UI改动。即便笔者展示的TableView极其简单,上图中仍然可以观察到丢帧,无法在滑动中完美稳定120Hz。这也许说明UIKit的渲染性能在120Hz下会有某种程度上的原生瓶颈。

然后降低滑动屏幕的速度,得到了和慢速动画相似的结果,尽管DisplayLink回调速度不减,但是VSync信号频率一直保持在较低的水平:

卡顿场景

上面两次测试都接近理想情况,即整个RenderLoop执行几乎没有延迟与卡顿。但是现实中应用的运行总是有着各种各样的或大或小的卡顿问题。

为了验证更接近现实情况下,DisplayLink和VSync信号之间的关系,在连续滑动的情况下笔者人为加入了一个20ms的微小卡顿进行测试:

上图中可以看到,ProMotion屏幕很好的处理了这次卡顿,由于三缓冲机制的存在,再RenderLoop渲染Surface4卡顿期间,通过改变VSync间隔,系统尝试将缓冲区中的Surface283与Surface250延迟上屏,尽量缩短了用户看到静止画面的时长。

随后,主线程恢复执行,可以看到DisplayLink的回调频率很快恢复至卡顿前的高水平。而此时VSync信号由于前述卡顿减缓机制的存在频率其实有所降低。此时二者频率并不吻合。

这和之前播放慢速动画/慢速滑动的情况很相似,由于卡顿加上缓冲机制的存在导致短时间内系统将屏幕的刷新频率降低,但在CPU侧依然维持了DisplayLink的高速回调,满足了使用方对preferredFrameRateRange这一API的设置。

为了进一步分析了这种机制的本质,笔者接下来会尝试逆向分析iOS15中的系统库相关实现的改动。

逆向分析DisplayLink驱动方式的变化

在CADisplayLink回调***上设置断点,分别在iOS14和15ProMotion设备上运行,可以得到:

在iOS14上,CADisplayLink是通过Source1mach_port直接接受VSync信号驱动的在iOS15ProMotion设备上,CADisplayLink不再由VSync信号驱动,而是由一个UIKit内部的Source0信号驱动

在15中,CADisplayLink第一次创建并添加至RunLoop的时候,会注册一个Source1信号,这和14中行为一致。

其callout回调地址对应符号为同样为display_timer_callback,同样和14中的一致。

这也可以解释为什么15上VSync信号确实会唤醒一次RunLoop,只是这次唤醒并不一定触发DisplayLink的回调,这就说明display_timer_callback行为和14相比一定发生了某种变化。

display_timer_callback逻辑的变化

使用Hopper分析display_timer_callback的实现,发现15和14的实现并无区别。使用LLDB进行debug,逐步分析,观察到后续调用函数为CA::Display::DisplayLink::callback,其关键反汇编代码如下图所示:

观察反汇编代码可以发现,如果CA::display_link_will_fire_handler这个block返回了NO,则这次VSync信号回调不会触发后续的CA::DisplayLink::dispatch_items调用。

实际上在LLDB中也验证了这点:

注意上图中的_CFRunLoopCurrentIsMain和上图红框代码接近,后续的blraa指令看起来很明显是调用了一个block(上面的ldrx9[x8,#0x10]就是把invoke指针从block结构体中取出的意思)。tbz指令中w0寄存器为block执行的返回值,为0(即NO)时跳转至0x1848dbc08,而0x1848dbc08刚好在dispatch_items的调用之后,跳过了该调用。

通过对上图中blraa指令stepin,我们发现这个block实际上是由UIKitCore注册的:

找到引用了该符号的UIKit的私有***__UIUpdateCycleSchedulerStart,反汇编结果也验证了这点。

同时发现这个block的返回值固定为0x0。

而同样的symbol在之前的iOS版本上并不存在,也就是说这个应该是iOS15的变动。换安装了iOS15的非ProMotion设备,重走上面的逆向流程发现,该设备的CA::display_link_will_fire_handler为nil,未注册:

这里cbz执行了跳转,说明x0为nil,而x0是由ldrx0,[x8,#0x1c8]得到。

可以看到x0就是CA::display_link_will_fire_handler。继续分析之前找到的私有符号__UIUpdateCycleSchedulerStart的相关实现,可以知道这是因为在非ProMotion设备上_UIUpdateCycleEnabled返回了NO导致的。

在返回NO的情况下__UIUpdateCycleSchedulerStart***不会执行,CA::display_link_will_fire_handler也就不会被注册。

_UIUpdateCycleEnabled所带来的变化

继续研究_UIUpdateCycleEnabled相关的代码,笔者发现这个的改动并不是仅仅影响DisplayLink驱动方式那么简单。

当_UIUpdateCycleEnabled返回YES时,UIKit会在UIApplicationMain中执行_UIUpdateCycleSchedulerStart。分析该函数,发现_UIUpdateCycleEnabled启用时会调用[CATransactionsetDisableRunLoopObserverCommits:YES]。

CoreAnimation是绝大部分iOS应用的渲染引擎,熟悉iOS渲染流程的同学想必都知道它的执行也是由MainRunLoop驱动,大致为:

MainRunLoop因为用户操作/Timer/GCD等被唤醒,派发相应的事件/回调回调中应用修改LayerTree,触发setNeedsLayout或setNeedsDisplayMainRunLoop即将完成本次执行,在即将休眠前向Observer派发BeforeWaiting事件BeforeWaiting中触发CoreAnimation注册的MainRunLoopObserver,触发事务提交CA::Transaction::commit():自顶向下触发各种Layout/Display等逻辑,更新布局/内容CoreAnimation将更新后的LayerTree打包发送给RenderServer

5.随后MainRunLoop进入休眠

6.RenderServer将打包好的LayerTree解码,生成并提交对应的drawcalls

7.GPU执行渲染指令,渲染出FrameBuffer,待后续VSync信号来临时上屏展示

上图中+[CATransactionsetDisableRunLoopObserverCommits:YES]这个调用给了笔者提示,让我们验证一下CA::Transaction::commit()在iOS15ProMotion设备上的执行时机,会发现确实不再由BeforeWaiting事件驱动了:

实际上同样的Source0信号同时也驱动了CADisplayLink的回调:

关注这个Source0的回调符号runloopSourceCallback,会发现这个Source0是由signalChanges函数驱动:

而signalChanges又是由多个回调所驱动:

其中:

runloopObserverCallback为一个BeforeWaiting的MainRunLoopobserver驱动。runloopTimerCallback由mk_timer驱动,对应的mach_port不明,测试发现其回调频率在1Hz左右,但也会不断变化,猜测是某种系统计时器。inputGroupSignaledCallback由mk_timer驱动,对应的mach_port正是VSync信号。

4.requestRegistrySignaledCallback由UIScrollView在即将开始滑动时驱动。

通过上面的分析,笔者有理由认为在iOS15上应用的渲染驱动机制出现了比较大的变化。其中之一便是DisplayLink的驱动源的改变。

结论iOS15上Apple改变了在ProMotion设备的渲染事件循环的驱动方式,CoreAnimation的事务提交不再由完全由RunLoop驱动,而是涉及了多个信号源系统动态帧率选择的机制会综合考虑使用方设置的API(如preferredFrameRateRange)和实际展示的内容的变化频率。具体对CADisplayLink而言:内容低速变化时,CADisplayLink解锁高刷新率仅影响自身的回调频率,系统仍可能选择较低的屏幕刷新率来降低功耗内容中高速变化时,CADisplayLink解锁高刷新率可以让系统选择更高的刷新频率,甚至实现锁定120Hz的刷新

关于如何界定低速/中高速,笔者在下文中CAAnimation设置动态帧率部分做了一些试验,可作为参考。

同时,默认配置的CADisplayLink回调频率最高为60Hz,无法监控更高频率的刷新事件。

3.ProMotion设备中,DisplayLink不再由VSync信号直接驱动,而是在新引入的渲染事件循环中执行。新版本iOS系统实现了某种更复杂的机制来尽可能满足使用者设置的偏好频率进行回调,但并不保证它与VSync信号的强关联性。这意味着默认的CADisplayLink的回调频率与实际帧率并不匹配,之前基于CADisplayLink进行帧率监控的方案在ProMotion设备上变得不再可行。

动态帧率的应用场景监控动态帧率下的流畅度表现

业界中一般采用CADisplayLink对应用的流畅度进行监控。由于CADisplayLink的行为在iOS15上的变化,原先的监控方案无法评估ProMotion屏幕在超过60Hz时的表现。

根据上面的探索结论,目前笔者设想了三种针对ProMotion设备的兼容性修改方案:

方案一[Pass]

对于任何设备都以60Hz为优化目标,只考虑刷新间隔长于16.67ms的情况。换句话说,在屏幕以120Hz刷新时,对于丢1帧的情况也认为不丢帧,因为此时两帧之间的间隔仍然小于16.67ms,理论上用户感知不大。

优点:

方案简单,仅需设置preferredFramesPerSecond为固定值60即可兼容之前的指标。依然可以计算FPS指标,对于刷新率高于60Hz的情况统一认为刷新率为60Hz

缺点:

由于只能监控最高60Hz的情况,无法评估更高刷新率下一些微小丢帧对用户体验带来的影响,也无法评估对高刷屏的一些优化所带来的技术影响在低刷新率时,MainRunLoop依然会以60Hz运行,对功耗有一定影响方案二[Pass]

通过一些手段,可以替换驱动display_timer_callback的Source1信号的回调,使用它来准确监听VSync信号,实现对动态帧率的准确监控。

优点:

理论上最精确的监控方案对功耗的影响最小,回调频率只有在屏幕刷新率实际升高时才会随之提升

缺点:

使用了私有APIFPS指标从此不再适用VSync信号目前和渲染流程不完全匹配,虽然精确但不一定实用方案三[Pick]

通过在CADisplayLink回调中确认duration参数,计算得到当前屏幕的实时刷新率,并修改preferredFrameRateRange来进行跟踪。

优点:

方案相对简单,只需在每次回调中更新DisplayLink对象的preferredFrameRateRange属性即可

缺点:

由于动态帧率的存在,FPS指标可以反映实时屏幕刷新情况,但是聚合后的意义不大,消费时需要区分特定机型/场景观察到目前的最小回调频率为60Hz,也就是说无法确认ProMotion屏幕在48Hz、30Hz甚至更低刷新率下的表现在低刷新率时,MainRunLoop依然会以60Hz运行,对功耗有一定影响

需要注意的是,CADisplayLink的preferredFrameRateRange需要以类似一下格式进行设置:

NSIntegercurrentFPS=(NSInteger)ceil(1.0/displayLink.duration);displayLink.preferredFrameRateRange=CAFrameRateRangeMake(10.0,currentFPS,0.0);

CAFrameRateRange.minimum传最小值10.0,preferred传0.0,可以让该CADisplayLink只用于监控当前的系统帧率,而不影响帧率的动态选择。

相比前两个方案,方案三改动小,不使用私有API,监控准确性也较高,缺点相对来说可以接受。

FPS的替代指标

考虑到在ProMotion屏幕上FPS指标不再与应用运行是否流畅直接相关,它的聚合值参考价值不大,有必要寻找一个新指标作为替换。

Apple官方在WWDC20-10077EliminateanimationhitcheswithXCTest中介绍了HitchTimeRatio这一概念,并着重说明了它比单纯的FPS更能适配不同刷新率的场景。

在XCTest框架中,苹果提供了APIXCTOSSignpostMetric帮助开发者在单测中即时地获取该指标,但相关API尽在单测中提供,线上无法使用。而MetricKit中的MXAnimationMetric尽管可以在线上获取,但却不是实时的,无法满足大型App对不同场景的监控需求。

因此,遵循下面Apple对HitchRatio的定义:

Hitchtime:

Timeinmsthataframeislatetodisplay.

Hitchtimeratio:

Hitchtimeinmspersecondforagivenduration.

笔者尝试实现了基于CADisplayLink的(Scroll)HitchTimeRatio的计算方案:

计算上一帧的帧时间戳与上上一帧的目标帧时间戳得到上一帧的HitchTime确定该帧是否是在滑动中渲染累计得到整体的HitchFrame,与累积的帧间隔相比,得到(Scroll)HitchTimeRatio关键场景提升帧率

在测试过程中笔者发现,系统App滑动时是稳定以最高刷新率120Hz运行的:

而第三方App即便设置了CADisableMinimumFrameDurationOnPhone为true也无法稳定以满帧率滑动(经过验证,这一点在iOS15.4beta系统上依然成立)。

通过利用iOS15引入的新API,我们可以在关键场景如滑动、转场、动画过程中主动解锁更高/限制更低的动态帧率,从而优化流畅度或者优化功率,提升用户体验目标。

滑动中稳定120Hz

首先,笔者希望非系统App也可以尽可能实现滑动中稳定120Hz刷新。

结合上述分析,这一点可以用CADisplayLink来实现。这里笔者提出两种可能方案仅供参考:

创建CADisplayLink,配置其preferredFramesPerSecond为120,然后将其添加到UITrackingRunLoopMode中。CADisplayLink*dp=...dp.preferredFramesPerSecond=120;//或者dp.preferredFrameRateRange=CAFrameRateRangeMake(120.0,120.0,0.0);[dpaddToRunLoop:[NSRunLoopmainRunLoop]forMode:UITrackingRunLoopMode];

在滑动中,该CADisplayLink被激活,系统锁定当前帧率为最高120Hz(仅在内容中高速变化时生效)。停止滑动时则恢复正常帧率。

添加CADisplayLink至CommonModes中,分别在开始/停止滑动时启用/暂停CADisplayLink,并修改对应的preferredFramesPerSecond等属性,触发帧率变化。CADisplayLink*dp=...dp.paused=YES;[dpaddToRunLoop:[NSRunLoopmainRunLoop]forMode:NSRunLoopCommonModes];CFRunLoopAddObserver(CFRunLoopGetMain(),CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault,kCFRunLoopEntry|kCFRunLoopExit,YES,0,^(CFRunLoopObserverRefobserver,CFRunLoopActivityactivity){if(activity==kCFRunLoopEntry){dp.paused=NO;dp.preferredFramePerSecond=120;}else{dp.paused=YES;dp.preferredFramePerSecond=0;}}),(__bridgeCFStringRef)UITrackingRunLoopMode);

在实践中,由于也存在需要在非滑动状态下解锁帧率上限的情况,所以方案2的通用性会更好。

CAAnimation设置动态帧率

目前苹果只提供了修改CAAnimation动画帧率的API,设置CAAnimation.preferredFrameRateRange即可改变其对屏幕刷新率的影响。

对于用户感知明显的,如转场动画,可以设置为120Hz。对于感知不明显的,如旋转动画,可以降低其帧率,比如设置为30Hz。

但是,和DisplayLink相同,过上述API的设置虽然会“影响”系统的动态帧率的选择,但这种影响并不是绝对的。在实际使用中,笔者发现屏幕选择的刷新率和CAAnimation在屏幕上变化的速度有关。

关于此点,以iPhone13Pro为例,笔者使用了一个简单的、偏好帧率为固定120Hz平移动画进行说明:

CABasicAnimation*anim=[CABasicAnimationanimationWithKeyPath:@"transform.translation.y"];CGFloatspeed=170.0/330.0;anim.toValue=@(100);anim.fromValue=@(0);anim.duration=10.0;anim.repeatCount=FLT_MAX;anim.preferredFrameRateRange=CAFrameRateRangeMake(120,120,120);

其中speed变量为平移的速度,单位为pt/s,试验发现:

speed取(0,160]时,屏幕刷新率为60Hzspeed取[161,320]时,屏幕刷新率为80Hzspeed取[321,+∞)时,屏幕刷新率为120Hz

笔者仅在iPhone13Pro上测试了平移动画的场景,以上数据仅供参考。

最后,对于其他的常见的动画API,例如UIView.animateWithDuration、UIViewPropertyAnimator等,则没有提供对应API进行修改。理论上也可以通过某些手段拿到这些上层API所创建的CAAnimation对象来实现修改。

手势/转场等其他场景解锁120Hz

其他场景需要控制动态帧率的也可以通过手动修改CADisplayLink的preferredFramePerSecond/preferredFrameRateRange属性来实现,其实现和通过监听RunLoop来修改滑动帧率基本相同。

UIGestureRecognizer常被用于实现的交互式动画。经过测试,发现在触发手势回调的同时启用一个解锁了频率的CADisplayLink也可以间接提高UIGestureRecognizer的回调频率,从而实现更高帧率的交互动画。

对于转场的场景,一个简单的方案是swizzleUIViewController的生命周期消息,在出现/消失的节点启用/停用CADisplayLink帧率的解锁,从而实现通用的页面转场动画帧率解锁方案。

Flutter官方也计划提供类似API让应用侧可以针对不同的场景(滑动、动画etc)动态切换屏幕刷新率:https://github.com/flutter/flutter/issues/90675

上线收益

基于上述思路,笔者所在团队在国际化短视频业务落地了优化项目,经过实验验证:

大盘滑动帧率P50从81.57上升至112.2核心业务指标也有一定收益结语

近年来,Apple生态中软硬件的发展日新月异,有软件层的dyld的持续优化和iOS15新引入的Prewarm机制,也有新的ProMotion屏幕,可以看到Apple一直致力于打造更丝滑流畅的用户体验。

Apple提供的系统级优化方案一般通用而无感知,但通用往往也意味着一定的局限性,可能预留了额外优化空间,应用开发者们可以进一步去研究如何更好地适配。

例如本文中,笔者通过研究新引入的ProMotion屏幕背后的机制,透过表象/深入汇编管中窥豹看到一部分本质,最终落地了监控+优化的方案,让大盘滑动帧率P50从80上升至112左右,取得了额外的业务收益。

最后,笔者认为,我们普通开发者作为Apple生态链中的一环,在享受系统级别优化自动带来的收益的同时,也应该主动去了解上述优化背后的底层原理。一方面,了解与学习Apple的成熟优化思路可以提升我们作为工程师的眼界。另一方面,对系统底层原理的了解可以拓充我们的“弹药库”,对业务价值交付的全链路了解越广越深,越有可能抓住潜在的优化点,从而在性能优化工程师这条职业道路上走得更远更好。

参考资料WWDC20-10077EliminateanimationhitcheswithXCTest

https://developer.apple.com/videos/play/wwdc2020/10077

WWDC21-10147Optimizeforvariablerefreshratedisplays

https://developer.apple.com/videos/play/wwdc2021/10147/

OptimizingProMotionRefreshRatesforiPhone13ProandiPadPro

https://developer.apple.com/documentation/quartzcore/optimizing_promotion_refresh_rates_for_iphone_13_pro_and_ipad_pro?language=objc

WhatisAdaptiveSync?

https://www.viewsonic.com/library/tech/explained/what-is-adaptive-sync/

https://github.com/flutter/flutter/issues/90675加入我们

我们是字节国际化短视频基础技术团队,是一个深度追求极致的团队,我们专注于性能、架构、包大小、稳定性、自动化测试、基础库、编译构建等方向的深耕,保障超大规模团队的研发效率和全球数亿用户的使用体验。目前上海、杭州、新加坡、美国都有大量人才需要,欢迎有志之士与我们共同建设亿级用户全球化APP!

可以点击「链接」,进入字节跳动招聘官网投递简历,也可以邮件联系:kazec.liu@bytedance.com咨询相关信息或者直接发送简历内推!

猜你喜欢