扣丁書屋

iOS 高刷屏監控 + 優化:從理論到實踐全面解析

背景

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 屏幕所帶來的變化之前,我們先回顧一個似乎耳熟能詳的概念:

什么是幀率?

眾所周知,顯示器并不能顯示真正動態的畫面,所有動畫效果都是靠高速播放一幀幀靜態畫面欺騙人類視覺所造成的假象。那么幀率最基本的定義便是屏幕內容的變化頻率,是一個物理意義上的指標。這種變化頻率又由以下兩個值共同決定:

  • 刷新幀率:由屏幕硬件規格控制,傳統顯示設備一般為 59.94Hz,決定了幀率的上限。
  • 渲染幀率:由 CPU -> GPU 渲染管線的執行速率控制,決定了幀率的下限。

理想情況下,渲染幀率和刷新幀率最好完全匹配,或者渲染幀率是刷新幀率的整數倍,這樣實際展現的內容不會出現任何異常。但現實中二者往往會出現不匹配的情況,卡頓就是其中之一:

卡頓

當 CPU -> GPU 的渲染管線遇到瓶頸,導致某一幀的渲染耗時大于屏幕的刷新間隔時,上一幀畫面會在屏幕上多停留數幀的時間。當這個滯留時間過長,用戶感知到畫面更新的延遲,這稱為卡頓。這也是 iOS 開發過程中會遇到的主要性能問題之一。

實際幀率

幀率并不等同于刷新率,它和所展示的內容息息相關:

  • 展示靜態畫面時,理想情況只需要進行一次渲染,盡管屏幕仍然以 60Hz 或者更高的頻率進行刷新,每次刷新所展示的內容(FrameBuffer)也未改變,用戶感知到的實際幀率依然接近 0。
  • 展示固定幀率的元素,例如 24FPS 的電影視頻時,用戶感知到的實際幀率自然也是 24 FPS 左右。
  • 展示超高幀率的內容,例如 CS:GO 不鎖幀跑 >200 FPS,但由于顯示設備刷新率限制,用戶感知到的幀率依然不會超過硬件幀率的上限。

什么是動態刷新率

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 屏幕的刷新幀率并不固定,系統會實時地根據當前顯示內容的類型和狀態來動態切換屏幕的刷新幀率。為了更好地理解這種動態幀率的表現形式,筆者分別在

  • iPhone XR - 無 ProMotion
  • iPhone 13 Pro - 有 ProMotion 默認鎖頻

上對一些典型渲染場景進行了測試,發現搭載了 ProMotion 屏幕的設備上運行 App 時,不同的場景下的各種統計口徑的幀率指標確實展示出了有趣的變化。

具體而言,筆者分別在以下幾種場景:

測試場景

1 . 靜態頁面

靜態的 UIView,無動畫/視頻等元素

2 . 滑動中的頁面

包含靜態 Cell 的 UITableView,僅觀察滑動中的表現

3 . Core Animation 默認刷新率動畫

顯示基于 CABasicAnimation 實現的簡單位移動畫

4 . Core Animation 120Hz 高刷新率動畫

僅在 ProMotion 設備上測試,基于 CABasicAnimation 實現的簡單位移動畫,同時解鎖了 CADisableMinimumFrameDurationOnPhonepreferredFrameRateRange 幀率限制。(關于此限制下文會有具體介紹)

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 信號時間戳。如下圖所示:

  • Display:指對應顯示器的單個 Surface 上屏持續的時間,對應 CPU-GPU 管線的渲染頻率
  • VSync:指垂直同步信號時間戳,對應屏幕硬件的刷新頻率

在 60Hz 屏幕上,iOS 設備默認采用雙緩沖刷新機制,也就是前幀緩存和后幀緩存。GPU 總是在后幀緩存上進行當前幀的繪制。當 VSync 信號到來時,交換前后幀緩存的指針(Swap FrameBuffer),屏幕刷新顯示新的內容。

而當屏幕以 120Hz 顯示內容時,iOS 會切換成三緩沖刷新機制(見上圖中三種顏色的 Surface),這減少渲染管線的壓力,但同時會增加一定的渲染上屏延遲。

Metal 應用可以通過設置 -[CAMetalLayer setMaximumDrawableCount:] 為 2 來在 120Hz 屏幕上強制啟用雙緩沖機制,避免這種延遲。

