我們可以通過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
類。
其繼承自InheritedWidget
,自身並沒有重寫createElement
方法,從flutter三棵樹的角度講,對應的Element
即為InheritedElement
。有兩個屬性,data和child,我們可以從data中獲取一些裝置/系統相關的屬性。
另外還有兩個比較重要的方法:
此方法直接返回_MediaQueryFromWindow
物件,後面會詳細介紹。
方法裡呼叫了dependOnInheritedWidgetOfExactType,接下來我們詳細分析下背後的呼叫流程。
入參是context
,本例中的主介面是StatelessWidget
,那麼這裡的context
便是StatelessElement
。整體呼叫流程如下:
從_inheritedWidgets
列表中查詢是否有MediaQuery
型別的InheritedElement
,從三棵樹的角度講,就是從當前節點一直向上查詢,找到最近的MediaQuery
控制元件。如果找到,則呼叫dependOnInheritedElement
方法(一般情況下是一定能找到的,下面再詳細介紹)。
此方法負責將找到的InheritedElement
(也就是MediaQuery
對應的Element
)存起來,並且呼叫InheritedElement#updateDependencies
方法。
最後兩個方法很簡單,其作用是將主頁對應的StatelessElement
儲存到了MediaQuery
對應的InheritedElement#_dependents
中。
研究完MediaQuery.of(context)
背後的原理,我們可以知道:通過呼叫of方法,主介面對應的Element
和MediaQuery
建立了繫結關係,MediaQuery
對應的InheritedElement
儲存了主介面Element
的參照。
當介紹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
對應的Element
的updateChild
方法,簡單看下updateChild
的內部處理規則:
對MediaQueryFromWindow而言,每次都會建立新的MediaQuery Widget,根據Element#updateChild原始碼(不是本文討論重點,不再詳細分析其原始碼)得知,最終會呼叫MediaQuery對應的Element的update方法。
經過一系列的跳轉過後,最終會呼叫到下面的兩個核心方法:
上面介紹的MediaQuery.of(context)
方法最終會把入參Context
放到_dependents
變數裡,而這裡會遍歷這個map
,呼叫每一個Context
的didChangeDependecies
方法,didChangeDependecies
會將此Context
置為dirty狀態,下一幀來臨時會被重新繪製,並呼叫此Context
的build
方法。
所以,破案了,當鍵盤彈起/隱藏時快遞主頁會被rebuild的原因找到了!
整體的rebuild呼叫流程如下,感興趣的可以結合這個呼叫流程圖去看原始碼:
研究過原始碼後,解決方案就變的很簡單。
useInheritedMediaQuery
屬性為true,並在最外面包一層MediaQuery
,讓WidgetsApp
建立時使用MediaQuery
,而不去使用監聽了application尺寸變化的_MediaQueryFromWindow
控制元件。MediaQuery.of(context)
方法,可以使用對應的替代方法,比如本例可以採用下面的程式碼進行替代,注意單位的轉換。MediaQuery.of(context)
方法,可以使用Builder
控制元件包裹下,of方法的入參傳入此Builder
的context
即可,這樣被rebuild僅是Builder
控制元件包裹下的widget子樹。app介面逐漸複雜時,我們不得不考慮去優化介面效能。本文中介紹的例子在開發中是很常見的,如果不瞭解MediaQuery.of的機制,可能會引起大量使用此方法的介面發生重繪操作,造成頁面卡頓、影格率下降。我們詳細分析了背後的原始碼邏輯,介紹瞭解決辦法,希望能給大家的調優工作提供些許幫助。
作者:京東物流 沈明亮
來源:京東雲開發者社群