Flutter調優--深入探究MediaQuery引起介面Rebuild的原因及解決辦法

2023-05-29 18:00:43

前言

我們可以通過MediaQuery.of(context)方法獲取到一些裝置和系統的相關資訊,比如狀態列的高度、當前是否是黑暗模式等等,使用起來相當方便,但是也要注意可能引起的頁面rebuild問題。本文會介我們可以通過MediaQuery.of(context)方法獲取到一些裝置和系統的相關資訊,比如狀態列的高度、當前是否是黑暗模式等等,使用起來相當方便,但是也要注意可能引起的頁面rebuild問題。本文會介紹一個典型的例子,並深入原始碼來探討引起rebuild的原因,最後介紹避免rebuild的幾個辦法。
紹一個典型的例子,並深入原始碼來探討引起rebuild的原因,最後介紹避免rebuild的幾個辦法。

典型例子

以快遞App中的查快遞場景舉例,首頁用MediaQuery.of(context).padding.top獲取了狀態列高度,使用者點選「查快遞」按鈕會跳轉到查快遞介面,在查快遞介面,使用者輸入單號可進行查詢操作。


當首頁的build方法被呼叫時,會輸出我們提前加好的紀錄檔。我們發現,當查快遞介面的鍵盤彈出時,首頁的build方法被呼叫了多次:

主介面的build程式碼如下:

原始碼探究

既然是因為主介面在build方法裡使用了MediaQuery.of(context),從而導致當鍵盤彈出/隱藏時進行rebuild操作,那麼就先來看下MediaQuery類。

MediaQuery

其繼承自InheritedWidget,自身並沒有重寫createElement方法,從flutter三棵樹的角度講,對應的Element即為InheritedElement。有兩個屬性,data和child,我們可以從data中獲取一些裝置/系統相關的屬性。

另外還有兩個比較重要的方法:

fromWindow(key : Key, child : Widget)

此方法直接返回_MediaQueryFromWindow物件,後面會詳細介紹。

of(context : BuildContext)

方法裡呼叫了dependOnInheritedWidgetOfExactType,接下來我們詳細分析下背後的呼叫流程。

MediaQuery.of(context) 呼叫流程

入參是context,本例中的主介面是StatelessWidget,那麼這裡的context便是StatelessElement。整體呼叫流程如下:

dependOnInheritedWidgetOfExactType

_inheritedWidgets列表中查詢是否有MediaQuery型別的InheritedElement,從三棵樹的角度講,就是從當前節點一直向上查詢,找到最近的MediaQuery控制元件。如果找到,則呼叫dependOnInheritedElement方法(一般情況下是一定能找到的,下面再詳細介紹)。

dependOnInheritedElement

此方法負責將找到的InheritedElement(也就是MediaQuery對應的Element)存起來,並且呼叫InheritedElement#updateDependencies方法。

updateDependencies

setDependencies

最後兩個方法很簡單,其作用是將主頁對應的StatelessElement儲存到了MediaQuery對應的InheritedElement#_dependents中。

研究完MediaQuery.of(context)背後的原理,我們可以知道:通過呼叫of方法,主介面對應的ElementMediaQuery建立了繫結關係,MediaQuery對應的InheritedElement儲存了主介面Element的參照。

Rebuild起點

當介紹dependOnInheritedWidgetOfExactType方法時,我們提道:從當前節點往父節點尋找,一般情況下是一定能找到的MediaQuery控制元件的。這是因為在WidgetsApp裡會自動給我們建立一個根MediaQuery

main方法裡,無論使用CupertinoApp還是MaterialApp,最後都會在內部建立WidgetsApp。我們直接看_WidgetsAppState#build方法裡的一個程式碼片段:

會首先檢查widget.useInheritedMediaQuery,這個屬性預設為false。如果你建立MaterialApp/CupertinoApp時,沒有設定useInheritedMediaQuery屬性,或者設定了這個屬性為null,但找不到MediaQueryData,那麼這裡就會呼叫MediaQuery.fromWindow方法。

上面介紹MediaQuery#fromWindow時,我們知道它會建立_MediaQueryFromWindow控制元件。

_MediaQueryFromWindow的程式碼不是很多,把和本文相關的程式碼全部貼出來了,大家可以自己看下,程式碼如上圖所示。

build方法裡建立了MediaQuery控制元件,並實現了didChangeMetrics方法,當手機發生旋轉、鍵盤彈出/隱藏時就會呼叫此方法,didChangeMetrics內部又呼叫了setSate,從而導致build方法被重新呼叫。

通過flutter三顆樹的原理我們可以知道,上述所說的「build方法被重新呼叫」涉及到MediaQueryFromWindow對應的ElementupdateChild方法,簡單看下updateChild的內部處理規則:

對MediaQueryFromWindow而言,每次都會建立新的MediaQuery Widget,根據Element#updateChild原始碼(不是本文討論重點,不再詳細分析其原始碼)得知,最終會呼叫MediaQuery對應的Element的update方法。

經過一系列的跳轉過後,最終會呼叫到下面的兩個核心方法:

上面介紹的MediaQuery.of(context)方法最終會把入參Context放到_dependents變數裡,而這裡會遍歷這個map,呼叫每一個ContextdidChangeDependecies方法,didChangeDependecies會將此Context置為dirty狀態,下一幀來臨時會被重新繪製,並呼叫此Contextbuild方法。

所以,破案了,當鍵盤彈起/隱藏時快遞主頁會被rebuild的原因找到了!

整體的rebuild呼叫流程如下,感興趣的可以結合這個呼叫流程圖去看原始碼:

避免rebuild的辦法

研究過原始碼後,解決方案就變的很簡單。

  • 自定義useInheritedMediaQuery屬性為true,並在最外面包一層MediaQuery,讓WidgetsApp建立時使用MediaQuery,而不去使用監聽了application尺寸變化的_MediaQueryFromWindow控制元件。

  • 避免在頁面中使用MediaQuery.of(context)方法,可以使用對應的替代方法,比如本例可以採用下面的程式碼進行替代,注意單位的轉換。

  • 如果必須要使用MediaQuery.of(context)方法,可以使用Builder控制元件包裹下,of方法的入參傳入此Buildercontext即可,這樣被rebuild僅是Builder控制元件包裹下的widget子樹。

總結

app介面逐漸複雜時,我們不得不考慮去優化介面效能。本文中介紹的例子在開發中是很常見的,如果不瞭解MediaQuery.of的機制,可能會引起大量使用此方法的介面發生重繪操作,造成頁面卡頓、影格率下降。我們詳細分析了背後的原始碼邏輯,介紹瞭解決辦法,希望能給大家的調優工作提供些許幫助。

作者:京東物流 沈明亮

來源:京東雲開發者社群