一、無(wú)用的知識(shí)
首先科普無(wú)用的知識(shí),說(shuō)起高刷新率,就不得不提兩個(gè)詞匯 ProMotion 和 LTPO 。 ProMotion 是 iOS 在支持 120hz 之后出現(xiàn)的動(dòng)態(tài)刷新率支持,也就是不同場(chǎng)景使用不同的屏幕刷新率,從而實(shí)現(xiàn)體驗(yàn)上提升的同時(shí)降低了電池的消耗。

LTPO(low-temperature Polycrystalline oxide) 允許顯示器動(dòng)態(tài)改變屏幕刷新率 ,而早在三星S20 Ultra、OPPO Find X3系列、一加 9 Pro 等系列產(chǎn)品上都率先采用了這種顯示技術(shù),但是實(shí)際上大家在 LTPO 又有不同的技術(shù)調(diào)教,從而出現(xiàn)了我們后續(xù)要聊的問(wèn)題。

例如 LTPO 1.0 時(shí)代可能大部分實(shí)現(xiàn)都只是強(qiáng)硬的根據(jù)場(chǎng)景鎖死 60Hz/120Hz 的刷新率,而 LTPO 2.0 開(kāi)始各大廠家則是升級(jí)了自適應(yīng)策略,例如最常見(jiàn)的就是升級(jí)了滑動(dòng)變頻:

當(dāng)然,除了最常見(jiàn)的滑動(dòng), LTPO 2.0 上廠家可能還會(huì)有對(duì)動(dòng)畫(huà)、視頻、文字輸入、應(yīng)用切換等場(chǎng)景進(jìn)行不同的升頻和降頻策略,而其實(shí)介紹上面這些的原因是:
- 蘋(píng)果 ProMotion 是基于官方實(shí)現(xiàn)的統(tǒng)一方案;
- Android 的 LTPO 是基于供應(yīng)商硬件后Android OEM 廠家自主調(diào)教的實(shí)現(xiàn);
以上部分資料來(lái)自《LTPO到底是不是真的省電?-一加LTPO 2.0上手體驗(yàn)》
所以這也造就了 Flutter 需要在 Android 和 iOS 上進(jìn)行單獨(dú)適配的主要原因。
二、Android
前面介紹里引用了一加的 LTPO 2.0 實(shí)現(xiàn)是有原因的,首先知道自適應(yīng)屏幕刷新率是 OEM 廠商自主調(diào)教,也就是理論上作為 App 是不需要做任何適配,因?yàn)楦S Android 就行,Android 本身也是使用 Skia 渲染。
但是往往事與愿違,在 Flutter 關(guān)于 高刷問(wèn)題 最先被提及的就是一加,那時(shí)候基本都引用了 《The OnePlus 7 Pro’s 90Hz Refresh Rate Doesn’t Support Every App 》 這篇文章:
一加 7 Pro 的 90 fps 模式對(duì)于某些 App 而言只有 60 fps,要在所有 App 上都強(qiáng)制 90 fps,需要執(zhí)行
adb shell settings put global oneplus_screen_refresh_rate 0命令, 相比之下 Pixel 4 無(wú)需任何更改就直接可以支持渲染 90 fps 的 Flutter App。
也就是問(wèn)題最開(kāi)始是在一加的 90 fps 上不支持,而社區(qū)通過(guò)和一加的溝通得到的回復(fù)是:
一加7 Pro 為了平衡性能和功耗,采用的是基于 Android 定制自己的幀率控制邏輯,一般屏幕會(huì)以高幀率工作,但在某些場(chǎng)景下系統(tǒng)會(huì)切回到低幀率,而由于引入了這種機(jī)制,可能會(huì)出現(xiàn)當(dāng) App 希望屏幕以高幀率運(yùn)行時(shí)卻被系統(tǒng)強(qiáng)制設(shè)置為低幀率的問(wèn)題。
那如何通過(guò) App 設(shè)置 fps ? 如果應(yīng)用程序需要設(shè)置幀速率,那首先需要通過(guò)
getSupportedModes()獲取目前屏幕支持的模式列表,然后遍歷列表,根據(jù)找到想要使用的分辨率和刷新率的modeId,賦值給窗口的preferredDisplayModeId。
所以基于這個(gè)問(wèn)題修復(fù)的方案,社區(qū)內(nèi)提出了 flutter_displaymode 插件,插件主要提供了獲取 Display.Mode 和設(shè)置 preferredDisplayModeId 的支持,用于臨時(shí)解決類似 一加7 Pro 上的這種刷新率問(wèn)題。
/// On OnePlus 7 Pro:
/// #1 1080x2340 @ 60Hz
/// #2 1080x2340 @ 90Hz
/// #3 1440x3120 @ 90Hz
/// #4 1440x3120 @ 60Hz
/// On OnePlus 8 Pro:
/// #1 1080x2376 @ 60Hz
/// #2 1440x3168 @ 120Hz
/// #3 1440x3168 @ 60Hz
/// #4 1080x2376 @ 120Hz
那什么是 PreferredDisplayModeId ?通過(guò)官方的 《setframerate-vs-preferreddisplaymodeid》 可以了解:
WindowManager.LayoutParams.preferredDisplayModeId是 App 向平臺(tái)設(shè)置所需幀率的一種方式,因?yàn)橛袝r(shí)候 App 只想改變刷新率,但是不需要更改其他顯示模式如分辨率等。類似設(shè)置還有setFrameRate(),使用setFrameRate()代替preferredDisplayModeId會(huì)更簡(jiǎn)單, 因?yàn)?code>setFrameRate() 可以自動(dòng)匹配顯示模式列表里具有特定幀速率的模式。
那為什么不直接用 setFrameRate ?其中之一因?yàn)檫@是一個(gè) Target 很高的 API。