如果屏幕顯示內容未發生變化,Surface 則不會發生交換,一個 Surface 的 Display 可能持續數個 VSync 間隔,但多余的 VSync 信號依然代表著硬件層額外的屏幕刷新,造成額外的電量消耗。

非 ProMotion 設備

首先讓我們看看傳統的固定刷新率的設備的情況。

VSync 信號間隔固定為 16.67ms

XR 的屏幕刷新率為固定的 60Hz,這一點對應的具體指標是 VSync 信號的間隔,而在任何場景下,XR 的 VSync 信號的間隔均為固定的 16.67ms。

此外,在顯示靜態內容時,由于視圖 Layer Tree 無變化,Core Animation 不會有提交新的事務提交,backboardd 不會進行刷新,所以對應這一幀的 Surface 也長時間(數十秒)未被交換下去,Core Animation FPS 的值顯示為 0。

但由于 VSync 信號仍然以 60Hz 的頻率持續觸發,屏幕此時正在不停重復展示同樣的 Frame Buffer,消耗了額外的電量。

CADisplayLink 基本完全跟隨 VSync 信號

根據過去對 iOS 系統的認知,我們知道 CADisplayLink 是由 VSync 信號驅動的:

默認配置的 CADisplayLink 的回調應該與 VSync 信號基本同時。

這一點在 XR 上得到了驗證,用 Instruments 記錄一次主線程發生的卡頓,得到:

其中:

  • 第一行 runloop 記錄每次 RunLoop AfterWaiting -> BeforeWaiting 的間隔
  • 第二行 tick 記錄默認配置的 CADisplayLink 回調間的間隔
  • 最下面則是硬件 Display/VSync 事件時序圖

可以觀察到下述現象,符合我們之前的對 DisplayLink 的認識:

  • 沒有卡頓的情況下,VSync 信號和 RunLoop 的喚醒 & CADisplayLink 回調的觸發嚴格一一對應。
  • RunLoop 卡頓,無法處理 Source 1 信號,DisplayLink 回調被延遲到卡頓結束時。
  • 在此過程中 VSync 信號間隔始終保持不變。

ProMotion 設備

下面看看 ProMotion 設備的測試結果。

VSync 信號間隔可變

在 ProMotion 屏幕上 VSync 信號間隔是可變的,具體而言:

  • 顯示靜態內容時,屏幕降頻,最低以 10Hz 的頻率進行刷新

  • 顯示 Core Animation 動畫時,系統會適配動畫的幀率設置改變刷新率

  • *通過 preferredFrameRateRange 可以設置 hint 請求高刷,但并不一定生效,詳見下文“動態幀率的應用場景”部分。

  • 顯示滑動中內容時,刷新率在 80Hz 左右波動,并且跟隨滑動速度變化而變化??旎瑫r刷新率升高,慢滑時降低。

  • 顯示視頻時,刷新率和視頻幀率維持一致

可以看到 VSync 信號間隔能主動跟隨顯示內容的渲染幀率的改變而改變。

減少卡頓造成的顯示延遲

在主線程發生卡頓導致滑動中某一幀渲染耗時過長時,系統會改變這一幀所對應的 VSync 信號間隔(下圖 Surface 5),減小從渲染到展示的延時,從而減緩用戶感知到的卡頓時長。

DisplayLink 不完全跟隨 VSync 信號

如圖是一張滑動中場景的 CADisplayLink 回調 和 Display/VSync 事件對照記錄。和之前不同的是,再 ProMotion 設備上 DisplayLink 和 VSync 信號之間沒有表現出明顯的跟隨關系:

具體而言:

  1. 第三個箭頭所指向的 DisplayLink 的回調并不及時。在這之前主線程的卡頓已經結束,并且額外執行了兩次 RunLoop,但直到第三次才調用了 DisplayLink 的回調。
  2. 不僅僅是時機不匹配,也存在收到 VSync 但不觸發 DisplayLink 回調的情況(并且主線程處于空閑狀態),例如上圖中的 ? 處。

解除 DisplayLink 的幀數限制

我們知道,在 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,僅在丟幀時有降頻。

  1. 需要注意的是筆者在 CADisplayLink 的回調中除了調用 os_signpost 上報 log 外無任何 UI 改動。
  2. 即便筆者展示的 TableView 極其簡單,上圖中仍然可以觀察到丟幀,無法在滑動中完美穩定 120Hz。這也許說明 UIKit 的渲染性能在 120Hz 下會有某種程度上的原生瓶頸。

