app界面逐漸複雜時,我們不得不考慮去優化界面性能。本文中介紹的例子在開發中是很常見的,如果不瞭解MediaQuery.of的機制,可能會引起大量使用此方法的界面發生重繪操作,造成頁面卡頓、幀率下降。我們詳細分析了背後的源碼邏輯,介紹瞭解決辦法,希望能給大家的調優工作提供些許幫助。 ...
前言
我們可以通過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方法,主界面對應的Element
和MediaQuery
建立了綁定關係,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
對應的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調用流程如下,感興趣的可以結合這個調用流程圖去看源碼:
避免rebuild的辦法
研究過源碼後,解決方案就變的很簡單。
- 自定義
useInheritedMediaQuery
屬性為true,併在最外麵包一層MediaQuery
,讓WidgetsApp
創建時使用MediaQuery
,而不去使用監聽了application尺寸變化的_MediaQueryFromWindow
控制項。
- 避免在頁面中使用
MediaQuery.of(context)
方法,可以使用對應的替代方法,比如本例可以採用下麵的代碼進行替代,註意單位的轉換。
- 如果必須要使用
MediaQuery.of(context)
方法,可以使用Builder
控制項包裹下,of方法的入參傳入此Builder
的context
即可,這樣被rebuild僅是Builder
控制項包裹下的widget子樹。
總結
app界面逐漸複雜時,我們不得不考慮去優化界面性能。本文中介紹的例子在開發中是很常見的,如果不瞭解MediaQuery.of的機制,可能會引起大量使用此方法的界面發生重繪操作,造成頁面卡頓、幀率下降。我們詳細分析了背後的源碼邏輯,介紹瞭解決辦法,希望能給大家的調優工作提供些許幫助。
作者:京東物流 沈明亮
來源:京東雲開發者社區