跳至主要內容

使用記憶體檢視

記憶體檢視提供了關於應用程式記憶體分配的詳細資訊,以及偵測和除錯特定問題的工具。

有關如何在不同 IDE 中找到 DevTools 畫面的資訊,請參閱DevTools 概述

為了更好地理解此頁面上的資訊,第一部分將解釋 Dart 如何管理記憶體。如果您已經了解 Dart 的記憶體管理,可以直接跳到記憶體檢視指南

使用記憶體檢視的原因

#

在進行預防性記憶體最佳化或應用程式遇到以下情況時,請使用記憶體檢視

  • 記憶體不足時當機
  • 速度變慢
  • 導致裝置速度變慢或無回應
  • 因超出作業系統強制執行的記憶體限制而關閉
  • 超出記憶體使用限制
    • 此限制可能會因您的應用程式所針對的裝置類型而異。
  • 懷疑有記憶體洩漏

基本記憶體概念

#

使用類別建構函式 (例如,使用 MyClass()) 建立的 Dart 物件會存放在稱為 *堆積* 的記憶體區域中。堆積中的記憶體由 Dart VM(虛擬機器)管理。Dart VM 在物件建立時為物件分配記憶體,並在物件不再使用時釋放(或解除分配)記憶體(請參閱Dart 垃圾回收)。

物件類型

#

可處置的物件

#

可處置的物件是任何定義了 dispose() 方法的 Dart 物件。為了避免記憶體洩漏,當物件不再需要時,請呼叫 dispose

有記憶體風險的物件

#

有記憶體風險的物件是如果未正確處置或處置後未被 GC (垃圾回收) 時,*可能* 會導致記憶體洩漏的物件。

根物件、保留路徑和可達性

#

根物件

#

每個 Dart 應用程式都會建立一個 *根物件*,該物件直接或間接地引用應用程式分配的所有其他物件。

可達性

#

如果在應用程式執行的某個時刻,根物件停止引用已分配的物件,則該物件會變成 *無法到達* 的狀態,這表示垃圾回收 (GC) 可以解除分配該物件的記憶體。

保留路徑

#

從根到物件的引用序列稱為物件的 *保留* 路徑,因為它保留了物件的記憶體,使其不會被垃圾回收。一個物件可以有多個保留路徑。至少有一個保留路徑的物件稱為 *可到達* 的物件。

範例

#

以下範例說明了這些概念

dart
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.


}

淺層大小與保留大小

#

淺層大小僅包含物件及其引用的物件的大小,而保留大小還包含保留物件的大小。

根物件的保留大小包含所有可到達的 Dart 物件。

在以下範例中,myHugeInstance 的大小不屬於父物件或子物件的淺層大小,而是屬於它們的保留大小