然后降低滑動屏幕的速度,得到了和慢速動畫相似的結果,盡管 DisplayLink 回調速度不減,但是 VSync 信號頻率一直保持在較低的水平:

卡頓場景

上面兩次測試都接近理想情況,即整個 Render Loop 執行幾乎沒有延遲與卡頓。但是現實中應用的運行總是有著各種各樣的或大或小的卡頓問題。

為了驗證更接近現實情況下,DisplayLink 和 VSync 信號之間的關系,在連續滑動的情況下筆者人為加入了一個 20ms 的微小卡頓進行測試:

上圖中可以看到,ProMotion 屏幕很好的處理了這次卡頓,由于三緩沖機制的存在,再 Render Loop 渲染 Surface 4 卡頓期間,通過改變 VSync 間隔,系統嘗試將緩沖區中的 Surface 283Surface 250 延遲上屏,盡量縮短了用戶看到靜止畫面的時長。

隨后,主線程恢復執行,可以看到 DisplayLink 的回調頻率很快恢復至卡頓前的高水平。而此時 VSync 信號由于前述卡頓減緩機制的存在頻率其實有所降低。此時二者頻率并不吻合。

這和之前播放慢速動畫/慢速滑動的情況很相似,由于卡頓加上緩沖機制的存在導致短時間內系統將屏幕的刷新頻率降低,但在 CPU 側依然維持了 DisplayLink 的高速回調,滿足了使用方對 preferredFrameRateRange 這一 API 的設置。

為了進一步分析了這種機制的本質,筆者接下來會嘗試逆向分析 iOS 15 中的系統庫相關實現的改動。

逆向分析

DisplayLink 驅動方式的變化

在 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 所帶來的變化

繼續研究 _UIUpdateCycleEnabled 相關的代碼,筆者發現這個的改動并不是僅僅影響 DisplayLink 驅動方式那么簡單。

_UIUpdateCycleEnabled 返回 YES 時,UIKit 會在 UIApplicationMain 中執行 _UIUpdateCycleSchedulerStart。分析該函數,發現 _UIUpdateCycleEnabled 啟用時會調用 [CATransaction setDisableRunLoopObserverCommits:YES]。

Core Animation 是絕大部分 iOS 應用的渲染引擎,熟悉 iOS 渲染流程的同學想必都知道它的執行也是由 MainRunLoop 驅動,大致為:

  1. MainRunLoop 因為用戶操作/Timer/GCD 等被喚醒,派發相應的事件/回調
  2. 回調中應用修改 Layer Tree,觸發 setNeedsLayoutsetNeedsDisplay
  3. MainRunLoop 即將完成本次執行,在即將休眠前向 Observer 派發 BeforeWaiting 事件
  4. BeforeWaiting 中觸發 Core Animation 注冊的 MainRunLoop Observer,觸發事務提交 CA::Transaction::commit()
  • 自頂向下觸發各種 Layout/Display 等邏輯,更新布局/內容
  • Core Animation 將更新后的 Layer Tree 打包發送給 Render Server

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 又是由多個回調所驅動:

其中:

  1. runloopObserverCallback 為一個 BeforeWaitingMainRunLoop observer 驅動。
  2. runloopTimerCallbackmk_timer 驅動,對應的 mach_port 不明,測試發現其回調頻率在 1Hz 左右,但也會不斷變化,猜測是某種系統計時器。
  3. inputGroupSignaledCallbackmk_timer 驅動,對應的 mach_port 正是 VSync 信號。

4 . requestRegistrySignaledCallbackUIScrollView 在即將開始滑動時驅動。

通過上面的分析,筆者有理由認為在 iOS 15 上應用的渲染驅動機制出現了比較大的變化。其中之一便是 DisplayLink 的驅動源的改變。

結論

  1. iOS 15 上 Apple 改變了在 ProMotion 設備的渲染事件循環的驅動方式,CoreAnimation 的事務提交不再由完全由 RunLoop 驅動,而是涉及了多個信號源
  2. 系統動態幀率選擇的機制會綜合考慮使用方設置的 API(如 preferredFrameRateRange)和實際展示的內容的變化頻率。具體對 CADisplayLink 而言:
  • 內容低速變化時,CADisplayLink 解鎖高刷新率僅影響自身的回調頻率,系統仍可能選擇較低的屏幕刷新率來降低功耗
  • 內容中高速變化時,CADisplayLink 解鎖高刷新率可以讓系統選擇更高的刷新頻率,甚至實現鎖定 120Hz 的刷新

