Android 效能優化系列 — 15 Memory leak 記憶體洩漏

Evan Chen
7 min readOct 5, 2022

--

Photo by Marianna Lutkova on Unsplash

當我們產生了一個物件,就會在 Heap 中配置一塊記憶體來處理,當一個物件不再使用時,就會進行 GC (Garbage Collection ) 垃圾回收機制,將資源釋放。當一個物件不再被使用卻無法被回收時,我們稱為 Memory leak 記憶體洩漏。當 Memory leak 越來越多,就代表被占用無法回收的記憶體越多,最後就可能會造成 OOM。

Activity 被 Singleton 所參考

當 Activity 被 Singleton 所參考時會造成 Memory leak。下方程式碼用 object 宣告了一個 Singleton Class1 帶有一個屬性 context。在MainActivity裡的 Class1.context = this,就代表 Activity 被這個 Class1 所參考了。當我們按下返回鍵離開這個 Activity 時,Activity 就因為被一個比它生命週期還長的 Singleton 所參考而無法釋放資源。當一個不再使用的資源無法被釋放,就是 Memory leak。

object Class1 {
lateinit var context: Context
}
class MainActivity2 : AppCompatActivity() {
private val binding by lazy { ActivityMain2Binding.inflate(layoutInflater) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
//Activity 被一個static 參考
Class1.context = this
}
}

要知道有沒有產生 Memory leak,可以使用 Profiler 工具。點選 Run → profile app 來開啟 Profiler。開啟後的畫面如下圖呈現了CPU、MEMORY、ENERGY 的使用狀況。

點選 MEMORY 區塊,可以看到記憶體的更多資訊。

接著點選 Capture heap dump 來看 heap 的資料。

Dump 之後會看到 Heap 的資料,當看到有一個黃色的警告就表示有 Memory leak。

後續會有更詳細的介紹 Profiler 工具,讓我們繼續來看其他的 Memory leak。

把 View 放到 static property

在 Kotlin 的 static property 是以 companion object 來表示。如果再裡面放一個 View 也會造成Activity 被銷毀時無法釋放資源。

class MainActivity : AppCompatActivity() {    companion object {
private var textView: TextView? = null
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main7)
textView = findViewById(R.id.textView)
}
}

其實像這樣 Memory leak,Android Studio 是可以偵測出來的,所以平常就要注意 Android Studio 的警告提示。

Activity 裡的背景執行緒處理

在 Activity 裡執行一段非同步處理未完成就按返回鍵離開,本應該在離開時被銷毀的 Activity,因為這個非同步未完成造成 Memory leak。

class MainActivity : AppCompatActivity() {
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
//未執行完離開App會造成memory leak
object : Thread() {
override fun run() {
sleep(10000)
runOnUiThread {
binding.result.text = "Done"
}
}
}.start()
}
}

未取消註冊 BroadcastReceiver

BroadcastReceiver 應在 onResume 時註冊,在 onPause 取消註冊。若未取消註冊將造成 Memory leak。

建立 BroadcastReceiver

class MyBroadcastReceiver:BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
Log.d("MyBroadcastReceiver", "Receive")
}
}

Activity 在 onResume 註冊 BroadcastReceiver,未在 onPause 取消註冊。

class MainActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
//未取消註冊BroadcastReceiver
val intentFilter = IntentFilter("evan.chen.tutorial.broadcast.Action1")
registerReceiver(broadcastReceiver, intentFilter)
}
override fun onPause() {
super.onPause()
//若沒有加上這段取消註冊 BroadcastReceiver,會造成 Memory leak
//unregisterReceiver(broadcastReceiver)
}
}

從以上常見 Memory leak 可以知道當一個較短生命週期的物件被一個較長生命週期的物件所參考,短生命週期的物件就無法在應該結束生命週期時結束。更遭的情況是這個長生命週期是像 static 這種跟 Application 的生命週期是一樣的,也就是只有在 App 結束時,這些被參考的物件才會被釋放。

下一篇:Application context 與 Activity context

--

--