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.67msXR的屏幕刷新率為固定的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打包發送給RenderServer5.隨后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-10077EliminateanimationhitcheswithXCTesthttps://developer.apple.com/videos/play/wwdc2020/10077
WWDC21-10147Optimizeforvariablerefreshratedisplayshttps://developer.apple.com/videos/play/wwdc2021/10147/
OptimizingProMotionRefreshRatesforiPhone13ProandiPadProhttps://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咨詢相關信息或者直接發送簡歷內推!