關于如何界定低速/中高速,筆者在下文中 CAAnimation 設置動態幀率 部分做了一些試驗,可作為參考。

同時,默認配置的 CADisplayLink 回調頻率最高為 60Hz,無法監控更高頻率的刷新事件。

3 . ProMotion 設備中,DisplayLink 不再由 VSync 信號直接驅動,而是在新引入的渲染事件循環中執行。新版本 iOS 系統實現了某種更復雜的機制來盡可能滿足使用者設置的偏好頻率進行回調,但并不保證它與 VSync 信號的強關聯性。這意味著默認的 CADisplayLink 的回調頻率與實際幀率并不匹配,之前基于 CADisplayLink 進行幀率監控的方案在 ProMotion 設備上變得不再可行。

動態幀率的應用場景

監控動態幀率下的流暢度表現

業界中一般采用 CADisplayLink 對應用的流暢度進行監控。由于 CADisplayLink 的行為在 iOS 15 上的變化,原先的監控方案無法評估 ProMotion 屏幕在超過 60Hz 時的表現。

根據上面的探索結論,目前筆者設想了三種針對 ProMotion 設備的兼容性修改方案:

方案一 [Pass]

對于任何設備都以 60Hz 為優化目標,只考慮刷新間隔長于 16.67ms 的情況。換句話說,在屏幕以 120Hz 刷新時,對于丟 1 幀的情況也認為不丟幀,因為此時兩幀之間的間隔仍然小于 16.67ms,理論上用戶感知不大。

優點

  • 方案簡單,僅需設置 preferredFramesPerSecond 為固定值 60 即可
  • 兼容之前的指標。依然可以計算 FPS 指標,對于刷新率高于 60Hz 的情況統一認為刷新率為 60Hz

缺點

  • 由于只能監控最高 60Hz 的情況,無法評估更高刷新率下一些微小丟幀對用戶體驗帶來的影響,也無法評估對高刷屏的一些優化所帶來的技術影響
  • 在低刷新率時,MainRunLoop 依然會以 60Hz 運行,對功耗有一定影響

方案二 [Pass]

通過一些手段,可以替換驅動 display_timer_callback 的 Source 1 信號的回調,使用它來準確監聽 VSync 信號,實現對動態幀率的準確監控。

優點

  • 理論上最精確的監控方案
  • 對功耗的影響最小,回調頻率只有在屏幕刷新率實際升高時才會隨之提升

缺點

  • 使用了私有 API
  • FPS 指標從此不再適用
  • VSync 信號目前和渲染流程不完全匹配,雖然精確但不一定實用

方案三 [Pick]

通過在 CADisplayLink 回調中確認 duration 參數,計算得到當前屏幕的實時刷新率,并修改 preferredFrameRateRange 來進行跟蹤。

優點

方案相對簡單,只需在每次回調中更新 DisplayLink 對象的 preferredFrameRateRange 屬性即可

缺點

  • 由于動態幀率的存在,FPS 指標可以反映實時屏幕刷新情況,但是聚合后的意義不大,消費時需要區分特定機型/場景
  • 觀察到目前的最小回調頻率為 60Hz,也就是說無法確認 ProMotion 屏幕在 48Hz、30Hz 甚至更低刷新率下的表現
  • 在低刷新率時,MainRunLoop 依然會以 60Hz 運行,對功耗有一定影響

需要注意的是,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,監控準確性也較高,缺點相對來說可以接受。

FPS 的替代指標

考慮到在 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 的計算方案:

  1. 計算上一幀的幀時間戳與上上一幀的目標幀時間戳得到上一幀的 Hitch Time
  2. 確定該幀是否是在滑動中渲染
  3. 累計得到整體的 Hitch Frame,與累積的幀間隔相比,得到 (Scroll) Hitch Time Ratio

關鍵場景提升幀率

在測試過程中筆者發現,系統 App 滑動時是穩定以最高刷新率 120Hz 運行的:

