Apple 在今年推出了支持 ProMotion 屏幕的 iPhone 設備,讓 App 在 iPhone 13 Pro 和 iPhone 13 Pro Max 上的最大刷新幀率可到達 120Hz,極大優化了應用滑動/動畫的流暢度體驗。
ProMotion 并不是一個新的概念,早在 2017 年,Apple 推出的第二代 iPad Pro 便搭載了這一刷新率最高可達 120Hz 的屏幕。在 iPad 上,高刷新率默認對所有 App 啟用。而也許是出于能耗的考慮,在 iPhone 上,Apple 并未將這個能力自動對所有 App 啟用,而是需要開發者手動添加配置項來進行適配。
近期有消息指出 iOS 15.4 beta 修正了這一行為(https://www.macrumors.com/2022/01/27/ios-15-4-apps-120-hz-promotion/),經過筆者驗證額外的配置項依然是需要的,并且本文內容依然適用。
本文介紹了在 iPhone 上對 ProMotion 動態幀率的適配時觀察到的現象和遇到的問題,嘗試推測了背后的原理,并探討了解決問題的可能思路,最終基于調研結果在國際化短視頻業務上線優化方案,取得了核心業務指標的收益。
在深入探究 ProMotion 屏幕所帶來的變化之前,我們先回顧一個似乎耳熟能詳的概念:
什么是幀率?
眾所周知,顯示器并不能顯示真正動態的畫面,所有動畫效果都是靠高速播放一幀幀靜態畫面欺騙人類視覺所造成的假象。那么幀率最基本的定義便是屏幕內容的變化頻率,是一個物理意義上的指標。這種變化頻率又由以下兩個值共同決定:
理想情況下,渲染幀率和刷新幀率最好完全匹配,或者渲染幀率是刷新幀率的整數倍,這樣實際展現的內容不會出現任何異常。但現實中二者往往會出現不匹配的情況,卡頓就是其中之一:
當 CPU -> GPU 的渲染管線遇到瓶頸,導致某一幀的渲染耗時大于屏幕的刷新間隔時,上一幀畫面會在屏幕上多停留數幀的時間。當這個滯留時間過長,用戶感知到畫面更新的延遲,這稱為卡頓。這也是 iOS 開發過程中會遇到的主要性能問題之一。
幀率并不等同于刷新率,它和所展示的內容息息相關:
ProMotion 本質上是對 Adaptive-Sync 顯示標準的一種實現。
Ref: https://en.wikipedia.org/wiki/Variable_refresh_rate
根據 Apple 官方文檔顯示,ProMotion 屏幕支持的刷新率是可變的。
具體來說,對 iPhone 而言:
The iPhone 13 Pro and iPhone 13 Pro Max ProMotion displays can present content on the display using the following refresh rates and timings:
120Hz (8ms), 80Hz (12ms), 60Hz (16ms), 48Hz (20ms), 40Hz (25ms), 30Hz (33ms), 24Hz (41ms), 20Hz (50ms), 16Hz (62ms), 15Hz (66ms), 12Hz (83ms), 10Hz (100ms)
而對 iPad Pro 來說:
The iPad Pro’s ProMotion display can present content on the display using the following refresh rates and timings:
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 屏幕在這種情況下可以主動降低刷新率,減少屏幕功耗,這對于移動設備來說尤其重要。
The iPhone 13 Pro, the iPhone 13 Pro Max, and the iPad Pro ProMotion displays are capable of dynamically switching between:
- Faster refresh rates up to 120Hz
- Slower refresh rates down to 24Hz or 10Hz
已知,ProMotion 屏幕的刷新幀率并不固定,系統會實時地根據當前顯示內容的類型和狀態來動態切換屏幕的刷新幀率。為了更好地理解這種動態幀率的表現形式,筆者分別在
上對一些典型渲染場景進行了測試,發現搭載了 ProMotion 屏幕的設備上運行 App 時,不同的場景下的各種統計口徑的幀率指標確實展示出了有趣的變化。
具體而言,筆者分別在以下幾種場景:
1 . 靜態頁面
靜態的 UIView,無動畫/視頻等元素
2 . 滑動中的頁面
包含靜態 Cell 的 UITableView,僅觀察滑動中的表現
3 . Core Animation 默認刷新率動畫
顯示基于 CABasicAnimation 實現的簡單位移動畫
4 . Core Animation 120Hz 高刷新率動畫
僅在 ProMotion 設備上測試,基于 CABasicAnimation 實現的簡單位移動畫,同時解鎖了 CADisableMinimumFrameDurationOnPhone
和 preferredFrameRateRange
幀率限制。(關于此限制下文會有具體介紹)
5 . Metal 渲染 30Hz/60Hz 視頻
使用基于 MTKView
進行渲染的播放器,播放源幀率分別為 30Hz/60Hz 的視頻文件
并使用以下幾種統計口徑的幀率指標進行測試:
1 . CADisplayLink 計算幀率
iOS 中主要的幀率統計手段。
根據 CADisplayLink.h
頭文件中描述,CADisplayLink 是一個 ”Class representing a timer bound to the display vsync “。在回調中比較當前幀/前一幀的時間戳,可以計算出上一幀的渲染耗時(ts),其倒數(1/ts)即為當前的實時幀率。
2 . Xcode GPU Report 幀率
Xcode -> Show Debug Navigator -> FPS 中顯示的幀率。這個只能統計當前應用直接通過 OpenGL ES 或者 Metal 進行繪制的幀率,例如游戲渲染/視頻播放,無法統計 Core Animation 的幀率(眾所周知,后者通過 backboardd
進行繪制)。
3 . Instruments Core Animation FPS
Instruments 中 Core Animation FPS 工具所顯示的幀率。這個統計的是 Core Animation 的幀率,即 Render Server backboardd
繪制的頻率。目前該工具有 BUG 無法顯示高于 60 FPS 的幀率。
4 . Instruments Display/VSync 信號頻率
Instruments 中 Display 工具所顯示的 Surface/VSync 信號時間戳。如下圖所示:
在 60Hz 屏幕上,iOS 設備默認采用雙緩沖刷新機制,也就是前幀緩存和后幀緩存。GPU 總是在后幀緩存上進行當前幀的繪制。當 VSync 信號到來時,交換前后幀緩存的指針(Swap FrameBuffer),屏幕刷新顯示新的內容。
而當屏幕以 120Hz 顯示內容時,iOS 會切換成三緩沖刷新機制(見上圖中三種顏色的 Surface),這減少渲染管線的壓力,但同時會增加一定的渲染上屏延遲。
Metal 應用可以通過設置
-[CAMetalLayer setMaximumDrawableCount:]
為 2 來在 120Hz 屏幕上強制啟用雙緩沖機制,避免這種延遲。
如果屏幕顯示內容未發生變化,Surface 則不會發生交換,一個 Surface 的 Display 可能持續數個 VSync 間隔,但多余的 VSync 信號依然代表著硬件層額外的屏幕刷新,造成額外的電量消耗。
首先讓我們看看傳統的固定刷新率的設備的情況。
XR 的屏幕刷新率為固定的 60Hz,這一點對應的具體指標是 VSync 信號的間隔,而在任何場景下,XR 的 VSync 信號的間隔均為固定的 16.67ms。
此外,在顯示靜態內容時,由于視圖 Layer Tree 無變化,Core Animation 不會有提交新的事務提交,backboardd
不會進行刷新,所以對應這一幀的 Surface 也長時間(數十秒)未被交換下去,Core Animation FPS 的值顯示為 0。
但由于 VSync 信號仍然以 60Hz 的頻率持續觸發,屏幕此時正在不停重復展示同樣的 Frame Buffer,消耗了額外的電量。
根據過去對 iOS 系統的認知,我們知道 CADisplayLink 是由 VSync 信號驅動的:
默認配置的 CADisplayLink 的回調應該與 VSync 信號基本同時。
這一點在 XR 上得到了驗證,用 Instruments 記錄一次主線程發生的卡頓,得到:
其中:
runloop
記錄每次 RunLoop AfterWaiting
-> BeforeWaiting
的間隔tick
記錄默認配置的 CADisplayLink 回調間的間隔可以觀察到下述現象,符合我們之前的對 DisplayLink 的認識:
下面看看 ProMotion 設備的測試結果。
在 ProMotion 屏幕上 VSync 信號間隔是可變的,具體而言:
顯示靜態內容時,屏幕降頻,最低以 10Hz 的頻率進行刷新
顯示 Core Animation 動畫時,系統會適配動畫的幀率設置改變刷新率
*通過 preferredFrameRateRange
可以設置 hint
請求高刷,但并不一定生效,詳見下文“動態幀率的應用場景”部分。
顯示滑動中內容時,刷新率在 80Hz 左右波動,并且跟隨滑動速度變化而變化??旎瑫r刷新率升高,慢滑時降低。
顯示視頻時,刷新率和視頻幀率維持一致
可以看到 VSync 信號間隔能主動跟隨顯示內容的渲染幀率的改變而改變。
在主線程發生卡頓導致滑動中某一幀渲染耗時過長時,系統會改變這一幀所對應的 VSync 信號間隔(下圖 Surface 5),減小從渲染到展示的延時,從而減緩用戶感知到的卡頓時長。
如圖是一張滑動中場景的 CADisplayLink 回調 和 Display/VSync 事件對照記錄。和之前不同的是,再 ProMotion 設備上 DisplayLink 和 VSync 信號之間沒有表現出明顯的跟隨關系:
具體而言:
我們知道,在 iOS 15 上 Apple 對第三方應用的顯示幀率默認做了限制。第三方應用需要在 Info.plist 中添加<key>CADisableMinimumFrameDurationOnPhone</key><true/>
字段才可以解鎖 120Hz 的刷新率。
于此同時,在 iOS 15 中,CADisplayLink 等動畫相關 API 也新增了一個用于配置偏好幀率的屬性:
/* Defines the range of desired callback rate in frames-per-second for this
display link. If the range contains the same minimum and maximum frame rate,
this property is identical as preferredFramesPerSecond. Otherwise, the actual
callback rate will be dynamically adjusted to better align with other
animation sources. */
@property(nonatomic) CAFrameRateRange preferredFrameRateRange
API_AVAILABLE(ios(15.0), watchos(8.0), tvos(15.0));
為了進一步探究新設備上 DisplayLink 和 VSync 信號之間的關系,筆者將測試 App 的 Core Animation 的幀率限制解除,并配置對應的 API,分別在不同的場景重新進行測試:
展示一個速度中等的位移動畫,得到下圖:
可以很直觀地發現,DisplayLink 解鎖幀率后的屏幕刷新率基本穩定在 120Hz。并且 VSync 和 DisplayLink 的關系似乎又重新一一對應了起來。
但是,將動畫速度減慢,筆者發現這種對應關系發生了變化:
可以觀察到在播放慢速動畫時,DisplayLink 的頻率依然是配置的 120Hz,但是實際的屏幕刷新率卻只有 30Hz。
讓我們換一種場景再次進行測試,快速滑動視圖,在 Instruments 中得到下圖:
可以發現,DisplayLink 解鎖幀率后,屏幕刷新率同樣基本穩定在 120Hz,僅在丟幀時有降頻。
然后降低滑動屏幕的速度,得到了和慢速動畫相似的結果,盡管 DisplayLink 回調速度不減,但是 VSync 信號頻率一直保持在較低的水平:
上面兩次測試都接近理想情況,即整個 Render Loop 執行幾乎沒有延遲與卡頓。但是現實中應用的運行總是有著各種各樣的或大或小的卡頓問題。
為了驗證更接近現實情況下,DisplayLink 和 VSync 信號之間的關系,在連續滑動的情況下筆者人為加入了一個 20ms 的微小卡頓進行測試:
上圖中可以看到,ProMotion 屏幕很好的處理了這次卡頓,由于三緩沖機制的存在,再 Render Loop 渲染
Surface 4
卡頓期間,通過改變 VSync 間隔,系統嘗試將緩沖區中的 Surface 283
與 Surface 250
延遲上屏,盡量縮短了用戶看到靜止畫面的時長。
隨后,主線程恢復執行,可以看到 DisplayLink 的回調頻率很快恢復至卡頓前的高水平。而此時 VSync 信號由于前述卡頓減緩機制的存在頻率其實有所降低。此時二者頻率并不吻合。
這和之前播放慢速動畫/慢速滑動的情況很相似,由于卡頓加上緩沖機制的存在導致短時間內系統將屏幕的刷新頻率降低,但在 CPU 側依然維持了 DisplayLink 的高速回調,滿足了使用方對 preferredFrameRateRange
這一 API 的設置。
為了進一步分析了這種機制的本質,筆者接下來會嘗試逆向分析 iOS 15 中的系統庫相關實現的改動。
在 CADisplayLink 回調方法上設置斷點,分別在 iOS 14 和 15 ProMotion 設備上運行,可以得到:
1 . 在 iOS 14 上,CADisplayLink 是通過 Source 1 mach_port 直接接受 VSync 信號驅動的
2 . 在 iOS 15 ProMotion 設備上,CADisplayLink 不再由 VSync 信號驅動,而是由一個 UIKit 內部的 Source0 信號驅動
在 15 中,CADisplayLink 第一次創建并添加至 RunLoop 的時候,會注冊一個 Source 1 信號,這和 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(上面的 ldr x9 [x8, #0x10] 就是把 invoke 指針從 block 結構體中取出的意思)。tbz
指令中 w0 寄存器為 block 執行的返回值,為 0(即 NO)時跳轉至 0x1848dbc08,而 0x1848dbc08 剛好在 dispatch_items
的調用之后,跳過了該調用。
通過對上圖中 blraa
指令 step in,我們發現這個 block 實際上是由 UIKitCore 注冊的:
找到引用了該符號的 UIKit 的私有方法
__UIUpdateCycleSchedulerStart
,反匯編結果也驗證了這點。
同時發現這個 block 的返回值固定為 0x0。
而同樣的 symbol 在之前的 iOS 版本上并不存在,也就是說這個應該是 iOS 15 的變動。換安裝了 iOS 15 的非 ProMotion 設備,重走上面的逆向流程發現,該設備的
CA::display_link_will_fire_handler
為 nil,未注冊:
這里
cbz
執行了跳轉,說明 x0
為 nil,而 x0
是由 ldr x0, [x8, #0x1c8]
得到。
可以看到
x0
就是 CA::display_link_will_fire_handler
。繼續分析之前找到的私有符號 __UIUpdateCycleSchedulerStart
的相關實現,可以知道這是因為在非 ProMotion 設備上 _UIUpdateCycleEnabled
返回了 NO 導致的。
在返回 NO 的情況下
__UIUpdateCycleSchedulerStart
方法不會執行,CA::display_link_will_fire_handler
也就不會被注冊。
繼續研究 _UIUpdateCycleEnabled
相關的代碼,筆者發現這個的改動并不是僅僅影響 DisplayLink 驅動方式那么簡單。
當 _UIUpdateCycleEnabled
返回 YES 時,UIKit 會在 UIApplicationMain
中執行 _UIUpdateCycleSchedulerStart
。分析該函數,發現 _UIUpdateCycleEnabled
啟用時會調用 [CATransaction setDisableRunLoopObserverCommits:YES]
。
Core Animation
是絕大部分 iOS 應用的渲染引擎,熟悉 iOS 渲染流程的同學想必都知道它的執行也是由 MainRunLoop
驅動,大致為:
MainRunLoop
因為用戶操作/Timer/GCD 等被喚醒,派發相應的事件/回調Layer Tree
,觸發 setNeedsLayout
或 setNeedsDisplay
MainRunLoop
即將完成本次執行,在即將休眠前向 Observer
派發 BeforeWaiting
事件BeforeWaiting
中觸發 Core Animation
注冊的 MainRunLoop Observer
,觸發事務提交 CA::Transaction::commit()
:5 . 隨后 MainRunLoop
進入休眠
6 . Render Server
將打包好的 Layer Tree
解碼,生成并提交對應的 draw calls
7 . GPU
執行渲染指令,渲染出 FrameBuffer
,待后續 VSync 信號來臨時上屏展示
上圖中 +[CATransaction setDisableRunLoopObserverCommits:YES]
這個調用給了筆者提示,讓我們驗證一下 CA::Transaction::commit()
在 iOS 15 ProMotion 設備上的執行時機,會發現確實不再由 BeforeWaiting
事件驅動了:
實際上同樣的
Source 0
信號同時也驅動了 CADisplayLink 的回調:
關注這個
Source 0
的回調符號 runloopSourceCallback
,會發現這個 Source0 是由 signalChanges
函數驅動:
而
signalChanges
又是由多個回調所驅動:
其中:
runloopObserverCallback
為一個 BeforeWaiting
的 MainRunLoop observer
驅動。runloopTimerCallback
由 mk_timer
驅動,對應的 mach_port
不明,測試發現其回調頻率在 1Hz 左右,但也會不斷變化,猜測是某種系統計時器。inputGroupSignaledCallback
由 mk_timer
驅動,對應的 mach_port
正是 VSync 信號。4 . requestRegistrySignaledCallback
由 UIScrollView
在即將開始滑動時驅動。
通過上面的分析,筆者有理由認為在 iOS 15 上應用的渲染驅動機制出現了比較大的變化。其中之一便是 DisplayLink 的驅動源的改變。
關于如何界定低速/中高速,筆者在下文中 CAAnimation 設置動態幀率 部分做了一些試驗,可作為參考。
同時,默認配置的 CADisplayLink 回調頻率最高為 60Hz,無法監控更高頻率的刷新事件。
3 . ProMotion 設備中,DisplayLink 不再由 VSync 信號直接驅動,而是在新引入的渲染事件循環中執行。新版本 iOS 系統實現了某種更復雜的機制來盡可能滿足使用者設置的偏好頻率進行回調,但并不保證它與 VSync 信號的強關聯性。這意味著默認的 CADisplayLink 的回調頻率與實際幀率并不匹配,之前基于 CADisplayLink 進行幀率監控的方案在 ProMotion 設備上變得不再可行。
業界中一般采用 CADisplayLink 對應用的流暢度進行監控。由于 CADisplayLink 的行為在 iOS 15 上的變化,原先的監控方案無法評估 ProMotion 屏幕在超過 60Hz 時的表現。
根據上面的探索結論,目前筆者設想了三種針對 ProMotion 設備的兼容性修改方案:
對于任何設備都以 60Hz 為優化目標,只考慮刷新間隔長于 16.67ms
的情況。換句話說,在屏幕以 120Hz 刷新時,對于丟 1 幀的情況也認為不丟幀,因為此時兩幀之間的間隔仍然小于 16.67ms
,理論上用戶感知不大。
優點:
preferredFramesPerSecond
為固定值 60 即可缺點:
通過一些手段,可以替換驅動 display_timer_callback
的 Source 1 信號的回調,使用它來準確監聽 VSync 信號,實現對動態幀率的準確監控。
優點:
缺點:
通過在 CADisplayLink 回調中確認 duration
參數,計算得到當前屏幕的實時刷新率,并修改 preferredFrameRateRange
來進行跟蹤。
優點:
方案相對簡單,只需在每次回調中更新 DisplayLink 對象的 preferredFrameRateRange
屬性即可
缺點:
需要注意的是,CADisplayLink 的 preferredFrameRateRange
需要以類似一下格式進行設置:
NSInteger currentFPS = (NSInteger)ceil(1.0 / displayLink.duration);
displayLink.preferredFrameRateRange = CAFrameRateRangeMake(10.0, currentFPS, 0.0);
CAFrameRateRange.minimum 傳最小值 10.0,preferred 傳 0.0,可以讓該 CADisplayLink 只用于監控當前的系統幀率,而不影響幀率的動態選擇。
相比前兩個方案,方案三改動小,不使用私有 API,監控準確性也較高,缺點相對來說可以接受。
考慮到在 ProMotion 屏幕上 FPS 指標不再與應用運行是否流暢直接相關,它的聚合值參考價值不大,有必要尋找一個新指標作為替換。
Apple 官方在 WWDC20 - 10077 Eliminate animation hitches with XCTest 中介紹了 Hitch Time Ratio
這一概念,并著重說明了它比單純的 FPS 更能適配不同刷新率的場景。
在 XCTest 框架中,蘋果提供了 API XCTOSSignpostMetric
幫助開發者在單測中即時地獲取該指標,但相關 API 盡在單測中提供,線上無法使用。而 MetricKit 中的 MXAnimationMetric
盡管可以在線上獲取,但卻不是實時的,無法滿足大型 App 對不同場景的監控需求。
因此,遵循下面 Apple 對 Hitch Ratio
的定義:
Hitch time:
- Time in ms that a frame is late to display.
Hitch time ratio:
- Hitch time in ms per second for a given duration.
筆者嘗試實現了基于 CADisplayLink 的 (Scroll) Hitch Time Ratio
的計算方案:
在測試過程中筆者發現,系統 App 滑動時是穩定以最高刷新率 120Hz 運行的:
而第三方 App 即便設置了
CADisableMinimumFrameDurationOnPhone
為 true
也無法穩定以滿幀率滑動(經過驗證,這一點在 iOS 15.4 beta 系統上依然成立)。
通過利用 iOS 15 引入的新 API,我們可以在關鍵場景如滑動、轉場、動畫過程中主動解鎖更高/限制更低的動態幀率,從而優化流暢度或者優化功率,提升用戶體驗目標。
首先,筆者希望非系統 App 也可以盡可能實現滑動中穩定 120Hz 刷新。
結合上述分析,這一點可以用 CADisplayLink 來實現。這里筆者提出兩種可能方案僅供參考:
preferredFramesPerSecond
為 120,然后將其添加到 UITrackingRunLoopMode
中。CADisplayLink *dp = ...
dp.preferredFramesPerSecond = 120;
// 或者
dp.preferredFrameRateRange = CAFrameRateRangeMake(120.0, 120.0, 0.0);
[dp addToRunLoop:[NSRunLoop mainRunLoop] forMode:UITrackingRunLoopMode];
在滑動中,該 CADisplayLink 被激活,系統鎖定當前幀率為最高 120Hz(僅在內容中高速變化時生效)。停止滑動時則恢復正常幀率。
2 . 添加 CADisplayLink 至 CommonModes
中,分別在開始/停止滑動時啟用/暫停 CADisplayLink,并修改對應的 preferredFramesPerSecond
等屬性,觸發幀率變化。
CADisplayLink *dp = ...
dp.paused = YES;
[dp addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
CFRunLoopAddObserver(CFRunLoopGetMain(),
CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopEntry | kCFRunLoopExit, YES, 0,
^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
if (activity == kCFRunLoopEntry) {
dp.paused = NO;
dp.preferredFramePerSecond = 120;
} else {
dp.paused = YES;
dp.preferredFramePerSecond = 0;
}
}), (__bridge CFStringRef)UITrackingRunLoopMode);
在實踐中,由于也存在需要在非滑動狀態下解鎖幀率上限的情況,所以方案 2 的通用性會更好。
目前蘋果只提供了修改 CAAnimation
動畫幀率的 API,設置 CAAnimation.preferredFrameRateRange
即可改變其對屏幕刷新率的影響。
但是,和 DisplayLink 相同,過上述 API 的設置雖然會“影響”系統的動態幀率的選擇,但這種影響并不是絕對的。在實際使用中,筆者發現屏幕選擇的刷新率和 CAAnimation 在屏幕上變化的速度有關。
關于此點,以 iPhone 13 Pro 為例,筆者使用了一個簡單的、偏好幀率為固定 120Hz 平移動畫進行說明:
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"transform.translation.y"];
CGFloat speed = 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筆者僅在 iPhone 13 Pro 上測試了平移動畫的場景,以上數據僅供參考。
最后,對于其他的常見的動畫 API,例如 UIView.animateWithDuration
、UIViewPropertyAnimator
等,則沒有提供對應 API 進行修改。理論上也可以通過某些手段拿到這些上層 API 所創建的 CAAnimation 對象來實現修改。
其他場景需要控制動態幀率的也可以通過手動修改 CADisplayLink 的 preferredFramePerSecond/preferredFrameRateRange
屬性來實現,其實現和通過監聽 RunLoop 來修改滑動幀率基本相同。
UIGestureRecognizer
常被用于實現的交互式動畫。經過測試,發現在觸發手勢回調的同時啟用一個解鎖了頻率的 CADisplayLink 也可以間接提高 UIGestureRecognizer
的回調頻率,從而實現更高幀率的交互動畫。
對于轉場的場景,一個簡單的方案是 swizzle UIViewController 的生命周期消息,在出現/消失的節點啟用/停用 CADisplayLink 幀率的解鎖,從而實現通用的頁面轉場動畫幀率解鎖方案。
Flutter 官方也計劃提供類似 API 讓應用側可以針對不同的場景(滑動、動畫 etc)動態切換屏幕刷新率:https://github.com/flutter/flutter/issues/90675
基于上述思路,筆者所在團隊在國際化短視頻業務落地了優化項目,經過實驗驗證:
近年來,Apple 生態中軟硬件的發展日新月異,有軟件層的 dyld 的持續優化和 iOS 15 新引入的 Prewarm 機制,也有新的 ProMotion 屏幕,可以看到 Apple 一直致力于打造更絲滑流暢的用戶體驗。
Apple 提供的系統級優化方案一般通用而無感知,但通用往往也意味著一定的局限性,可能預留了額外優化空間,應用開發者們可以進一步去研究如何更好地適配。
例如本文中,筆者通過研究新引入的 ProMotion 屏幕背后的機制,透過表象/深入匯編管中窺豹看到一部分本質,最終落地了監控 + 優化的方案,讓大盤滑動幀率 P50 從 80 上升至 112 左右,取得了額外的業務收益。
最后,筆者認為,我們普通開發者作為 Apple 生態鏈中的一環,在享受系統級別優化自動帶來的收益的同時,也應該主動去了解上述優化背后的底層原理。一方面,了解與學習 Apple 的成熟優化思路可以提升我們作為工程師的眼界。另一方面,對系統底層原理的了解可以拓充我們的“彈藥庫”,對業務價值交付的全鏈路了解越廣越深,越有可能抓住潛在的優化點,從而在性能優化工程師這條職業道路上走得更遠更好。
1 . WWDC20 - 10077 Eliminate animation hitches with XCTest
https://developer.apple.com/videos/play/wwdc2020/10077
2 . WWDC21 - 10147 Optimize for variable refresh rate displays
https://developer.apple.com/videos/play/wwdc2021/10147/
3 . Optimizing ProMotion Refresh Rates for iPhone 13 Pro and iPad Pro
https://developer.apple.com/documentation/quartzcore/optimizing_promotion_refresh_rates_for_iphone_13_pro_and_ipad_pro?language=objc
4 . What is Adaptive Sync?
https://www.viewsonic.com/library/tech/explained/what-is-adaptive-sync/
5 . https://github.com/flutter/flutter/issues/90675
最多閱讀