使用記憶體檢視
記憶體檢視可提供有關應用程式記憶體分配的詳細資訊,並提供檢測和除錯特定問題的工具。
有關如何在不同 IDE 中找到 DevTools 介面的資訊,請參閱 DevTools 概述。
為了更好地理解本頁內容,第一部分解釋了 Dart 如何管理記憶體。如果您已瞭解 Dart 的記憶體管理,可以直接跳至 記憶體檢視指南。
使用記憶體檢視的原因
#當您的應用程式出現以下任一情況時,請使用記憶體檢視進行預防性記憶體最佳化,或用於除錯此類問題:
- 執行時因記憶體不足而崩潰
- 執行緩慢
- 導致裝置變慢或無響應
- 因超出作業系統強制執行的記憶體限制而被關閉
- 超出記憶體使用限制
- 此限制可能因您應用的目標裝置型別而異。
- 懷疑發生記憶體洩漏
基本記憶體概念
#使用類建構函式(例如,透過使用 MyClass())建立的 Dart 物件存在於稱為堆(heap)的記憶體區域中。堆中的記憶體由 Dart 虛擬機器 (VM) 管理。Dart VM 在物件建立時為其分配記憶體,並在物件不再使用時釋放(或取消分配)記憶體(請參閱 Dart 垃圾回收)。
物件型別
#可Dispose物件
#可Dispose物件是任何定義了 dispose() 方法的 Dart 物件。為避免記憶體洩漏,請在不再需要物件時呼叫 dispose。
記憶體風險物件
#記憶體風險物件是指可能導致記憶體洩漏的物件,如果它未被正確 dispose 或被 dispose 但未被 GC(垃圾回收)。
根物件、保留路徑和可達性
#根物件
#每個 Dart 應用程式都會建立一個根物件,該物件直接或間接地引用應用程式分配的所有其他物件。
可達性
#如果在應用程式執行的某個時刻,根物件不再引用已分配的物件,該物件就變得不可達,這是垃圾回收器 (GC) 取消分配物件記憶體的訊號。
保留路徑
#從根到物件的引用序列稱為物件的保留路徑,因為它使物件記憶體免受垃圾回收。一個物件可以有多個保留路徑。至少有一條保留路徑的物件稱為可達物件。
示例
#以下示例說明了這些概念
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 保留大小
#淺大小僅包括物件本身及其引用的記憶體大小,而保留大小還包括被保留物件的大小。
根物件的保留大小包括所有可達的 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 可能會將 handler 儲存在長生命週期區域。
// 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 需要特別注意。它們由兩個類組成:widget 和widget state,其中 widget 的生命週期較短,而 state 的生命週期較長。widget 所擁有的 build context 絕不應從 state 的欄位中引用,因為 state 不會隨 widget 一起被垃圾回收,並且其生命週期可能遠遠長於 widget。
記憶體洩漏 vs 記憶體膨脹
#在記憶體洩漏中,應用程式會逐漸使用記憶體,例如,透過反覆建立監聽器但不 dispose 它。
記憶體膨脹是指使用的記憶體超過了最佳效能所需的記憶體,例如,使用過大的影像或在流的整個生命週期中保持開啟狀態。
無論是洩漏還是膨脹,當規模很大時,都會導致應用程式因 out-of-memory 錯誤而崩潰。然而,洩漏更有可能導致記憶體問題,因為即使是很小的洩漏,如果重複多次,也會導致崩潰。
記憶體檢視指南
#DevTools 的記憶體檢視可以幫助您調查記憶體分配(包括堆內和堆外記憶體)、記憶體洩漏、記憶體膨脹等。該檢視具有以下功能:
- 可展開圖表
- 獲取記憶體分配的高層跟蹤,並檢視標準事件(如垃圾回收)和自定義事件(如影像分配)。
- 記憶體分析(Profile Memory)選項卡
- 按類和記憶體型別檢視當前的記憶體分配。
- 差異快照(Diff Snapshots)選項卡
- 檢測和調查功能的記憶體管理問題。
- 例項跟蹤(Trace Instances)選項卡
- 為指定的一組類調查功能的記憶體管理。
可展開圖表
#可展開圖表提供以下功能:
記憶體解剖
#時間序列圖視覺化 Flutter 記憶體隨時間推移的狀態。圖表上的每個資料點對應於時間戳(x 軸)上堆的測量值(y 軸)。例如,會捕獲使用量、容量、外部記憶體、垃圾回收和常駐集大小。

記憶體概覽圖
#記憶體概覽圖是一個已收集記憶體統計資訊的時間序列圖。它直觀地展示了 Dart 或 Flutter 堆以及 Dart 或 Flutter 的原生記憶體隨時間的變化情況。
圖表的 x 軸是事件(時間序列)的時間線。y 軸上的資料都具有資料收集的時間戳。換句話說,它每 500 毫秒顯示一次記憶體的輪詢狀態(容量、已使用、外部、RSS(常駐集大小)和 GC(垃圾回收))。這有助於在應用程式執行時提供記憶體狀態的即時檢視。
單擊圖例(Legend)按鈕會顯示收集到的測量值、符號和用於顯示資料的顏色。

y 軸上的記憶體大小刻度(Memory Size Scale)會自動調整以適應當前可見圖表範圍內收集的資料。
y 軸上繪製的數量如下:
- Dart/Flutter 堆
- 堆中的物件(Dart 和 Flutter 物件)。
- Dart/Flutter 原生記憶體
- 不屬於 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)
- 堆的當前容量通常略大於所有堆物件的大小之和。
- RSS - 常駐集大小 (Resident Set Size)
- 常駐集大小顯示程序的記憶體量。它不包括已交換出去的記憶體。它包括已載入的共享庫的記憶體,以及所有棧和堆記憶體。有關更多資訊,請參閱 Dart VM 內部機制。
記憶體分析(Profile Memory)選項卡
#使用記憶體分析(Profile Memory)選項卡按類和記憶體型別檢視當前記憶體分配。要深入分析,可以下載 CSV 格式的資料到 Google Sheets 或其他工具中。切換GC 時重新整理(Refresh on GC),可以即時檢視分配情況。

差異快照(Diff Snapshots)選項卡
#使用差異快照(Diff Snapshots)選項卡來調查功能的記憶體管理。按照選項卡上的指南,在與應用程式互動之前和之後拍攝快照,然後進行差異比較。

點選過濾類和包(Filter classes and packages)按鈕,縮小資料範圍。

要深入分析,可以下載 CSV 格式的資料到 Google Sheets 或其他工具中。
例項跟蹤(Trace Instances)選項卡
#使用例項跟蹤(Trace Instances)選項卡來調查在功能執行期間,哪些方法為一組類分配了記憶體。
- 選擇要跟蹤的類
- 與您的應用進行互動,以觸發您感興趣的程式碼
- 點選重新整理(Refresh)
- 選擇一個已跟蹤的類
- 檢視收集到的資料

自底向上 vs 呼叫樹檢視
#根據任務的特定需求,在自底向上和呼叫樹檢視之間切換。

呼叫樹檢視顯示每個例項的方法分配。該檢視是對呼叫堆疊的自頂向下表示,意味著可以展開一個方法來顯示其呼叫的子方法。
自底向上檢視顯示已分配例項的各種呼叫堆疊列表。
其他資源
#更多資訊,請參閱以下資源:
- 要了解如何使用 DevTools 監控應用的記憶體使用情況並檢測記憶體洩漏,請參閱本教程 記憶體檢視教程。
- 要了解 Android 的記憶體結構,請參閱 Android:程序間的記憶體分配。