PS:這里和大家介紹一位 Flutter 大佬, 事實(shí)上這個(gè) 問(wèn)題 作為 GDE 的 AlexV525 大佬跟進(jìn)了很久,上面的插件也是他在參與維護(hù),同時(shí)也恭喜?? 大佬獲得 Google Open Source Peer Bonus Winners in 2022 的??。
但是在安穩(wěn)一段時(shí)間之后,一加 9 pro 上了 LTPO 和 ColorOS,之前的 adb 命令在新來(lái)的 ColorOS 上也隨之失效,不過(guò)不要擔(dān)心,后續(xù)發(fā)現(xiàn)這個(gè)其實(shí)是官方的一個(gè)bug,在 ColorOS 11_A.06 版本后修復(fù)了該問(wèn)題,也就是插件還可以繼續(xù)生效。
而如今兩年快過(guò)去了,對(duì)于此問(wèn)題還是只能通過(guò)插件去臨時(shí)解決,因?yàn)閺墓俜降膽B(tài)度上好像并不是特別支持嵌入這種方式:
- Flutter 應(yīng)該將刷新率控制交給 OS 處理, Flutter 不應(yīng)該對(duì)單個(gè)刷新率去進(jìn)行 hardcode;
- 處理類似 OEM 廠商問(wèn)題最好通過(guò)插件解決而不是 Flutter Engine ;
在這方面的處理思路和決策感覺(jué)和 iOS 差異較大,大概也有平臺(tái)限制的因素吧。
事實(shí)上不同廠商對(duì)于 LTPO 的實(shí)現(xiàn)邏輯確實(shí)差異性很大,比如下圖是一加10pro 在 LTPO 渲染是會(huì)選擇性壓縮或者丟棄一些冗余的指令。

我們知道 Flutter 是把 Widget 渲染到 Surface 上,在這點(diǎn)上和使用 SurfaceView 和 OpenGL 實(shí)現(xiàn)的 Google Map 很類似,而經(jīng)過(guò)測(cè)試 Google Map 在這些設(shè)備上,不特殊設(shè)置和 Flutter 一樣也只能以 60hz 渲染運(yùn)行。
對(duì)于 OEM 廠商,在調(diào)教的 LTPO 上有權(quán)決定是否允許 App 使用更高的刷新率,即使 App 要求更高的刷新率,這難道又是一個(gè)“白名單模式”?
所以如果需要讓 Surface 在某些特殊設(shè)備支持 90/120 hz 運(yùn)行,就需要使用 preferredDisplayModeId 或者 setFrameRate , 同時(shí)前提是廠商沒(méi)有強(qiáng)行鎖死幀率。
一些手機(jī)廠商,會(huì)因?yàn)?“馴龍” 和控溫的需要,都有自己的“穩(wěn)幀”策略,甚至強(qiáng)制鎖死幀率并且顯示假幀率。