dart
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`)

只有最短路徑的成員 (de) 會將 x 納入其保留大小。

Dart 中會發生記憶體洩漏嗎?

#

垃圾回收器無法防止所有類型的記憶體洩漏,開發人員仍然需要監看物件,以確保無洩漏的生命週期。

為什麼垃圾回收器無法防止所有洩漏?

#

雖然垃圾回收器會處理所有無法到達的物件,但應用程式有責任確保不再需要的物件不再可到達(從根引用)。

因此,如果不需要的物件仍然被引用(在全域或靜態變數中,或作為長生命週期物件的欄位),垃圾回收器將無法識別它們,記憶體分配會逐漸增加,應用程式最終會因 out-of-memory 錯誤而當機。

為什麼閉包需要特別注意

#

一種難以捕捉的洩漏模式與使用閉包有關。在以下程式碼中,對預期為短生命週期的 myHugeObject 的引用會隱式儲存在閉包內容中,並傳遞給 setHandler。因此,只要 handler 可到達,myHugeObject 就不會被垃圾回收。

dart
  final handler = () => print(myHugeObject.name);
  setHandler(handler);

為什麼 BuildContext 需要特別注意

#

一個可能擠入長生命週期區域並因此導致洩漏的大型短生命週期物件的範例,是傳遞給 Flutter build 方法的 context 參數。

以下程式碼容易發生洩漏,因為 useHandler 可能會將處理程式儲存在長生命週期區域中

dart
// BAD: DO NOT DO THIS
// This code is leak prone:
@override
Widget build(BuildContext context) {
  final handler = () => apply(Theme.of(context));
  useHandler(handler);

如何修復容易洩漏的程式碼?

#

以下程式碼不容易發生洩漏,因為

  1. 閉包不使用大型且短生命週期的 context 物件。
  2. theme 物件(改用)是長生命週期的。它會建立一次,並在 BuildContext 實例之間共享。
dart
// GOOD
@override
Widget build(BuildContext context) {
  final theme = Theme.of(context);
  final handler = () => apply(theme);
  useHandler(handler);

BuildContext 的一般規則

#

一般來說,請對 BuildContext 使用以下規則:如果閉包的生命週期沒有超出 widget 的生命週期,則可以將 context 傳遞給閉包。

Stateful widgets 需要特別注意。它們由兩個類別組成:widget 和 widget 狀態,其中 widget 是短生命週期的,而狀態是長生命週期的。由 widget 擁有的 build context 絕對不應該從狀態的欄位中引用,因為狀態不會與 widget 一起被垃圾回收,而且生命週期可能會明顯超過 widget 的生命週期。

記憶體洩漏 vs 記憶體膨脹

#

在記憶體洩漏中,應用程式會逐漸使用記憶體,例如,重複建立監聽器,但不處置它。

記憶體膨脹會使用比最佳效能所需更多的記憶體,例如,使用過大的圖像或在其生命週期內保持串流開啟。

當洩漏和膨脹都很嚴重時,都會導致應用程式因 out-of-memory 錯誤而當機。但是,洩漏更可能導致記憶體問題,因為即使是小的洩漏,如果重複多次,也會導致當機。

記憶體檢視指南

#

DevTools 記憶體檢視可協助您調查記憶體分配(堆積和外部)、記憶體洩漏、記憶體膨脹等等。此檢視具有以下功能

可展開的圖表
取得記憶體分配的高階追蹤,並檢視標準事件(如垃圾回收)和自訂事件(如圖像分配)。
分析記憶體標籤頁
查看按類別和記憶體類型列出的目前記憶體分配。
差異快照標籤頁
偵測和調查功能的記憶體管理問題。
追蹤實例標籤頁
調查指定類別集的記憶體管理。

可展開的圖表

#

可展開的圖表提供以下功能

記憶體結構

#

時間序列圖表可視覺化 Flutter 記憶體在連續時間間隔的狀態。圖表上的每個資料點都對應於堆積的測量數量 (y 軸) 的時間戳記 (x 軸)。例如,會擷取使用量、容量、外部、垃圾回收和駐留集大小。

Screenshot of a memory anatomy page

記憶體概觀圖表

#

記憶體概觀圖表是收集的記憶體統計資料的時間序列圖表。它以視覺方式呈現 Dart 或 Flutter 堆積和 Dart 或 Flutter 原生記憶體隨時間變化的狀態。

圖表的 x 軸是事件的時間軸 (時間序列)。繪製在 y 軸中的資料都具有收集資料的時間戳記。換句話說,它顯示每 500 毫秒輪詢的記憶體狀態(容量、已用、外部、RSS (駐留集大小) 和 GC (垃圾回收))。這有助於在應用程式執行時即時呈現記憶體的狀態。

按一下 圖例 按鈕會顯示收集的測量值、符號和用於顯示資料的色彩。

Screenshot of a memory anatomy page

記憶體大小比例 y 軸會自動調整為目前可見圖表範圍中收集的資料範圍。

繪製在 y 軸上的數量如下

Dart/Flutter 堆積
堆積中的物件(Dart 和 Flutter 物件)。
Dart/Flutter 原生
不在 Dart/Flutter 堆積中但仍是總記憶體佔用空間一部分的記憶體。此記憶體中的物件將是原生物件(例如,從將檔案讀入記憶體或已解碼的圖像)。原生物件是使用 Dart 嵌入器從原生 OS(例如 Android、Linux、Windows、iOS)公開給 Dart VM 的。嵌入器會建立具有終結器的 Dart 包裝函式,允許 Dart 程式碼與這些原生資源通訊。Flutter 有適用於 Android 和 iOS 的嵌入器。如需更多資訊,請參閱 命令列和伺服器應用程式使用 Dart Frog 在伺服器上執行 Dart自訂 Flutter Engine 嵌入器使用 Heroku 部署 Dart Web 伺服器
時間軸
在特定時間點(時間戳記)收集的所有記憶體統計資料和事件的時間戳記。
點陣快取
Flutter 引擎點陣快取圖層或圖片的大小,同時在合成後執行最終呈現。如需更多資訊,請參閱Flutter 架構概觀DevTools 效能檢視
已分配
堆積的目前容量通常略大於所有堆積物件的總大小。
RSS - 駐留集大小
駐留集大小會顯示處理序的記憶體量。它不包含換出(swap out)的記憶體。它包含從載入的共享程式庫以及所有堆疊和堆積記憶體。如需更多資訊,請參閱 Dart VM 內部機制

分析記憶體標籤頁

#

使用 分析記憶體 標籤頁查看按類別和記憶體類型列出的目前記憶體分配。如需在 Google 試算表或其他工具中進行更深入的分析,請以 CSV 格式下載資料。切換 在 GC 時重新整理,以即時查看分配。

Screenshot of the profile tab page

差異快照標籤頁

#

使用 差異快照 標籤頁來調查功能的記憶體管理。請依照標籤頁上的指南,在與應用程式互動之前和之後拍攝快照,並比較快照的差異

Screenshot of the diff tab page

點擊 篩選類別和套件 按鈕以縮小資料範圍

Screenshot of the filter options ui

如需在 Google 試算表或其他工具中進行更深入的分析,請以 CSV 格式下載資料。

追蹤實例標籤頁

#

使用追蹤實例分頁來調查在功能執行期間,哪些方法會為一組類別配置記憶體。

  1. 選取要追蹤的類別
  2. 與您的應用程式互動以觸發您感興趣的程式碼
  3. 點擊重新整理
  4. 選取一個已追蹤的類別
  5. 檢閱收集到的資料

Screenshot of a trace tab

由下而上 vs 呼叫樹狀檢視

#

根據您的任務細節,在由下而上和呼叫樹狀檢視之間切換。

Screenshot of a trace allocations

呼叫樹狀檢視顯示每個實例的方法配置。此檢視是呼叫堆疊的由上而下表示法,這表示可以展開一個方法來顯示其被呼叫者。

由下而上檢視顯示已配置實例的不同呼叫堆疊清單。

其他資源

#

如需更多資訊,請查看以下資源