Flutter調優--深入探究MediaQuery引起界面Rebuild的原因及解決辦法 | 京東雲技術團隊

前言

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

典型例子

以快遞App中的查快遞場景舉例,首頁用MediaQuery.of(context).padding.top獲取了狀態欄高度,用戶點擊“查快遞”按鈕會跳轉到查快遞界面,在查快遞界面,用戶輸入單號可進行查詢操作。

99ee34767149efcdb4c81258d84649c76ec7d0b5.jpeg
當首頁的build方法被調用時,會輸出我們提前加好的日誌。我們發現,當查快遞界面的鍵盤彈出時,首頁的build方法被調用了多次:

1c71b25e3294787a554ff654302a1950b05f1e2c.png

主界面的build代碼如下:

0e60e8a0008a952db4058f2f6c7c7f4d2156d1bf.png

源碼探究

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

MediaQuery

ca93e1de11acadcdacc1be8bf1621c53ab4d4e67.png

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

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

fromWindow(key : Key, child : Widget)

5b724d4ff9ae1364473ce47a81a5c0c4fcd247ef.png

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

of(context : BuildContext)

50.png

方法裏調用了dependOnInheritedWidgetOfExactType,接下來我們詳細分析下背後的調用流程。

MediaQuery.of(context) 調用流程

入參是context,本例中的主界面是StatelessWidget,那麼這裏的context便是StatelessElement。整體調用流程如下:

845592be3d798d7f664f343d1c3bc2e28b2ed114.png

dependOnInheritedWidgetOfExactType

0921532a03e10a237cce55d11b688cac76e3744e.png

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

dependOnInheritedElement

3475047fddc5392b520ed244eaf9bde411a21349.png

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

updateDependencies

fc0db47c49a1f82bde4797e316087c40bda77d35.png

setDependencies

a33b8a6c17284ec860c808104a5e9b5796a3e247.png

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

研究完MediaQuery.of(context)背後的原理,我們可以知道:通過調用of方法,主界面對應的ElementMediaQuery建立了綁定關係,MediaQuery對應的InheritedElement存儲了主界面Element的引用。

Rebuild起點

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

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

b99a11825cca09b3db11d7d08c0f2f7e97b1b83a.png

會首先檢查widget.useInheritedMediaQuery,這個屬性默認爲false。如果你創建MaterialApp/CupertinoApp時,沒有設置useInheritedMediaQuery屬性,或者設置了這個屬性爲null,但找不到MediaQueryData,那麼這裏就會調用MediaQuery.fromWindow方法。

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

628215f6ff6f55a7e4253fd82aec42af96564435.png

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

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

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

a33b8a6c17284ec860c808104a5e9b5796a3e247.png

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

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

beb27e10f74c988d10e0de4fda450f52aec77be7.png

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

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

整體的rebuild調用流程如下,感興趣的可以結合這個調用流程圖去看源碼:

bf648f2c2399def55a9fdb1a20260ef2fe858394.png

避免rebuild的辦法

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

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

f048696ba22681d3cefab511748656b38a3d1098.png

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

17ebf14ed1e437106cd05b5c17f2da2aeb815213.png

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

5e9bafc27455cbba7c69653d1052f73c1ed71fa5.png

總結

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

作者:京東物流 沈明亮

來源:京東雲開發者社區

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章