DEV Community

Papon Ahasan
Papon Ahasan

Posted on • Edited on

Memory Leaks

🧠 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
Enter fullscreen mode Exit fullscreen mode

✅ 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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. Holding a reference to an Activity or View in a Singleton ❌ Problem:
object MyManager {
    var context: Context? = null // holding activity context
}
Enter fullscreen mode Exit fullscreen mode

📌 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
Enter fullscreen mode Exit fullscreen mode
  1. 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)
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ Solution:

Make the runnable a static class or cancel delayed tasks in onDestroy().

override fun onDestroy() {
    super.onDestroy()
    handler.removeCallbacksAndMessages(null)
}
Enter fullscreen mode Exit fullscreen mode
  1. 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)
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ Solution:

  • Pass applicationContext if needed.
  • Cancel coroutine on lifecycle.
fun loadData(appContext: Context) {
    CoroutineScope(Dispatchers.IO).launch {
        val db = DB.getInstance(appContext)
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. 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
}
Enter fullscreen mode Exit fullscreen mode

✅ Solution:

Always observe with LifecycleOwner (Activity or Fragment) unless you manually remove observers.

viewModel.data.observe(viewLifecycleOwner) { ... }
Enter fullscreen mode Exit fullscreen mode
  1. Dialogs or Toasts shown after Activity is destroyed ❌ Problem:
fun showDialog(context: Context) {
    AlertDialog.Builder(context)
        .setMessage("Hello")
        .show()
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode
  1. 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
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ Solution:

Clear binding in onDestroyView():

override fun onDestroyView() {
    super.onDestroyView()
    _binding = null
}
Enter fullscreen mode Exit fullscreen mode
  1. Custom callbacks or listeners not removed ❌ Problem:
someView.setOnClickListener {
    // Holds reference to outer Activity
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode
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()
    }
}
Enter fullscreen mode Exit fullscreen mode
private var weakActivity: WeakReference<Activity>? = null

fun bindActivity(activity: Activity) {
    weakActivity = WeakReference(activity)
}

fun doSomething() {
    val activity = weakActivity?.get() ?: return
    // Use it safely
}
Enter fullscreen mode Exit fullscreen mode

❌ Problem: 🔴 Without Job — āϏāĻŽāĻ¸ā§āϝāĻž:

👉 āϝāĻĻāĻŋ user ā§Ē āϏ⧇āϕ⧇āĻ¨ā§āĻĄā§‡āϰ āĻŽāĻ§ā§āϝ⧇ āĻŦāĻžāϰāĻŦāĻžāϰ call āĻ•āϰ⧇, āϏāĻŦ coroutine queue āϤ⧇ āĻĸ⧁āϕ⧇ āϝāĻžāĻŦ⧇ → multiple popups → đŸ˜ĩ UI bug!

lifecycleScope.launch {
    delay(4000)
    getDrugDetailPopupImage(genericId)
}
Enter fullscreen mode Exit fullscreen mode

đŸŸĸ With Job — Smart & Controlled: 👉 āĻāĻ•āĻŦāĻžāϰ⧇ āĻļ⧁āϧ⧁āĻŽāĻžāĻ¤ā§āϰ āĻāĻ•āϟāĻž popup āϚāϞāĻŦ⧇āĨ¤ āύāϤ⧁āύ āĻ•āĻžāϜ āĻāϞ⧇ āĻĒ⧁āϰāύ⧋āϟāĻž āϕ⧇āĻŸā§‡ āϝāĻžāĻŦ⧇āĨ¤

private var popupJob: Job? = null

popupJob?.cancel() // āĻĒ⧁āϰāύ⧋ āϚāϞāĻŽāĻžāύ coroutine āĻŦāĻ¨ā§āϧ
popupJob = lifecycleScope.launch {
    delay(4000)
    getDrugDetailPopupImage(genericId)
}
Enter fullscreen mode Exit fullscreen mode

📌 āĻāϰ āωāĻĻā§āĻĻ⧇āĻļā§āϝ:

  • āφāϗ⧇āϰ 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"
}
Enter fullscreen mode Exit fullscreen mode

Leak Report āĻĻ⧇āĻ–āϤ⧇ āϕ⧇āĻŽāύ āĻšā§Ÿ?

// âžĄī¸ āĻāχ report āĻŦāϞāϛ⧇ MyActivity destroy āĻšāĻ“ā§ŸāĻžāϰ āĻĒāϰ⧇āĻ“ mContext āϰ⧇āĻĢāĻžāϰ⧇āĻ¨ā§āϏ⧇ āĻĨāĻžāĻ•āĻžāϰ āĻ•āĻžāϰāϪ⧇ release āĻšā§ŸāύāĻŋāĨ¤
ActivityLeakActivity has leaked:
â†ŗ xyz.MyActivity instance
   |   mContext → xyz.MyActivity
   |   mDestroyed → true
Enter fullscreen mode Exit fullscreen mode

Tip1: Leak Canary Debug Log:

D LeakCanary: Watching instance of xyz.MyActivity (Activity) with key XYZ123...
Enter fullscreen mode Exit fullscreen mode
â”Ŧ
├─ com.example.MyActivity
│    Leaking: YES
│    Retaining 1.2 MB in 10060 bytes
│    ↓ MyActivity.someStaticReference
│                        ~~~~~~~~~~~~~
├─ android.widget.SomeView
│    ↓ View.mContext
│               ~~~~~~~~
...
Enter fullscreen mode Exit fullscreen mode


- MyFragment.binding null āĻ•āϰāĻž āĻšā§ŸāύāĻŋ, āϤāĻžāχ View āĻāϰ āĻŽāĻ§ā§āϝ⧇ āĻĨāĻžāĻ•āĻž context (Activity) leak āĻšā§Ÿā§‡āϛ⧇āĨ¤

✅ āϏāĻŽāĻžāϧāĻžāύ: onDestroyView() āĻ binding = null āĻ•āϰ⧇ āĻĻāĻŋāύāĨ¤

â”Ŧ
├─ MyFragment
│    Leaking: YES
│    ↓ MyFragment.binding
│                 ~~~~~~~
├─ MyFragmentBinding
│    ↓ binding.textView
│              ~~~~~~~~
├─ TextView
│    ↓ View.mContext
│              ~~~~~~~
Enter fullscreen mode Exit fullscreen mode

āĻĒā§āϰāϤāĻŋāϟāĻŋ āϞāĻžāχāύ āĻĻ⧇āϖ⧁āύ āϝ⧇āϟāĻžāϤ⧇ ↓ (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 āϏ⧇āχ āĻ•āĻžāĻœā§‡ āϏāĻžāĻšāĻžāĻ¯ā§āϝ āĻ•āϰ⧇āĨ¤
Enter fullscreen mode Exit fullscreen mode

Top comments (0)