đ§ When Can Memory Leaks Happen?
Memory leaks in Android typically happen when an object that holds a reference to a Context (especially Activity) outlives its lifecycle, preventing the system from garbage collecting the Activity.
đ§Š Types of Context and When to Use Which
â đĢ Avoid Memory Leaks with These Tips
â 1. Use applicationContext when:
You donât need to access UI
You're working in long-lived classes: e.g., repositories, databases, analytics trackers
val context = context.applicationContext
â 2. Avoid Anonymous Inner Classes in Activities
Use static classes or top-level classes to avoid holding implicit references to Activities.
â 3. Clear Resources in Lifecycle
Always clear:
Observers (LiveData, BroadcastReceiver)
Coroutine scopes (Job.cancel())
Ad SDKs (nativeAd.destroy())
â 4. Donât Hold Context in Singletons
If you must, store applicationContext, not Activity.
class MySingleton private constructor(private val context: Context) {
companion object {
fun init(appContext: Context): MySingleton {
return MySingleton(appContext.applicationContext)
}
}
}
- Holding a reference to an Activity or View in a Singleton â Problem:
object MyManager {
var context: Context? = null // holding activity context
}
đ Real-World Example:
You create a singleton for analytics or ads that stores an Activity context to show a Toast or dialog â but forget to clear it.
â Solution:
Always use context.applicationContext in singletons.
context = context.applicationContext
- Inner classes or anonymous classes (e.g. Runnable, Listener) holding implicit reference to outer Activity
â Problem:
class MyActivity : AppCompatActivity() {
private val handler = Handler(Looper.getMainLooper())
fun start() {
handler.postDelayed({
// 'this' refers to Activity, leak if Activity is destroyed
}, 10000)
}
}
â Solution:
Make the runnable a static class or cancel delayed tasks in onDestroy().
override fun onDestroy() {
super.onDestroy()
handler.removeCallbacksAndMessages(null)
}
- Long-running background tasks (Coroutines, AsyncTask, Threads) holding context
â Problem:
You start a coroutine in a ViewModel or plain class and hold an Activity context.
fun loadData(context: Context) {
CoroutineScope(Dispatchers.IO).launch {
// Holding context across configuration change or after activity is gone
val db = DB.getInstance(context)
}
}
â Solution:
- Pass applicationContext if needed.
- Cancel coroutine on lifecycle.
fun loadData(appContext: Context) {
CoroutineScope(Dispatchers.IO).launch {
val db = DB.getInstance(appContext)
}
}
- LiveData or Flow observing with lifecycle issues
â Problem:
You observe LiveData from ViewModel in a Context-based class (like a custom manager), not tied to lifecycle.
viewModel.data.observeForever {
// Forever means it never stops -> leak
}
â Solution:
Always observe with LifecycleOwner (Activity or Fragment) unless you manually remove observers.
viewModel.data.observe(viewLifecycleOwner) { ... }
- Dialogs or Toasts shown after Activity is destroyed â Problem:
fun showDialog(context: Context) {
AlertDialog.Builder(context)
.setMessage("Hello")
.show()
}
If context is an Activity and itâs already finishing, the window leaks.
â
Solution:
Check context:
if (context is Activity && !context.isFinishing) {
AlertDialog.Builder(context)
.setMessage("Safe")
.show()
}
- Static Views or holding binding in fragments after view is destroyed â Problem:
class MyFragment : Fragment() {
private lateinit var binding: FragmentMyBinding
override fun onDestroyView() {
super.onDestroyView()
// Forget to clear binding -> view leak
}
}
â Solution:
Clear binding in onDestroyView():
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
- Custom callbacks or listeners not removed â Problem:
someView.setOnClickListener {
// Holds reference to outer Activity
}
If the view lives longer (e.g., part of an SDK), it keeps the activity alive.
â
Solution:
Remove listeners:
override fun onDestroy() {
someView.setOnClickListener(null)
}
class MySingleton private constructor(private val context: Context) {
companion object {
@Volatile
private var INSTANCE: MySingleton? = null
fun getInstance(appContext: Context): MySingleton {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: MySingleton(appContext.applicationContext).also { INSTANCE = it }
}
}
}
// Example usage of application context
fun doSomethingGlobal() {
Toast.makeText(context, "Doing something!", Toast.LENGTH_SHORT).show()
}
}
private var weakActivity: WeakReference<Activity>? = null
fun bindActivity(activity: Activity) {
weakActivity = WeakReference(activity)
}
fun doSomething() {
val activity = weakActivity?.get() ?: return
// Use it safely
}
â Problem: đ´ Without Job â āϏāĻŽāϏā§āϝāĻž:
đ āϝāĻĻāĻŋ user ā§Ē āϏā§āĻā§āύā§āĻĄā§āϰ āĻŽāϧā§āϝ⧠āĻŦāĻžāϰāĻŦāĻžāϰ call āĻāϰā§, āϏāĻŦ coroutine queue āϤ⧠āĻĸā§āĻā§ āϝāĻžāĻŦā§ â multiple popups â đĩ UI bug!
lifecycleScope.launch {
delay(4000)
getDrugDetailPopupImage(genericId)
}
đĸ With Job â Smart & Controlled: đ āĻāĻāĻŦāĻžāϰ⧠āĻļā§āϧā§āĻŽāĻžāϤā§āϰ āĻāĻāĻāĻž popup āĻāϞāĻŦā§āĨ¤ āύāϤā§āύ āĻāĻžāĻ āĻāϞ⧠āĻĒā§āϰāύā§āĻāĻž āĻā§āĻā§ āϝāĻžāĻŦā§āĨ¤
private var popupJob: Job? = null
popupJob?.cancel() // āĻĒā§āϰāύ⧠āĻāϞāĻŽāĻžāύ coroutine āĻŦāύā§āϧ
popupJob = lifecycleScope.launch {
delay(4000)
getDrugDetailPopupImage(genericId)
}
đ āĻāϰ āĻāĻĻā§āĻĻā§āĻļā§āϝ:
- āĻāĻā§āϰ popup coroutine āĻāϞāĻŽāĻžāύ āĻĨāĻžāĻāϞ⧠āϤāĻž cancel āĻāϰ⧠āĻĢā§āϞāĻž
- āύāϤā§āύ coroutine āĻĻāĻŋā§ā§ popup image delayed show āĻāϰāĻž
- āĻāĻāĻ āϏāĻŽā§ āĻāĻāĻāĻŋāϰ āĻŦā§āĻļāĻŋ popup āύāĻž āĻĻā§āĻāĻžāύā§
- đ§ Memory leak āϰā§āϧ āύāĻž āĻāĻžāĻāϤā§āĻ āĻāϞāϤ⧠āĻĨāĻžāĻāĻž coroutine āĻŦāύā§āϧ āĻāϰ⧠āĻĻā§
āĻŽā§āĻŽā§āϰāĻŋ āϞāĻŋāĻ āĻā§?
āĻŽā§āĻŽā§āϰāĻŋ āϞāĻŋāĻ āϤāĻāύ āĻšā§, āϝāĻāύ āĻāĻĒāύāĻžāϰ āĻ ā§āϝāĻžāĻĒā§āϰ āĻ āĻŦāĻā§āĻā§āĻāĻā§āϞ⧠āĻāĻžāϰāĻŦā§āĻ āĻāĻžāϞā§āĻā§āĻāϰ (Garbage Collector) āϰāĻŋāĻŽā§āĻ āĻāϰāϤ⧠āĻĒāĻžāϰ⧠āύāĻž, āĻāĻžāϰāĻŖ āĻāĻāύāĻ āĻā§āĻ āϏā§āĻ āĻ āĻŦāĻā§āĻā§āĻā§āϰ āϰā§āĻĢāĻžāϰā§āύā§āϏ āϧāϰ⧠āϰā§āĻā§āĻā§ â āϝāĻĻāĻŋāĻ āϏā§āĻ āĻ āĻŦāĻā§āĻā§āĻ āĻĻāϰāĻāĻžāϰ āύā§āĻāĨ¤ āĻĢāϞ⧠āĻ ā§āϝāĻžāĻĒā§āϰ RAM āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻŦāĻžā§ā§, āĻĒāĻžāϰāĻĢāϰāĻŽā§āϝāĻžāύā§āϏ āĻāĻŽā§, āĻāĻŽāύāĻāĻŋ āĻ ā§āϝāĻžāĻĒ āĻā§āϰā§āϝāĻžāĻļāĻ āĻāϰāϤ⧠āĻĒāĻžāϰā§āĨ¤
LeakCanary āĻšāϞ Square āĻā§āĻŽā§āĻĒāĻžāύāĻŋāϰ āĻŦāĻžāύāĻžāύ⧠āĻāĻāĻāĻž open-source Android library āϝāĻž āĻŽā§āĻŽā§āϰāĻŋ āϞāĻŋāĻ āĻ
āĻā§ āĻĄāĻŋāĻā§āĻā§āĻ āĻāϰāϤ⧠āĻĒāĻžāϰ⧠āĻĄā§āĻā§āϞāĻĒāĻŽā§āύā§āĻ āĻŦāĻŋāϞā§āĻĄā§āĨ¤
// build.gradle (app)
dependencies {
debugImplementation "com.squareup.leakcanary:leakcanary-android:2.13"
}
Leak Report āĻĻā§āĻāϤ⧠āĻā§āĻŽāύ āĻšā§?
// âĄī¸ āĻāĻ report āĻŦāϞāĻā§ MyActivity destroy āĻšāĻā§āĻžāϰ āĻĒāϰā§āĻ mContext āϰā§āĻĢāĻžāϰā§āύā§āϏ⧠āĻĨāĻžāĻāĻžāϰ āĻāĻžāϰāĻŖā§ release āĻšā§āύāĻŋāĨ¤
ActivityLeakActivity has leaked:
âŗ xyz.MyActivity instance
| mContext â xyz.MyActivity
| mDestroyed â true
Tip1: Leak Canary Debug Log:
D LeakCanary: Watching instance of xyz.MyActivity (Activity) with key XYZ123...
âŦ
ââ com.example.MyActivity
â Leaking: YES
â Retaining 1.2 MB in 10060 bytes
â â MyActivity.someStaticReference
â ~~~~~~~~~~~~~
ââ android.widget.SomeView
â â View.mContext
â ~~~~~~~~
...
- MyFragment.binding null āĻāϰāĻž āĻšā§āύāĻŋ, āϤāĻžāĻ View āĻāϰ āĻŽāϧā§āϝ⧠āĻĨāĻžāĻāĻž context (Activity) leak āĻšā§ā§āĻā§āĨ¤
â
āϏāĻŽāĻžāϧāĻžāύ: onDestroyView() āĻ binding = null āĻāϰ⧠āĻĻāĻŋāύāĨ¤
âŦ
ââ MyFragment
â Leaking: YES
â â MyFragment.binding
â ~~~~~~~
ââ MyFragmentBinding
â â binding.textView
â ~~~~~~~~
ââ TextView
â â View.mContext
â ~~~~~~~
āĻĒā§āϰāϤāĻŋāĻāĻŋ āϞāĻžāĻāύ āĻĻā§āĻā§āύ āϝā§āĻāĻžāϤ⧠â (down arrow) āĻāĻā§, LeakCanary āĻāĻ arrows āĻĻāĻŋā§ā§ āĻŦā§āĻāĻžā§ āĻāĻžāϰ āĻŽāĻžāϧā§āϝāĻŽā§ āϰā§āĻĢāĻžāϰā§āύā§āϏ āϧāϰ⧠āĻāĻā§:
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads() // āĻĄāĻŋāϏā§āĻ āĻĨā§āĻā§ āĻĄāĻžāĻāĻž āĻĒā§āĻž āĻšāϞ⧠āϏāϤāϰā§āĻ āĻāϰāĻŦā§
.detectDiskWrites() // āĻĄāĻŋāϏā§āĻā§ āϞā§āĻāĻž āĻšāϞ⧠āϏāϤāϰā§āĻ āĻāϰāĻŦā§
.detectNetwork() // āύā§āĻāĻā§āĻžāϰā§āĻ āĻāϞ āĻšāϞ⧠āϏāϤāϰā§āĻ āĻāϰāĻŦā§
.penaltyLog() // āĻāϏāĻŦ āĻāĻāύāĻžāϰ āϏāĻŽā§ āϞāĻ āĻāϰāĻŦā§
.build()
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects() // āϝā§āϏāĻŦ Sqlite āĻ
āĻŦāĻā§āĻā§āĻ āĻŦāύā§āϧ āĻšā§āύāĻŋ, āϏā§āĻā§āϞ⧠āĻāĻŋāύāĻŦā§
.detectLeakedClosableObjects() // āϝā§āϏāĻŦ āĻā§āϞā§āĻā§āĻŦāϞ āĻ
āĻŦāĻā§āĻā§āĻ (āϝā§āĻŽāύ āĻĢāĻžāĻāϞ, āϏā§āĻā§āϰā§āĻŽ) āĻŦāύā§āϧ āĻšā§āύāĻŋ āϏā§āĻā§āϞ⧠āĻāĻŋāύāĻŦā§
.penaltyLog() // āĻāϏāĻŦ āĻāϏā§āϝ⧠āĻšāϞ⧠āϞāĻ āĻāϰāĻŦā§
.build()
)
- āĻŽā§āĻāύ āĻĨā§āϰā§āĻĄā§ āĻĄāĻŋāϏā§āĻ āĻŦāĻž āύā§āĻāĻā§āĻžāϰā§āĻ āĻ
āĻĒāĻžāϰā§āĻļāύ āĻšāϞ⧠āϤā§āĻŽāĻžāĻā§ āĻāĻžāύāĻžāĻŦā§
- āϝā§āϏāĻŦ āĻĄā§āĻāĻžāĻŦā§āϏ āĻŦāĻž āĻĢāĻžāĻāϞ āϰāĻŋāϏā§āϰā§āϏ āϤā§āĻŽāĻŋ āĻā§āϞ⧠āĻŦāύā§āϧ āĻāϰ⧠āύāĻŋ, āϏā§āĻā§āϞā§āĻ āϧāϰāĻŦā§
- āϏāĻŦāĻ āϞāĻ āĻāϰ⧠āĻĄā§āĻā§āϞāĻĒāĻžāϰāĻā§ āĻāϏā§āϝ⧠āϏāĻŽā§āĻĒāϰā§āĻā§ āϏāĻā§āϤāύ āĻāϰāĻŦā§
āĻā§āύ āĻĻāϰāĻāĻžāϰ?
āĻ
ā§āϝāĻžāĻĒ āĻ āĻŋāĻāĻ āĻžāĻ āĻāĻŦāĻ āĻĻā§āϰā§āϤ āĻāĻžāĻ āĻāϰā§āĻ āĻāϰ āĻāĻāĻāĻžāϰ āĻāĻžāϞ⧠āĻ
āĻāĻŋāĻā§āĻāϤāĻž āĻĒāĻžā§, āϏā§āĻāύā§āϝ āϝā§āϏāĻŦ āĻāĻžāĻ āĻŽā§āĻāύ āĻĨā§āϰā§āĻĄ āĻŦā§āϞāĻ āĻāϰ⧠āϏā§āĻā§āϞ⧠āĻā§āĻāĻā§ āĻŦā§āϰ āĻāϰāĻž āĻā§āĻŦ āĻāϰā§āϰāĻŋāĨ¤ StrictMode āϏā§āĻ āĻāĻžāĻā§ āϏāĻžāĻšāĻžāϝā§āϝ āĻāϰā§āĨ¤
Top comments (0)