Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/AmolPardeshi99/android-performance-skills/llms.txt

Use this file to discover all available pages before exploring further.

The most common source of long-lived Activity leaks is storing an Activity instance — or any object that holds one — in a location that outlives the Activity’s lifecycle. Static fields, singleton objects, and non-static inner classes are GC roots or connected to GC roots, and any Activity reference they hold prevents garbage collection.

1.2 Context misuse — Activity context in long-lived objects

A static Activity reference is a process-lifetime leak. Static fields are GC roots: anything reachable from them survives until the process dies. An Activity carries its entire View tree, all loaded Bitmaps, and the Context chain — storing one in a singleton can retain 5–50 MB indefinitely.
Root cause: Activity is a Context. Any long-lived object storing an Activity reference prevents that Activity from being GC’d. The worst offenders are singletons and static fields, which are GC roots and survive the process lifetime.
// ❌ LEAK: singleton stores Activity context
object ImageLoader {
    private var context: Context? = null
    fun init(activity: Activity) { context = activity }  // Activity is never GC'd
}

// ❌ LEAK: custom View stores Activity as a field
class BadView(context: Context) : View(context) {
    private val hostActivity = context as Activity  // strong ref; outlives View
}

// ✅ CORRECT: use applicationContext in singletons
object ImageLoader {
    private lateinit var appContext: Context
    fun init(ctx: Context) { appContext = ctx.applicationContext }  // lives as long as app
}

// ✅ CORRECT: WeakReference when Activity access is genuinely needed in long-lived objects
class SmartView(context: Context) : View(context) {
    private val weakActivity = WeakReference(context as? Activity)

    fun doActivityWork() {
        weakActivity.get()?.let { activity ->
            if (!activity.isFinishing && !activity.isDestroyed) {
                // safe to use
            }
        }
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        // release all resources
    }
}

Context selection quick reference

Object lifetimeCorrect context
Singletons, Room, OkHttpClient, WorkManagerapplicationContext
View inflation, dialogs, startActivityActivity context — only within lifecycle
System services tied to UI (LayoutInflater)Activity context
System services hardware-related (LocationManager, AlarmManager, CameraManager)applicationContext
RecyclerView Adaptersview.context — never store the Activity reference
ComposablesLocalContext.current — never hoist into ViewModel or singleton

1.3 Static fields and companion objects holding Views or Activities

Root cause: Static fields are GC roots. Any object stored in a static field lives for the process lifetime. A View holds a reference to its Context (usually an Activity) — a static View leaks the entire Activity plus the View tree.
// ❌ LEAK
class SplashActivity : AppCompatActivity() {
    companion object {
        var instance: SplashActivity? = null   // GC root → Activity never GC'd
        var logoView: ImageView? = null        // View → Context → Activity
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        instance = this
        logoView = binding.logo
    }
}

// ✅ CORRECT: share state via ViewModel or application-scoped event bus; no static UI references
class SplashViewModel : ViewModel() {
    val splashComplete = MutableStateFlow(false)
}

// ✅ If a static reference is truly unavoidable: WeakReference + null in onDestroy
class SplashActivity : AppCompatActivity() {
    companion object {
        private var weakRef: WeakReference<SplashActivity>? = null
        fun instance(): SplashActivity? = weakRef?.get()
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        weakRef = WeakReference(this)
    }
    override fun onDestroy() { super.onDestroy(); weakRef = null }
}

1.4 Non-static inner classes and anonymous classes

Root cause: In Kotlin and Java, a non-static inner class or an anonymous class defined inside an Activity or Fragment captures an implicit this reference to the outer class. If the inner class is passed to a long-lived object (such as a singleton event bus), the outer class is retained indefinitely.
// ❌ LEAK: anonymous object captures 'this' (Activity) → EventBus (singleton) holds it forever
class OrderActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        EventBus.subscribe(object : EventListener {
            override fun onOrderUpdate(event: OrderEvent) {
                updateUI(event)  // captures OrderActivity implicitly
            }
        })
        // No matching unsubscribe → Activity leaks for app lifetime
    }
}

// ✅ CORRECT: store listener reference; unsubscribe in lifecycle teardown
class OrderActivity : AppCompatActivity() {
    private val orderListener = EventListener { event -> updateUI(event) }

    override fun onStart()  { super.onStart(); EventBus.subscribe(orderListener) }
    override fun onStop()   { super.onStop(); EventBus.unsubscribe(orderListener) }
}

// ✅ CORRECT (Java static inner class pattern to avoid implicit reference)
class MyActivity extends AppCompatActivity {
    // Non-static: captures outer class → potential leak
    // class BadRunnable implements Runnable { void run() { doUiWork(); } }

    // Static: no implicit outer reference
    static class SafeRunnable implements Runnable {
        private final WeakReference<MyActivity> ref;
        SafeRunnable(MyActivity a) { ref = new WeakReference<>(a); }
        @Override public void run() {
            MyActivity a = ref.get();
            if (a != null && !a.isFinishing()) a.doUiWork();
        }
    }
}

Build docs developers (and LLMs) love