使用 Memory(記憶體)檢視
瞭解如何使用 DevTools 記憶體檢視。
記憶體檢視提供了對應用程式記憶體分配細節的洞察,以及用於檢測和除錯特定問題的工具。
有關如何在不同 IDE 中定位 DevTools 螢幕的資訊,請檢視 DevTools 概覽。
為了更好地理解本頁面中的見解,第一部分解釋了 Dart 如何管理記憶體。如果你已經瞭解 Dart 的記憶體管理,可以直接跳轉到 記憶體檢視指南。
使用記憶體檢視的理由
#在進行預防性記憶體最佳化,或應用程式遇到以下任一情況時,請使用記憶體檢視:
- 因記憶體不足而崩潰
- 執行速度變慢
- 導致裝置變慢或無響應
- 因超過作業系統強制執行的記憶體限制而被關閉
- 超過記憶體使用限制
- 此限制可能因應用目標裝置的型別而異。
- 懷疑存在記憶體洩漏
記憶體基本概念
#使用類建構函式(例如使用 MyClass())建立的 Dart 物件存在於記憶體的一個部分,稱為堆(heap)。堆中的記憶體由 Dart VM(虛擬機器)管理。Dart VM 在物件建立時為其分配記憶體,並在物件不再被使用時釋放(或撤銷分配)記憶體(參見 Dart 垃圾回收)。
物件型別
#可處置物件 (Disposable object)
#可處置物件是定義了 dispose() 方法的任何 Dart 物件。為避免記憶體洩漏,當物件不再需要時,請呼叫 dispose。
記憶體風險物件 (Memory-risky object)
#記憶體風險物件是指如果未正確處置,或者雖已處置但未被 GC(垃圾回收)回收,可能導致記憶體洩漏的物件。
根物件、保留路徑和可達性
#根物件 (Root object)
#每個 Dart 應用程式都會建立一個根物件,它直接或間接地引用應用程式分配的所有其他物件。
可達性 (Reachability)
#如果在應用程式執行的某個時刻,根物件停止引用已分配的物件,該物件就變得不可達,這是向垃圾回收器 (GC) 發出的釋放該物件記憶體的訊號。
保留路徑 (Retaining path)
#從根到物件的引用序列稱為該物件的保留路徑,因為它將物件的記憶體從垃圾回收中保留下來。一個物件可以有許多保留路徑。至少有一個保留路徑的物件稱為可達物件。
示例
#以下示例闡述了這些概念
class Child{}
class Parent {
Child? child;
}
Parent parent1 = Parent();
void myFunction() {
Child? child = Child();
// The `child` object was allocated in memory.
// It's now retained from garbage collection
// by one retaining path (root …-> myFunction -> child).
Parent? parent2 = Parent()..child = child;
parent1.child = child;
// At this point the `child` object has three retaining paths:
// root …-> myFunction -> child
// root …-> myFunction -> parent2 -> child
// root -> parent1 -> child
child = null;
parent1.child = null;
parent2 = null;
// At this point, the `child` instance is unreachable
// and will eventually be garbage collected.
…
}
淺層大小 vs 保留大小
#淺層大小 (Shallow size) 僅包含物件及其引用的自身大小,而 保留大小 (Retained size) 還包括所保留物件的大小。
根物件的 保留大小 包括所有可達的 Dart 物件。
在以下示例中,myHugeInstance 的大小不屬於父級或子級的淺層大小,但屬於它們的保留大小。
class Child{
/// The instance is part of both [parent] and [parent.child]
/// retained sizes.
final myHugeInstance = MyHugeInstance();
}
class Parent {
Child? child;
}
Parent parent = Parent()..child = Child();
在 DevTools 計算中,如果一個物件有多個保留路徑,其大小僅計入最短保留路徑成員的保留大小中。
在此示例中,物件 x 有兩條保留路徑
root -> a -> b -> c -> x
root -> d -> e -> x (shortest retaining path to `x`)
只有最短路徑的成員(d 和 e)會將 x 計入其保留大小。
Dart 中會發生記憶體洩漏嗎?
#垃圾回收器無法阻止所有型別的記憶體洩漏,開發人員仍需觀察物件以確保持續的生命週期內不發生洩漏。
為什麼垃圾回收器不能阻止所有洩漏?
#雖然垃圾回收器負責處理所有不可達物件,但應用程式有責任確保不再需要的物件不再是可達的(即不再被根物件引用)。
因此,如果不再需要的物件仍然被引用(在全域性或靜態變數中,或者作為長生命週期物件的欄位),垃圾回收器就無法識別它們,記憶體分配會持續增長,最終應用程式會因 out-of-memory(記憶體溢位)錯誤而崩潰。
為什麼閉包需要額外注意
#一種難以捕獲的洩漏模式與閉包的使用有關。在以下程式碼中,本應是短生命週期的 myHugeObject 的引用被隱式儲存在閉包上下文中,並傳遞給 setHandler。結果,只要 handler 可達,myHugeObject 就不會被垃圾回收。
final handler = () => print(myHugeObject.name);
setHandler(handler);
為什麼 BuildContext 需要額外注意
#
傳遞給 Flutter 的 build 方法的 context 引數,是一個可能擠入長生命週期區域並導致洩漏的短生命週期大型物件的典型例子。
以下程式碼容易發生洩漏,因為 useHandler 可能會將處理器儲存在長生命週期區域中
// BAD: DO NOT DO THIS
// This code is leak prone:
@override
Widget build(BuildContext context) {
final handler = () => apply(Theme.of(context));
useHandler(handler);
…
如何修復易洩漏的程式碼?
#以下程式碼不容易發生洩漏,因為:
- 閉包沒有使用大型且短生命週期的
context物件。 - (改用的)
theme物件是長生命週期的。它被建立一次並在BuildContext例項之間共享。
// GOOD
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final handler = () => apply(theme);
useHandler(handler);
…
關於 BuildContext 的一般規則
#
通常,請對 BuildContext 遵循以下規則:如果閉包的生命週期不會超過元件(widget),那麼將 context 傳遞給閉包是可以的。
StatefulWidget 需要額外注意。它們由兩個類組成:元件和元件狀態,其中元件是短生命週期的,而狀態是長生命週期的。由元件擁有的 build context 絕不應被狀態的欄位引用,因為狀態不會隨元件一起被垃圾回收,並且其壽命可能會顯著超過元件。
記憶體洩漏 vs 記憶體膨脹
#在記憶體洩漏中,應用程式會逐漸增加記憶體使用量,例如透過重複建立監聽器而不進行處置。
記憶體膨脹(Memory bloat)是指使用了比實現最佳效能所需的更多的記憶體,例如使用過大的圖片或在整個生命週期中保持流(stream)開啟。
如果規模較大,洩漏和膨脹都會導致應用程式出現 out-of-memory 錯誤並崩潰。然而,洩漏更容易導致記憶體問題,因為即使是很小的洩漏,如果重複多次,也會導致崩潰。
記憶體檢視指南
#DevTools 記憶體檢視可幫助您調查記憶體分配(堆記憶體和外部記憶體)、記憶體洩漏、記憶體膨脹等。該檢視具有以下功能:
- 可展開圖表
-
獲取記憶體分配的高階跟蹤,並檢視標準事件(如垃圾回收)和自定義事件(如圖片分配)。
- Profile Memory(記憶體分析) 標籤頁
-
按類和記憶體型別列出當前的記憶體分配情況。
- Diff Snapshots(快照對比) 標籤頁
檢測並調查某個功能的記憶體管理問題。
- Trace Instances(例項追蹤) 標籤頁
-
調查特定一組類的記憶體管理情況。
可展開圖表
#可展開圖表提供以下功能:
記憶體結構分析
#時序圖可視化了 Flutter 記憶體隨時間變化的趨勢。圖表上的每個資料點對應於測量量(y軸)在特定時間戳(x軸)的狀態。例如,捕獲了使用量、容量、外部記憶體、垃圾回收和常駐記憶體大小(RSS)。
記憶體概覽圖
#記憶體概覽圖是收集到的記憶體統計資訊的時序圖。它直觀地呈現了 Dart 或 Flutter 堆以及 Dart 或 Flutter 原生記憶體隨時間的狀態。
圖表的 x 軸是事件的時間軸(時序)。y 軸上繪製的所有資料都有一個收集資料時的時間戳。換句話說,它顯示了每 500 毫秒輪詢一次的記憶體狀態(容量、已用、外部、RSS(常駐記憶體大小)和 GC(垃圾回收))。這有助於在應用程式執行時提供記憶體狀態的即時顯示。
點選 Legend(圖例) 按鈕可顯示收集到的測量結果、符號以及用於展示資料的顏色。
Memory Size Scale(記憶體大小比例) y 軸會自動根據當前可見圖表範圍內的收集資料範圍進行調整。
y 軸上繪製的數量如下:
- Dart/Flutter 堆 (Heap)
堆中的物件(Dart 和 Flutter 物件)。
- Dart/Flutter 原生記憶體 (Native)
-
不在 Dart/Flutter 堆中,但仍屬於總記憶體佔用一部分的記憶體。此記憶體中的物件為原生物件(例如,從將檔案讀入記憶體,或解碼後的圖片)。這些原生物件透過 Dart 嵌入器從原生作業系統(如 Android、Linux、Windows、iOS)暴露給 Dart VM。嵌入器建立一個帶有終結器 (finalizer) 的 Dart 包裝器,允許 Dart 程式碼與這些原生資源進行通訊。Flutter 為 Android 和 iOS 提供了嵌入器。更多資訊,請參閱 命令列和伺服器應用、使用 Dart Frog 進行伺服器端 Dart 開發、自定義 Flutter 引擎嵌入器、使用 Heroku 部署 Dart Web 伺服器。
- 時間線
-
特定時間點(時間戳)所有收集到的記憶體統計資料和事件的時間戳。
- 柵格快取 (Raster Cache)
-
在合成後執行最終渲染時,Flutter 引擎柵格快取層或圖片的大小。更多資訊,請參閱 Flutter 架構概覽 和 DevTools 效能檢視。
- 已分配 (Allocated)
-
所有 Dart 堆的總容量。這通常略大於所有堆物件的總大小。
- RSS - 常駐記憶體大小 (Resident Set Size)
-
常駐記憶體大小顯示程序所佔用的記憶體量。它不包括被交換出去的記憶體。它包括已載入的共享庫的記憶體,以及所有的棧和堆記憶體。更多資訊,請參閱 Dart VM 內部原理。
Profile Memory(記憶體分析)標籤頁
#使用 Profile Memory(記憶體分析) 標籤頁按類和記憶體型別檢視當前的記憶體分配情況。如需在 Google Sheets 或其他工具中進行深度分析,請下載 CSV 格式的資料。切換 Refresh on GC(GC 時重新整理),可即時檢視分配情況。
Diff Snapshots(快照對比)標籤頁
#使用 Diff Snapshots(快照對比) 標籤頁調查功能的記憶體管理。按照標籤頁上的指導,在與應用程式互動前後拍攝快照,並對快照進行對比。
點選 Filter classes and packages(過濾類和包) 按鈕以縮小資料範圍。
如需在 Google Sheets 或其他工具中進行深度分析,請下載 CSV 格式的資料。
Trace Instances(例項追蹤)標籤頁
#使用 Trace Instances(例項追蹤) 標籤頁,調查在功能執行期間哪些方法為一組類分配了記憶體。
- 選擇要追蹤的類
- 與應用互動以觸發您感興趣的程式碼
- 點選 Refresh(重新整理)
- 選擇一個追蹤的類
- 檢視收集到的資料
自底向上 vs 呼叫樹檢視
#根據您的具體任務,在自底向上和呼叫樹檢視之間進行切換。
呼叫樹檢視顯示每個例項的方法分配情況。該檢視是呼叫棧的自頂向下表示,這意味著可以展開方法以檢視其被呼叫的函式。
自底向上檢視顯示了分配這些例項的不同調用棧列表。
其他資源
#有關更多資訊,請檢視以下資源:
- 要了解如何使用 DevTools 監控應用的記憶體使用情況並檢測記憶體洩漏,請檢視 Memory View 教程。
- 要了解 Android 記憶體結構,請檢視 Android:程序間的記憶體分配。