目錄;
1. 內存和虛擬機的概念
2. 內存檢測方案
內存泄露檢測原理, 工具的代碼具體實現, 具體的內存泄露案例
3. 優化手段
4. 優化項的總結
1. 內存和虛擬機的概念
flutter從使用的語言上,可以分成3大部分,
- Framework層 由Dart編寫,開發者接觸到頂層,用于應用層開發
- Engine 層,由C/C++編寫,主要進行圖形渲染
- Embedder層,由植入層語言編寫,如iOS使用Objective-C/swift,Android使用java
當我們從進程角度談論flutter應用的內存時,指的是這個三者所有的內存的總和。
Dart虛擬機的理解:
在Flutter中,內存管理是由Dart虛擬機負責的。Dart虛擬機使用垃圾回收器來管理內存,這意味著開發者不需要手動分配和釋放內存。垃圾回收器會自動識別不再使用的對象并將其回收,從而釋放內存。
Flutter應用程序中的內存分為兩種類型:堆內存和棧內存。堆內存用于存儲對象,棧內存用于存儲臨時變量。在Flutter中,大多數對象都是在堆內存中創建的,而臨時變量則是在棧內存中創建的。當一個對象不再被引用時,垃圾回收器會自動回收它所占用的堆內存。
Dark虛擬機:
Dart虛擬機是一個支持JIT編譯和AOT編譯的強類型腳本語言虛擬機,它可以運行在多種平臺上,包括PC、移動設備和嵌入式設備上。Dart虛擬機內置了大量的標準庫和工具,包括Dart SDK、Dart Analyzer、Dartfmt等,方便開發者進行開發和維護。Dart虛擬機支持使用各種IDE,包括Flutter、IntelliJ IDEA等,提供了強大的代碼編輯和調試功能,大大提高了開發效率。
Dart虛擬機的設計架構具有良好的可擴展性和可定制性,支持諸如Dart的Native擴展等機制,可以在運行時加載和卸載模塊進行動態擴展。Dart虛擬機還支持一些高級語言特性,如異步編程和生成器,使得開發者可以編寫更加高效的、帶有處理異步邏輯的程序。同時,Dart虛擬機還支持代碼優化和預編譯等特性,以提高程序的性能和運行效率。
總之,Dart虛擬機是一個具有很多優良特性的強類型腳本語言虛擬機,與Flutter等生態系統緊密結合,為移動應用開發提供了一個全面、高效的解決方案。
重點:
Dart虛擬機在初始化時,會將C++聲明的某個類或者函數和某個函數和Dart中的某個類或者綁定起來,依次注入Dart運行時的全局遍歷中,當Dart代碼執行某一個函數時,便是指向具體的C++對象或者函數。
2. 內存監測方法:
2.1 flutter編譯模式
Flutter支持Release、Profile、Debug編譯3種模式。
- Release模式,使用AOT預編譯模式,預編譯為機器碼,通過編譯生成對應架構的代碼,在用戶設備上直接運行對應的機器碼,運行速度快,執行性能好;此模式關閉了所有調試工具,只支持真機。
- Profile模式,和Release模式類似,使用AOT預編譯模式,此模式最重要的作用是可以用DevTools來檢測應用的性能,做性能調試分析。
- Debug模式,使用JIT(Just in time)即時編譯技術,支持常用的開發調試功能hot reload,在開發調試時使用,包括支持的調試信息、服務擴展、Observatory、DevTools等調試工具,支持模擬器和真機。
2.2 開啟profile模式
Android-Android Studio 開發工具來查看 APP 內存占用; 按照 Flutter 官方的性能優化指南,**Flutter-profile 模式下,使用 devTools **查看 Flutter 內存占用;
圖片過載檢測:切換到 Flutter Inspector,點擊 Highlight Oversized Images:
2.3 、檢測消耗多余內存的圖片
Flutter Inspector:點擊 “Highlight Oversizeded Images”,它會識別出那些解碼大小超過展示大小的圖片,并且系統會將其倒置,這些你就能更容易在 App 頁面中找到它。
3. 優化方向
3. 1 內存泄露原理:(重點)
從渲染原理出發探究Flutter內存泄漏
3.1.1案例1:
例如,存在兩個A,B界面,A界面通過Navigator.push的方式添加B界面,B界面通過Navigator.pop回退到A。 如果B界面因為某些寫法的緣故導致B的渲染樹雖然被從主渲染樹解開后依然無法被釋放,這會導致整個原來B的子樹都無法釋放
3.1.2 內存泄工具設計原理
flutter內存泄漏檢測工具的設計思路是: 借鑒Android LeakCanary,
用WeakPersitentHandle對象, 通過檢測渲染樹節點來檢測內存泄漏
對比界面進入前后的對象,尋找出未被釋放的對象,進而查看未釋放的引用關系(Retaining path或Inbound references),再結合源碼進行分析,最后找到錯誤代碼。
使用Flutter自帶的Observatory縱然可以一個一個查看每個泄漏對象的引用關系,但是對于一個稍微復雜一點的界面而言,最終生成的layer個數是非常龐雜的,想要在Observatory所有的泄漏對象中找到有問題的代碼是一項非常龐雜的任務。
為此我們將這些繁雜的定位工作都進行了可視化。
我們這里將每一幀提交到engine的所有EngineLayer進行了一個記錄,并且以折線圖的形式記錄下來,如果上文說的內存中的layer個數異常的大于使用中的layer個數,那么就可判斷前一個頁面存在有內存泄漏。
Dart代碼中都是通過往ui.SceneBuilder添加EngineLayer的方式去構建渲染樹,那么我們只要檢測c++中內存中EngineLayer的個數,對比當前幀使用的EngineLayer個數,如果內存中的EngineLayer個數長時間大于使用的個數,那么我們可以判斷存在有內存泄漏
內存泄露檢測原理總結:
1). dart代碼都會映射c++中對應的方法
2). 對象存放在 WeakPersitentHandle中
3). 根據渲染原理: 通過檢測渲染樹節點數量來檢測內存泄漏, 對比當前幀使用的EngineLayer個數,如果內存中的EngineLayer個數長時間大于使用的個數
4). 通過WeakPersitentHandle找到調用鏈
內存泄露工具: (重點分析)
內存泄露具體代碼總結:
1. 弱引用Expando
2. 解決訪問的私有屬性Dart vm_service
3. 判斷內存泄露
先通過 vm_service 獲取到 Instance
,遍歷里面的 fields
屬性,找到 _data
字段(注意 _data
是 ObjRef 類型),用同樣的辦法把 _data
字段轉成 Instance
類型(_data 是個數組,Obj 里面有數組的 child 信息)。
遍歷 _data
字段,如果都是 null,表明我們觀測的 key 對象已經被釋放了。如果 item 不為 null,再次把 item 轉為 Instance
對象,取它的 propertyKey
4. 手動調用GC
5.獲取泄漏路徑
6. 優化泄漏路徑, 得到最短泄漏路徑, 通過聚合和分類進行優化
3.2 內存泄漏的常見場景
案例:
目前發現異步執行的代碼的場景(Feature, async/await,methodChan)長期持有傳入的BuildContext,導致 element 被移除后,依然長期存在,最終導致以及關聯的 widget, state 發生泄漏。
3.2.1. 監聽反注冊缺失
排查內存泄漏的過程中,我們發現圖片內存大幅度超出了圖片緩存自身 size 限制的增長,并且不會被 GC 回收,經過排查發現我們封裝的一個底層圖片處理類,注冊了圖片事件流監聽后,并沒有在適當的時機做反注冊處理。
3.2.2. 長列表直接構建列表項
通過對列表數據遍歷的方式,一次生成所有數據對應的 widget 列表,直接塞進 Column 里展示給用戶,當加載了幾頁數據之后,數據量稍大就會輕易導致 OOM 或導致嚴重卡頓。
3.2.3. 延時、持續執行的閉包引用
Flutter 提供的延時和持續執行的對象有 Animation、Timer、Future 等,在結束執行之前,回調函數引用到的相關對象都會被強引用保留在內存中
3.3 內存泄漏的操作步驟和實戰: (重點)
通過Observatory分析內存泄漏
Android Studio 或 VS Code 插件幫我們包裝了相關內存工具,這些工具都基于 debug 模式下 Dart VM service 暴露的接口開發的,Dart VM service 自身也帶有協助排查內存問題的工具 - Dart VM Observatory,attach 之后訪問 service 提供的 http 鏈接即可使用該工具,內存排查功能路徑為:Isolate (main) --> Allocation Profile。
工具有了,如何排查定位導致內存泄漏的問題代碼呢?
對同一個功能或頁面進行反復相同的進入、退出操作;
然后執行強制 GC,查看不同操作后的內存快照;
對比該功能關聯的對象實例增加情況;
如果強制 GC 后實例只增不減或該回收的對象沒有被回收,沒有特殊的延時處理一般就可以判斷相關代碼有問題
3.3 圖片和listview優化
重繪情況,酌情考慮優化方案(控制 setState 粒度、引入全局狀態管理-redux/Provider、拆分 ViewModel、減少 build() 操作 等)
針對這些圖片,你可以指定 cacheWidth 和 cacheHeight 為展示大小,這樣可以讓 flutter 引擎以指定大小解析圖片,減少內存消耗。
針對 ListView item 中有 image 的情況來優化內存
ListView 不會銷毀那些在屏幕可視范圍之外的那些 item,如果 item 使用了高分辨率的圖片,那么它將會消耗非常多的內存。
ListView 在默認情況下會在整個滑動/不滑動的過程中讓子 Widget 保持活動狀態,這一點是通過 AutomaticKeepAlive 來保證,在默認情況下,每個子 Widget 都會被這個 Widget 包裹,以使被包裹的子 Widget 保持活躍。 其次,如果用戶向后滾動,則不會再次重新繪制子 Widget,這一點是通過 RepaintBoundaries 來保證,在默認情況下,每個子 Widget 都會被這個 Widget 包裹,它會讓被包裹的子 Widget 僅僅繪制一次,以此獲得更高的性能。 但,這樣的問題在于,如果加載大量的圖片,則會消耗大量的內存,最終可能使 App 崩潰。
圖片方案是自研的外接紋理方案
雖然實現了一個App內一個內存緩存,并且將紋理和Flutter圖片都存進去了,節省了內存空間,提高了內存使用率,但還是侵入了ImageCache源碼,后續flutter engine的升級和代碼維護,需要有額外的工作。
為什么flutter圖片加載要采用外接紋理方案?
其實Flutter本身已具備加載圖片的能力,Image組件就滿足網絡圖片、本地圖片、文件圖片的加載。那為什么我們還需要實現其他圖片加載方案呢?其實是因為Flutter圖片組件功能上存在一些缺陷:
- 圖片緩存沒有持久化能力,無網環境下不支持顯示圖片。
- 文件圖片與原生環境不共用,導致圖片資源文件重復。
我們不僅實現圖片方案的本地能力復用,而且還能實現視頻能力的紋理外接
內存性能不足
從整個APP的視角來說,采用原生圖片方案的情況下,其實我們維護了兩個大的緩存池:一個是Native的圖片緩存,一個是Flutter側的圖片緩存。兩個緩存無法互通,這無疑是一個巨大的浪費。特別是對內存的峰值內存性能產生了非常大的壓力。
調研梳理所有優化方向,確定圖片壓縮、更換圖片組件、降低頁面刷新次數三個方向
3.4 多頁面內存優化
這個優化策略是真真被逼出來的。在對線上數據分析以后,我們發現Flutter頁面棧有一個非常有意思的特點:
多頁面棧情況下,底層的頁面不會被釋放。即便是在內存非常緊張的情況下,也不會執行回收。這樣就會導致一個問題:隨著頁面的增多,內存消耗會線性增長。這里占比最高的就是圖片資源的占比了。
我們發現Flutter的layer層可以穩定感知到頁面棧的變化。
4. 寫代碼時候, 內存總結:
Flutter內存優化是一個非常復雜的問題,其中涉及多個方面的優化策略。下面將從以下幾個方面對Flutter的內存優化進行具體實現的總結。
一、減少Widget的創建和銷毀
Widget的創建和銷毀是Flutter中內存占用最大和最頻繁的操作之一,在開發過程中,應該盡量減少Widget的創建和銷毀。
1.1 重用現有的Widget
在同一個頁面內,如果多個Widget具有相同的樣式和行為,則可以共用同一個widget,而不是每個Widget都創建自己的。
1.2 使用Key避免重復構建
Flutter中相同類型的Widget,如果沒有設置Key屬性,則每次更新時都會創建新的Widget,即使兩個Widget的props完全一致。相反,如果為Widget設置Key屬性,則當Widget更新時,會嘗試在它們之間保持一個一致的關系,從而減少重新構建它們所需要的樹的大小。
1.3 避免狀態管理器泄露
在Flutter中,每個控件對應一個狀態,如果狀態管理不當,可能會導致內存泄漏。因此,在組件銷毀時必須釋放所占用的內存。在實現中可以使用StatefulWidget和StatelessWidget等控件,這些控件會自動處理狀態管理的問題。
二、優化圖片資源
在Flutter中,圖片資源可能會占用大量的內存,如果沒有得到合適的管理和優化,很容易導致內存溢出的問題。
2.1 去掉不必要的圖片
去掉那些不必要的圖片,或者壓縮那些需要的圖片,以減少內存占用。
2.2 避免圖片重復加載
重復加載的圖片會占用更多的內存,因此應該盡量避免這種情況的發生。在Flutter中可以使用MemoryImage對圖片進行緩存,提高圖片的復用率。
三、避免資源泄漏
資源泄漏是內存占用過多的常見問題之一,在Flutter中也是如此。應該在開發過程中注意避免資源泄漏。
3.1 及時釋放內存
當不再使用某個已分配的內存時,應該及時釋放它,避免內存泄漏等問題。
3.2 避免“閉包”泄漏
在Flutter中,如果一個Widget包含了另一個Widget,則可能會發生“閉包”,即某個Widget的生命周期不正確地延長。為了避免這種情況的發生,可以使用StatelessWidget等控件,不包含狀態變量,避免“閉包”泄漏。
四、合理管理內存
在Flutter中,應用程序使用的內存受到操作系統、硬件性能等多個因素的影響。為了合理地管理內存,需要進行特定的優化,以確保應用程序不會消耗過多的內存。
4.1 使用Navigator管理頁面歷史
在Flutter中,能夠使用Navigator管理頁面歷史,并對頁面進行統一的管理。使用Navigator可以對頁面進行釋放和回收,避免頁面占用過多內存。
4.2 使用異步方式加載數據
采用異步方式優化數據加載,可以避免在程序運行時出現阻塞,使得應用程序可響應。異步方式可以優化數據請求、數據轉換等操作,減少應用程序的響應時間。
綜上所述,Flutter內存優化的具體實現有許多技巧和方法。開發人員可以根據實際情況綜合運用各種優化策略,以提高應用程序的內存使用效率,提高用戶的使用體驗。
內存性能優化總結:
1. 內存泄露
2. listview優化
- 圖片優化, 圖片外接紋理優化
4. 頁面不可見,內存釋放優化!