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.
Resources like database Cursors, file streams, MediaPlayer, Bitmap objects, and custom View animators must be explicitly closed or released when they’re no longer needed. Forgetting to do so causes both heap memory leaks and native memory exhaustion, which won’t always surface in Java heap dumps.
Always use Kotlin’s use {} extension instead of try/finally for any Closeable resource. It calls close() on both the success and the exception path, and it’s harder to forget than a manual finally block.
// ❌ LEAK: BufferedReader never closed on exception path
fun readConfig(file: File): Config {
val reader = BufferedReader(FileReader(file))
return Json.decodeFromReader(reader)
// If decodeFromReader throws, reader is never closed → file handle + memory leak
}
// ✅ CORRECT: Kotlin use {} closes on success AND exception (try-with-resources)
fun readConfig(file: File): Config =
BufferedReader(FileReader(file)).use { Json.decodeFromReader(it) }
// ✅ Cursor via use {}
fun getNames(db: SQLiteDatabase): List<String> =
db.rawQuery("SELECT name FROM users", null).use { cursor ->
buildList { while (cursor.moveToNext()) add(cursor.getString(0)) }
}
// ✅ MediaPlayer in a Fragment
class AudioFragment : Fragment() {
private var player: MediaPlayer? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
player = MediaPlayer.create(requireContext(), R.raw.bgm).also { it.start() }
}
override fun onDestroyView() {
super.onDestroyView()
player?.stop()
player?.release()
player = null
}
}
1.10 Custom View lifecycle — animators, bitmaps, and listeners
Custom Views must release every resource they acquire when detached from the window. onAttachedToWindow and onDetachedFromWindow are the correct hooks for this pairing.
class AnimatedRingView(context: Context) : View(context) {
private var pulseAnimator: ObjectAnimator? = null
private var iconBitmap: Bitmap? = null
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) { invalidate() }
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
pulseAnimator = ObjectAnimator.ofFloat(this, "alpha", 0f, 1f).apply {
repeatCount = ObjectAnimator.INFINITE; start()
}
val cm = context.getSystemService(ConnectivityManager::class.java)
cm.registerDefaultNetworkCallback(networkCallback)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
pulseAnimator?.cancel()
pulseAnimator = null
iconBitmap?.recycle()
iconBitmap = null
val cm = context.getSystemService(ConnectivityManager::class.java)
cm.unregisterNetworkCallback(networkCallback)
}
}
1.11 Bitmap and large object memory management
// ❌ LEAK: unbounded static cache of Bitmaps
companion object {
val bitmapCache = HashMap<String, Bitmap>() // grows forever; bitmaps are large
}
// ✅ CORRECT: LruCache with size-aware eviction
val bitmapCache = object : LruCache<String, Bitmap>(
(Runtime.getRuntime().maxMemory() / 1024L / 8L).toInt() // 1/8 of max heap in KB
) {
override fun sizeOf(key: String, value: Bitmap) = value.byteCount / 1024
}
// ✅ Always prefer Glide / Coil for image loading — they manage pooling, lifecycle binding, and OOM
Glide.with(viewLifecycleOwner).load(url).into(imageView)
// Coil:
imageView.load(url) { lifecycle(viewLifecycleOwner) }
1.12 Services running unnecessarily
Per Android official memory guidance: “leaving unnecessary services running is one of the worst memory-management mistakes an Android app can make.” A running Service keeps the process alive at elevated priority — RAM is never freed until the Service stops.
// ❌ WRONG: service that never stops itself; stays alive indefinitely
class SyncService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
doSync()
return START_STICKY // restarts forever — keeps process alive; RAM never freed
}
}
// ✅ CORRECT: stop self after work is done
class SyncService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
CoroutineScope(Dispatchers.IO + Job()).launch {
doSync()
stopSelf(startId) // releases service memory
}
return START_NOT_STICKY
}
}
// ✅ BETTER: use WorkManager for background, deferrable, or constraint-based work
val request = OneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
.build()
WorkManager.getInstance(context).enqueue(request)
1.13 WebView process isolation
WebView has well-documented native memory leaks that affect the host process. Isolating it in a separate process prevents those leaks from exhausting your app’s heap.
<!-- Isolate WebView in a separate process to prevent its leaks from affecting your app -->
<activity
android:name=".WebViewActivity"
android:process=":webview" />
The onDestroy cleanup sequence must remove the WebView from the view hierarchy before calling destroy(), otherwise the WebView may hold references back to the Activity:
override fun onDestroy() {
(binding.webView.parent as? ViewGroup)?.removeView(binding.webView)
binding.webView.apply {
stopLoading()
clearHistory()
removeAllViews()
destroy()
}
super.onDestroy()
}
1.14 TypedArray not recycled in custom View init {}
Root cause: context.obtainStyledAttributes() allocates a TypedArray backed by a native pointer into the resource framework. If recycle() is not called, the native memory is never returned to the pool. This is a native/heap leak that LeakCanary won’t always surface because no Java/Kotlin object graph is involved.
// ❌ LEAK: TypedArray never recycled — native memory held forever
class RatingBarView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {
init {
val ta = context.obtainStyledAttributes(attrs, R.styleable.RatingBarView)
val starColor = ta.getColor(R.styleable.RatingBarView_starColor, Color.YELLOW)
val starCount = ta.getInt(R.styleable.RatingBarView_starCount, 5)
// ta.recycle() missing → native resource pool exhausted over time
}
}
// ✅ CORRECT option A: manual recycle in finally block
class RatingBarView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {
init {
val ta = context.obtainStyledAttributes(attrs, R.styleable.RatingBarView)
try {
val starColor = ta.getColor(R.styleable.RatingBarView_starColor, Color.YELLOW)
val starCount = ta.getInt(R.styleable.RatingBarView_starCount, 5)
} finally {
ta.recycle() // REQUIRED: always in finally so it runs even on exception
}
}
}
// ✅ CORRECT option B: Kotlin use {} extension (clean, exception-safe)
class RatingBarView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {
init {
context.obtainStyledAttributes(attrs, R.styleable.RatingBarView).use { ta ->
val starColor = ta.getColor(R.styleable.RatingBarView_starColor, Color.YELLOW)
val starCount = ta.getInt(R.styleable.RatingBarView_starCount, 5)
} // recycle() called automatically
}
}