而第三方 App 即便設置了 CADisableMinimumFrameDurationOnPhonetrue 也無法穩定以滿幀率滑動(經過驗證,這一點在 iOS 15.4 beta 系統上依然成立)。

通過利用 iOS 15 引入的新 API,我們可以在關鍵場景如滑動、轉場、動畫過程中主動解鎖更高/限制更低的動態幀率,從而優化流暢度或者優化功率,提升用戶體驗目標。

滑動中穩定 120Hz

首先,筆者希望非系統 App 也可以盡可能實現滑動中穩定 120Hz 刷新。

結合上述分析,這一點可以用 CADisplayLink 來實現。這里筆者提出兩種可能方案僅供參考:

  1. 創建 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 設置動態幀率

目前蘋果只提供了修改 CAAnimation 動畫幀率的 API,設置 CAAnimation.preferredFrameRateRange 即可改變其對屏幕刷新率的影響。

  • 對于用戶感知明顯的,如轉場動畫,可以設置為 120Hz。
  • 對于感知不明顯的,如旋轉動畫,可以降低其幀率,比如設置為 30Hz。

但是,和 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] 時,屏幕刷新率為 60Hz
  • speed 取 [161, 320] 時,屏幕刷新率為 80Hz
  • speed 取 [321, +∞) 時,屏幕刷新率為 120Hz

筆者僅在 iPhone 13 Pro 上測試了平移動畫的場景,以上數據僅供參考。

最后,對于其他的常見的動畫 API,例如 UIView.animateWithDuration、UIViewPropertyAnimator 等,則沒有提供對應 API 進行修改。理論上也可以通過某些手段拿到這些上層 API 所創建的 CAAnimation 對象來實現修改。

手勢/轉場等其他場景解鎖 120Hz

其他場景需要控制動態幀率的也可以通過手動修改 CADisplayLink 的 preferredFramePerSecond/preferredFrameRateRange 屬性來實現,其實現和通過監聽 RunLoop 來修改滑動幀率基本相同。

UIGestureRecognizer 常被用于實現的交互式動畫。經過測試,發現在觸發手勢回調的同時啟用一個解鎖了頻率的 CADisplayLink 也可以間接提高 UIGestureRecognizer 的回調頻率,從而實現更高幀率的交互動畫。

對于轉場的場景,一個簡單的方案是 swizzle UIViewController 的生命周期消息,在出現/消失的節點啟用/停用 CADisplayLink 幀率的解鎖,從而實現通用的頁面轉場動畫幀率解鎖方案。

Flutter 官方也計劃提供類似 API 讓應用側可以針對不同的場景(滑動、動畫 etc)動態切換屏幕刷新率:https://github.com/flutter/flutter/issues/90675

上線收益

基于上述思路,筆者所在團隊在國際化短視頻業務落地了優化項目,經過實驗驗證:

  • 大盤滑動幀率 P50 從 81.57 上升至 112.2
  • 核心業務指標也有一定收益

結語

近年來,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


https://mp.weixin.qq.com/s/gMxTq0_nmE-xW7GA3pkBJg

最多閱讀

iOS 性能檢測新方式?——AnimationHitches 8月以前  |  18029次閱讀
快速配置 Sign In with Apple 2年以前  |  5484次閱讀
APP適配iOS11 3年以前  |  4436次閱讀
App Store 審核指南[2017年最新版本] 3年以前  |  4262次閱讀
所有iPhone設備尺寸匯總 3年以前  |  4184次閱讀
使用 GPUImage 實現一個簡單相機 3年以前  |  3915次閱讀
開篇 關于iOS越獄開發 3年以前  |  3794次閱讀
在越獄的iPhone設置上使用lldb調試 3年以前  |  3719次閱讀
給數組NSMutableArray排序 3年以前  |  3642次閱讀
使用ssh訪問越獄iPhone的兩種方式 3年以前  |  3346次閱讀
UITableViewCell高亮效果實現 3年以前  |  3344次閱讀
關于Xcode不能打印崩潰日志 3年以前  |  3242次閱讀
使用ssh 訪問越獄iPhone的兩種方式 3年以前  |  3083次閱讀
為對象添加一個釋放時觸發的block 3年以前  |  2857次閱讀
使用最高權限操作iPhone手機 3年以前  |  2828次閱讀

手機掃碼閱讀
18禁止午夜福利体验区,人与动人物xxxx毛片人与狍,色男人窝网站聚色窝
<蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>