Flutter 120hz 高刷新率在 Android 和 iOS 上的調(diào)研總結(jié)

一、無(wú)用的知識(shí)

首先科普無(wú)用的知識(shí),說(shuō)起高刷新率,就不得不提兩個(gè)詞匯 ProMotionLTPO 。 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)上和使用 SurfaceViewOpenGL 實(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)于 CADisplayLinkCAAnimation 上高于 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)論討論。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容