使用記憶體檢視
記憶體檢視可深入了解應用程式的記憶體配置細節, 並提供偵測與除錯特定問題的工具。
若需瞭解如何在不同的 IDE 中找到 DevTools 螢幕, 請參閱 DevTools overview。
為了更好地理解本頁所介紹的資訊, 第一節將說明 Dart 如何管理記憶體。 如果你已經了解 Dart 的記憶體管理方式, 可以直接跳到 Memory view guide。
使用記憶體檢視的理由
#當你需要預先進行記憶體最佳化,或是你的應用程式出現以下情況時, 建議使用記憶體檢視:
- 記憶體不足時發生當機
- 執行速度變慢
- 導致裝置變慢或無回應
- 因超過作業系統強制的記憶體限制而關閉
- 超過記憶體使用上限
- 此上限會依你的應用程式目標裝置類型而有所不同。
- 懷疑有記憶體洩漏
基本記憶體概念
#使用類別建構函式建立的 Dart 物件 (例如,透過 MyClass() 建立)會存在於 稱為 heap(堆積)的記憶體區域中。 heap 中的記憶體由 Dart VM(虛擬機器)管理。 Dart VM 會在物件建立時分配記憶體, 並在物件不再使用時釋放(或解除分配)記憶體 (請參閱 Dart garbage collection)。
物件類型
#可釋放物件(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.
…
}Shallow size 與 retained size 的差異
#Shallow size(淺層大小)只包含物件本身及其參考(references)的大小,而 retained size(保留大小)則同時包含該物件所保留(retained)的其他物件的大小。
根物件(root object)的 retained size 包含所有可被存取的 Dart 物件。
在以下範例中,myHugeInstance 的大小不屬於父物件或子物件的 shallow size,但會被計算在它們的 retained size 之中:
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 的計算中,如果一個物件有多條保留路徑(retaining path),則其大小只會被分配為保留給最短保留路徑上的成員。
在這個例子中,物件 x 有兩條保留路徑:
root -> a -> b -> c -> x
root -> d -> e -> x (shortest retaining path to `x`)只有最短路徑上的成員(d 和 e)會將 x 計入其保留大小(retaining size)。
Dart 也會發生記憶體洩漏嗎?
#垃圾回收器(Garbage Collector)無法防止所有類型的記憶體洩漏,開發者 仍然需要監控物件,確保其生命週期不會發生洩漏。
為什麼垃圾回收器無法防止所有記憶體洩漏?
#雖然垃圾回收器會自動處理所有 不可到達(unreachable)的物件,但應用程式 本身必須確保不再需要的物件 不再可被存取(即不再從 root 被參考)。
因此,如果不需要的物件仍被參考 (例如存在於全域變數、靜態變數, 或長生命週期物件的欄位中), 垃圾回收器就無法辨識它們為可回收物件, 記憶體分配會逐漸增加, 最終應用程式會因為 out-of-memory 錯誤而崩潰。
為什麼閉包(closure)需要特別注意
#有一種難以察覺的記憶體洩漏模式與閉包(closure)的使用有關。 在下方的程式碼中,原本設計為短生命週期的 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);
…如何修正容易造成記憶體洩漏的程式碼?
#以下程式碼不容易造成記憶體洩漏,原因如下:
- 閉包(closure)沒有使用體積大且生命週期短的
context物件。 - 改而使用的
theme物件,其生命週期較長。該物件只會建立一次,並在多個BuildContext實例之間共用。
// GOOD
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final handler = () => apply(theme);
useHandler(handler);
…BuildContext 的一般規則
#一般而言,對於 BuildContext,請遵循以下規則:如果閉包(closure)不會比元件(Widget)存活得更久,則可以將 context 傳遞給該閉包。
有狀態元件(Stateful widgets)需要額外注意。它們由兩個類別組成:[元件與元件狀態][interactive],其中元件(Widget)壽命較短,而狀態(State)壽命較長。由元件所擁有的 build context,不應該從狀態(State)的欄位中被引用,因為狀態不會隨著元件一起被垃圾回收,且可能比元件存活得更久。 [interactive]: /ui/interactivity#creating-a-stateful-widget
記憶體洩漏 vs 記憶體膨脹
#在記憶體洩漏(memory leak)中,應用程式會逐漸消耗越來越多的記憶體,例如,不斷建立監聽器(listener)但未釋放(dispose)它。
記憶體膨脹(memory bloat)則是指使用了超過最佳效能所需的記憶體,例如,使用過大的圖片或在整個生命週期內保持串流(stream)開啟。
無論是洩漏還是膨脹,當規模很大時,都會導致應用程式因 out-of-memory 錯誤而崩潰。然而,洩漏更容易造成記憶體問題,因為即使是小規模的洩漏,只要重複多次,也會導致崩潰。
記憶體檢視指南
#DevTools 的記憶體檢視(memory view)可協助你調查記憶體配置(包含堆積與外部記憶體)、記憶體洩漏、記憶體膨脹等問題。此檢視包含以下功能:
- 可展開圖表
- 提供記憶體配置的高階追蹤,並可檢視標準事件(如垃圾回收)與自訂事件(如圖片配置)。
- Profile Memory 分頁
- 依類別與記憶體型態列出目前的記憶體配置。
- Diff Snapshots 分頁
- 偵測並調查特定功能的記憶體管理問題。
- Trace Instances 分頁
- 針對指定類別,調查特定功能的記憶體管理情形。
可展開圖表
#可展開圖表提供下列功能:
記憶體結構解析
#時間序列圖(timeseries graph)用於視覺化 Flutter 記憶體在不同時間區間的狀態。圖表上的每個資料點對應於堆積(heap)在某個時間戳(x 軸)下的測量數值(y 軸)。例如,會紀錄使用量、容量、外部記憶體、垃圾回收與常駐集大小(resident set size, RSS)等資訊。

