前言
我們可以通過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的機制,可能會引起大量使用此方法的界面發生重繪操作,造成頁面卡頓、幀率下降,我們詳細分析了背后的原始碼邏輯,介紹了解決辦法,希望能給大家的調優作業提供些許幫助,
作者:京東物流 沈明亮
來源:京東云開發者社區
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/553795.html
標籤:其他
下一篇:返回列表
