各位戰士,歡迎來到第四天的戰場。結束了對 CPU 的偵查後,我們將目光轉向另一個關鍵資源:記憶體 (Memory)。一個健康的應用程式應該像一個紀律嚴明的軍營,物件(士兵)在需要時被創建,在任務完成後就該被回收(銷毀)。
但如果有些士兵任務結束後還賴著不走,甚至越積越多,最終就會耗盡營地所有資源,導致整個營地崩潰。這就是記憶體洩漏 (Memory Leak),而它導致的 OutOfMemoryError
是 Android 開發中最常見也最致命的閃退原因之一。
與 CPU Profiler 類似,Memory Profiler 也是 Android Studio Profiler 工具套件的一部分。它能即時顯示你的 App 記憶體使用情況,並將其分為幾個部分:
我們的首要目標,是關注 Java 記憶體,因為大部分的應用程式碼物件都在這裡。
要找出那些不該存在的物件,我們需要對整個軍營進行一次「人口普查」。在記憶體管理中,這次普查被稱為捕獲堆積轉儲 (Capture Heap Dump)。
Heap Dump 會在你點擊按鈕的那一刻,凍結整個 Java 堆積(Heap),並將當時存在的所有物件、以及它們之間的引用關係全部記錄下來。這是一份詳細到極致的清單,也是我們抓出間諜(洩漏物件)的關鍵證據。
在 Memory Profiler 的時間線上,有兩個按鈕可以執行此操作:
記憶體洩漏最經典的案例,莫過於一個已經被銷毀的 Activity
因為被一個生命週期更長的物件持有引用,而無法被垃圾回收器 (Garbage Collector, GC) 回收。
讓我們來製造一個這樣的場景。假設我們有一個單例 (Singleton),它錯誤地持有了 Activity
的 Context
:
// 一個常見的錯誤設計:單例持有 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))
}
}
}
打開 App,進入 MainActivity
。
點擊按鈕,跳轉到 SecondActivity
。
在 SecondActivity
頁面,按下返回鍵,回到 MainActivity
。
重複步驟 2 和 3 數次,模擬使用者在兩個頁面間來回切換。
最後,回到 MainActivity
後,按下返回鍵,關閉 SecondActivity
。理論上,此時所有 SecondActivity
的實例都應該被銷毀了。
在 Memory Profiler 中,點擊垃圾桶圖示,手動觸發一次 GC,確保回收掉所有可回收的物件。
點擊 Dump Java heap 按鈕,捕獲記憶體快照。
捕獲完成後,Android Studio 會打開一個 Heap Dump 分析視窗。
在左側的類別列表中,使用右上角的搜尋框,搜尋 SecondActivity
。
如果 Leak Count > 0,恭喜你,你已經找到了洩漏的物件!這意味著記憶體中仍然存在著 SecondActivity
的實例。
在下方的 Instance View 中,點選一個 SecondActivity
的實例。
在右側的 References 視窗中,你將看到一個引用樹,它會清楚地告訴你,是誰「拉著」這個 Activity 不讓它被回收。
你應該能看到一條類似這樣的引用鏈:LeakySingleton
-> context
-> SecondActivity
罪證確鑿! 正是 LeakySingleton 這個靜態物件(生命週期與 App 一樣長)持有了 SecondActivity
的引用,導致 SecondActivity
在關閉後也無法被 GC 回收,造成了記憶體洩漏。
今日總結
今天,我們成功地從 CPU 戰場轉移到了記憶體戰場,並學會了:
如何使用 Memory Profiler 觀察記憶體使用情況。
理解 Heap Dump 的概念,並手動捕獲記憶體快照。
透過分析 Heap Dump,成功揪出了一個由靜態引用造成的典型 Activity 洩漏。
手動分析 Heap Dump 是一個非常強大的偵錯技巧,但它相對繁瑣。如果每次都要這樣操作,效率未免太低。
那麼,有沒有一個自動化的「哨兵」,能在我們開發時就即時回報洩漏問題呢?答案是肯定的。明天,我們將為專案部署大名鼎鼎的 LeakCanary,讓它成為我們永不疲倦的記憶體洩漏哨兵。
我們明天見!