iT邦幫忙

2025 iThome 鐵人賽

DAY 4
0
Mobile Development

Android 性能戰爭:從 Profiler 開始的 30 天優化實錄系列 第 4

# Day 4:【Memory Profiler】記憶體快照:揪出不該存在的物件

  • 分享至 

  • xImage
  •  

各位戰士,歡迎來到第四天的戰場。結束了對 CPU 的偵查後,我們將目光轉向另一個關鍵資源:記憶體 (Memory)。一個健康的應用程式應該像一個紀律嚴明的軍營,物件(士兵)在需要時被創建,在任務完成後就該被回收(銷毀)。

但如果有些士兵任務結束後還賴著不走,甚至越積越多,最終就會耗盡營地所有資源,導致整個營地崩潰。這就是記憶體洩漏 (Memory Leak),而它導致的 OutOfMemoryError 是 Android 開發中最常見也最致命的閃退原因之一。


我們的偵查工具:Memory Profiler

與 CPU Profiler 類似,Memory Profiler 也是 Android Studio Profiler 工具套件的一部分。它能即時顯示你的 App 記憶體使用情況,並將其分為幾個部分:

  • Java: 從 Java 或 Kotlin 程式碼分配的物件記憶體。
  • Native: 從 C/C++ 程式碼分配的物件記憶體。
  • Graphics: 圖形緩衝區佇列為在螢幕上顯示像素所使用的記憶體。
  • Stack: App 中的原生堆疊和 Java 堆疊使用的記憶體。
  • Code: App 用於處理程式碼和資源(如 dex 位元組碼、so 函式庫)的記憶體。
  • Others: App 使用但系統不確定如何分類的記憶體。

我們的首要目標,是關注 Java 記憶體,因為大部分的應用程式碼物件都在這裡。


核心戰術:捕獲記憶體快照 (Heap Dump)

要找出那些不該存在的物件,我們需要對整個軍營進行一次「人口普查」。在記憶體管理中,這次普查被稱為捕獲堆積轉儲 (Capture Heap Dump)

Heap Dump 會在你點擊按鈕的那一刻,凍結整個 Java 堆積(Heap),並將當時存在的所有物件、以及它們之間的引用關係全部記錄下來。這是一份詳細到極致的清單,也是我們抓出間諜(洩漏物件)的關鍵證據。

在 Memory Profiler 的時間線上,有兩個按鈕可以執行此操作:

  1. Dump Java heap:執行一次手動的 Heap Dump。
  2. Record native allocations:記錄原生程式碼的記憶體分配(較進階,我們暫不討論)。

實戰演練:揪出一個典型的 Activity 洩漏

記憶體洩漏最經典的案例,莫過於一個已經被銷毀的 Activity 因為被一個生命週期更長的物件持有引用,而無法被垃圾回收器 (Garbage Collector, GC) 回收。

讓我們來製造一個這樣的場景。假設我們有一個單例 (Singleton),它錯誤地持有了 ActivityContext

// 一個常見的錯誤設計:單例持有 Activity 的 Context
object LeakySingleton {

    private var context: Context? = null

    fun initialize(context: Context) {
        // 錯誤!這裡應該傳入 applicationContext
        this.context = context
    }

    fun doSomething() {
        // 使用 context...
    }
}

// 在你的 MainActivity 中
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 將 Activity 的實例傳給了單例
        LeakySingleton.initialize(this)

        val nextButton: Button = findViewById(R.id.next_button)
        nextButton.setOnClickListener {
            // 點擊按鈕,跳轉到 SecondActivity
            startActivity(Intent(this, SecondActivity::class.java))
        }
    }
}

作戰場景模擬:

  1. 打開 App,進入 MainActivity

  2. 點擊按鈕,跳轉到 SecondActivity

  3. SecondActivity 頁面,按下返回鍵,回到 MainActivity

  4. 重複步驟 2 和 3 數次,模擬使用者在兩個頁面間來回切換。

  5. 最後,回到 MainActivity 後,按下返回鍵,關閉 SecondActivity。理論上,此時所有 SecondActivity 的實例都應該被銷毀了。

抓捕行動:

  1. 在 Memory Profiler 中,點擊垃圾桶圖示,手動觸發一次 GC,確保回收掉所有可回收的物件。

  2. 點擊 Dump Java heap 按鈕,捕獲記憶體快照。

分析證據:

捕獲完成後,Android Studio 會打開一個 Heap Dump 分析視窗。

  1. 在左側的類別列表中,使用右上角的搜尋框,搜尋 SecondActivity

  2. 如果 Leak Count > 0,恭喜你,你已經找到了洩漏的物件!這意味著記憶體中仍然存在著 SecondActivity 的實例。

  3. 在下方的 Instance View 中,點選一個 SecondActivity 的實例。

  4. 在右側的 References 視窗中,你將看到一個引用樹,它會清楚地告訴你,是誰「拉著」這個 Activity 不讓它被回收。

你應該能看到一條類似這樣的引用鏈:
LeakySingleton -> context -> SecondActivity

罪證確鑿! 正是 LeakySingleton 這個靜態物件(生命週期與 App 一樣長)持有了 SecondActivity 的引用,導致 SecondActivity 在關閉後也無法被 GC 回收,造成了記憶體洩漏。

今日總結
今天,我們成功地從 CPU 戰場轉移到了記憶體戰場,並學會了:

如何使用 Memory Profiler 觀察記憶體使用情況。

理解 Heap Dump 的概念,並手動捕獲記憶體快照。

透過分析 Heap Dump,成功揪出了一個由靜態引用造成的典型 Activity 洩漏。

手動分析 Heap Dump 是一個非常強大的偵錯技巧,但它相對繁瑣。如果每次都要這樣操作,效率未免太低。

那麼,有沒有一個自動化的「哨兵」,能在我們開發時就即時回報洩漏問題呢?答案是肯定的。明天,我們將為專案部署大名鼎鼎的 LeakCanary,讓它成為我們永不疲倦的記憶體洩漏哨兵。

我們明天見!


上一篇
# Day 3:【CPU Profiler】火焰圖 (Flame Charts) 完全解析
下一篇
# Day 5:【Memory Profiler】LeakCanary:你的自動化洩漏哨兵
系列文
Android 性能戰爭:從 Profiler 開始的 30 天優化實錄8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言