đ§ 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)