記憶體總覽圖表
#記憶體總覽圖表是一個收集記憶體統計資料的時間序列圖。它以視覺化方式呈現 Dart 或 Flutter 堆積,以及 Dart 或 Flutter 原生記憶體(native memory)隨時間的變化。
圖表的 x 軸為事件時間軸(timeseries)。y 軸所繪製的資料皆帶有資料收集時的時間戳。換句話說,它每 500 毫秒顯示一次記憶體的輪詢狀態(容量、已用、外部、RSS(常駐集大小)、GC(垃圾回收))。這有助於在應用程式執行時,提供記憶體狀態的即時外觀。
點擊 Legend 按鈕可顯示收集到的測量項目、符號與用於顯示資料的顏色。

Memory Size Scale(記憶體大小刻度)y 軸會自動調整至目前可見圖表範圍內所收集資料的範圍。
y 軸上繪製的數值如下:
- Dart/Flutter Heap
- 堆積中的物件(Dart 與 Flutter 物件)。
- Dart/Flutter Native
- 不屬於 Dart/Flutter 堆積,但仍屬於總記憶體佔用的記憶體。這部分的物件通常是原生物件(例如,將檔案讀入記憶體,或解碼後的圖片)。這些原生物件會透過 Dart 嵌入器(embedder)從原生作業系統(如 Android、Linux、Windows、iOS)暴露給 Dart VM。嵌入器會建立帶有 finalizer 的 Dart 包裝器,讓 Dart 程式碼能與這些原生資源互動。Flutter 為 Android 與 iOS 提供了嵌入器。詳情請參閱 Command-line and server apps、Dart on the server with Dart Frog、Custom Flutter Engine Embedders、Dart web server deployment with Heroku。
- Timeline
- 在特定時間點(timestamp)收集到的所有記憶體統計資料與事件的時間戳。
- Raster Cache
- Flutter 引擎的 raster cache 圖層或圖片的大小,在經過合成後進行最終繪製時產生。詳情請參閱 Flutter architectural overview 與 DevTools Performance view。
- Allocated
- 堆積的目前容量,通常會比所有堆積物件的總大小略大。
- RSS - Resident Set Size
- 常駐集大小(RSS)顯示某個程序所佔用的記憶體量。不包含已被交換(swapped out)的記憶體。包含已載入的共用函式庫、所有堆疊與堆積記憶體。詳情請參閱 Dart VM internals。
Profile Memory 分頁
#使用 Profile Memory 分頁可依類別與記憶體型態檢視目前的記憶體配置。若需在 Google Sheets 或其他工具中進行更深入分析,可下載 CSV 格式的資料。切換 Refresh on GC,即可即時檢視配置狀態。

Diff Snapshots 分頁
#使用 Diff Snapshots 分頁來調查特定功能的記憶體管理。請依照分頁上的指引,在與應用程式互動前後分別擷取快照,並比較這些快照:

點擊 Filter classes and packages 按鈕可縮小資料範圍:

若需在 Google Sheets 或其他工具中進行更深入分析,可下載 CSV 格式的資料。
Trace Instances 分頁
#使用 Trace Instances 分頁可調查在功能執行期間,哪些方法為一組類別配置了記憶體:
- 選擇要追蹤的類別
- 與你的應用程式互動,以觸發你關心的程式碼
- 點擊 Refresh
- 選擇一個已追蹤的類別
- 檢視收集到的資料

Bottom up 與 Call tree 檢視
#可根據任務需求,在 bottom-up 與 call tree 檢視間切換。

call tree 檢視會顯示每個實例的方法配置情形。此檢視為呼叫堆疊的自頂向下(top-down)表示,亦即可展開方法以顯示其被呼叫者(callees)。
bottom-up 檢視則會顯示配置這些實例的不同呼叫堆疊清單。
其他資源
#如需更多資訊,請參考以下資源:
- 若想學習如何使用 DevTools 監控應用程式記憶體使用狀況並偵測記憶體洩漏,請參考導引式 Memory View tutorial。
- 若想了解 Android 的記憶體結構,請參考 Android: Memory allocation among processes。