而在 #78117 討論的最終討論結(jié)果就是:Flutter 并不會(huì)特別針對(duì)這部分廠商去特意做適配,如果需要,你可以通過(guò)第三方插件來(lái)解決,當(dāng)然在我的測(cè)試中,目前大部分設(shè)備的刷新率支持上還是正常。
同時(shí)在早期 Flutter 的 IntelliJ 插件也存在 bug ,即使應(yīng)用程序以 90 fps 運(yùn)行,Android Studio / IntelliJ 中的 Flutter 插件也會(huì)給出 60 fps ,當(dāng)然這個(gè)問(wèn)題在后續(xù)的 #4289 上得到了解決。
額外補(bǔ)充一種情況,廠家通常還會(huì)檢測(cè)
SurfaceView/TextureView是否超過(guò)屏幕的一半,因?yàn)檫@時(shí)候可能代表著你正在看視頻或者玩游戲,而這時(shí)候可能也會(huì)降低幀率。
最后,如果對(duì) Flutter 在 Android 上關(guān)于刷新率部分的代碼感性起,可以查閱:vsync_waiter.cc 、vsync_waiter_android.cc 、android_display.cc
三、iOS
回到 iOS 上,ProMotion 的支持思路就和原生不大一樣,因?yàn)樵趧偼瞥?ProMotion 時(shí)官方就在 《刷新率優(yōu)化上》 對(duì) ProMotion 的適配提及過(guò):
如果使用的是以下這些默認(rèn)框架的話,對(duì)于這些刷新率的變化 App 而無(wú)需進(jìn)行任何更改:
但是對(duì)于 Flutter 而言并沒(méi)用使用系統(tǒng)所提供的原生控件,所以目前需要在 Info.plist 文件中配置以下參數(shù),從而啟用關(guān)于 CADisplayLink 和 CAAnimation 上高于 120Hz 的相關(guān)支持:
<key>CADisableMinimumFrameDurationOnPhone</key><true/>
而在 Flutter 官方的討論記錄 flutter.dev/go/variable-refresh-rate 和 issue #90675 相關(guān)回復(fù)里可以看到,官方目前的決策是先使用 #29797 的實(shí)現(xiàn)解決,通過(guò)調(diào)整 vsync_waiter_ios.mm 相關(guān)的內(nèi)容來(lái)實(shí)現(xiàn)高刷支持:
- (void)setMaxRefreshRateIfEnabled {
NSNumber* minimumFrameRateDisabled =
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CADisableMinimumFrameDurationOnPhone"];
if (!minimumFrameRateDisabled) {
return;
}
double maxFrameRate = fmax([DisplayLinkManager displayRefreshRate], 60);
double minFrameRate = fmax(maxFrameRate / 2, 60);
if (@available(iOS 15.0, *)) {
display_link_.get().preferredFrameRateRange =
CAFrameRateRangeMake(minFrameRate, maxFrameRate, maxFrameRate);
} else if (@available(iOS 10.0, *)) {
display_link_.get().preferredFramesPerSecond = maxFrameRate;
}
}
- 默認(rèn)情況下幀率會(huì)是設(shè)置為 60;
- 在支持 ProMotion 的設(shè)備上會(huì)設(shè)置為顯示器支持的最大刷新率;
- 在 iOS 15 及更高版本上,還增加了設(shè)置幀率范圍,其中 preferred 和 max 均為屏幕支持的最大值,min 為最大值的 1/2;
其實(shí)在之前的討論中還有如 #29692 這種更靈活的實(shí)現(xiàn),也就是探索讓 Flutter Engine 根據(jù)渲染和使用場(chǎng)景去自己選擇當(dāng)前的幀率,因?yàn)樯鐓^(qū)認(rèn)為:對(duì)于普通用戶來(lái)說(shuō),在不知道平臺(tái)、性能等的情況下讓開(kāi)發(fā)者自己選擇正確的刷新并不靠譜,所以通過(guò) Engine 完成適配才是未來(lái)的方向。
當(dāng)然,基于社區(qū)里目前迫切地想讓 Flutter 得到 120Hz 的能力,所以會(huì)暫時(shí)優(yōu)先采用上述的 CADisableMinimumFrameDurationOnPhone 來(lái)解決目前的困境,這也是 iOS 官方提倡的方式。
另外值得一提的是,iOS 15.4 上的蘋(píng)果修復(fù)了導(dǎo)致 ProMotion 相關(guān)的 bug ,因?yàn)樵谶@之前會(huì)出現(xiàn) ProMontion 并不是完全開(kāi)放第三方支持的詭異情況,而在 iOS 15.4 后, iOS 會(huì)自動(dòng)為 App 中所有自定義動(dòng)畫(huà)內(nèi)容啟用120Hz刷新率,所以會(huì)出現(xiàn)一個(gè)神奇的情況:
- 在 iOS 15.4 上, App 可以兼容得到 120Hz 動(dòng)畫(huà);
- 在 iOS 15.4 之前,部分動(dòng)畫(huà)支持 ProMotion;

四、最后
可以看到就目前來(lái)說(shuō),高刷對(duì)于 Flutter 仍舊是一個(gè)挑戰(zhàn),作為獨(dú)立渲染引擎,這也是 Flutter 無(wú)法逃避的問(wèn)題,就目前情況來(lái)看:
- 在 Android 上你不需要做任何調(diào)整,如果遇到特殊設(shè)備或者系統(tǒng),建議通過(guò) flutter_displaymode 來(lái)解決;
- 在 iOS 上你可以添加
CADisableMinimumFrameDurationOnPhone來(lái)粗暴解決,然后等待 #29797 相關(guān)內(nèi)容的合并發(fā)布;
最后,如果關(guān)于高刷方面你還有什么資料或者想法,歡迎留言評(píng)論討論。