Refactor high-complexity React components in Dify frontend. Use when `pnpm analyze-component...
npx skills add HuxleyMc/Android-Skills --skill "android-performance"
Install specific skill from multi-skill repository
# Description
Guides Android performance optimizations. Use when app is slow, has memory leaks, ANR issues, battery drain, or janky UI. Covers profiling, memory management, rendering optimizations, and best practices.
# SKILL.md
name: android-performance
description: Guides Android performance optimizations. Use when app is slow, has memory leaks, ANR issues, battery drain, or janky UI. Covers profiling, memory management, rendering optimizations, and best practices.
tags: ["android", "performance", "optimization", "profiling", "memory", "battery"]
difficulty: advanced
category: performance
version: "1.0.0"
last_updated: "2025-01-29"
Android Performance Optimization
Quick Start
Common performance issues and solutions:
| Issue | Tool | Solution |
|---|---|---|
| Janky UI (dropped frames) | Profile GPU Rendering | Reduce overdraw, optimize layouts |
| Memory leaks | Memory Profiler | Fix lifecycle issues, avoid static refs |
| ANR | ANR traces | Move work off main thread |
| Slow startup | Macrobenchmark | Lazy init, reduce startup work |
| Battery drain | Battery Historian | Batch work, use WorkManager |
Profiling Tools
Android Studio Profiler
CPU Profiler:
// Add to gradle for method tracing
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
}
}
}
// Custom trace markers
trace("Bitmap Processing") {
processBitmap(image)
}
Memory Profiler:
// Force garbage collection before heap dump
Runtime.getRuntime().gc()
// Check memory before heavy operation
val runtime = Runtime.getRuntime()
val usedMem = (runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024
Log.d("Memory", "Used: ${usedMem}MB")
Macrobenchmark
@RunWith(AndroidJUnit4::class)
class StartupBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun startup() = benchmarkRule.measureRepeated(
packageName = "com.example.app",
metrics = listOf(StartupTimingMetric()),
iterations = 5,
startupMode = StartupMode.COLD
) {
pressHome()
startActivityAndWait()
}
}
Core Patterns
Rendering Optimization
Reduce Overdraw:
// Enable overdraw debugging in Developer Options
// Avoid multiple backgrounds
// Bad: Container with bg + Card with bg + Content with bg
Column(
modifier = Modifier.background(Color.White) // Remove if parent already has bg
) {
Card(
modifier = Modifier.background(Color.White) // Redundant
) { }
}
// Good: Single background at appropriate level
Column {
Card { } // Card handles its own background
}
Optimize RecyclerView:
class ProductAdapter : ListAdapter<Product, ViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
// Inflate view
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
// Use setHasStableIds for better performance
holder.bind(item)
}
class DiffCallback : DiffUtil.ItemCallback<Product>() {
override fun areItemsTheSame(old: Product, new: Product) =
old.id == new.id
override fun areContentsTheSame(old: Product, new: Product) =
old == new
}
}
// Usage optimizations
recyclerView.apply {
setHasFixedSize(true) // If layout size doesn't change
layoutManager = LinearLayoutManager(context)
adapter = productAdapter
// Use appropriate prefetch
(layoutManager as LinearLayoutManager).apply {
initialPrefetchItemCount = 4
}
}
ViewHolder Pattern:
class ProductViewHolder(
private val binding: ItemProductBinding
) : RecyclerView.ViewHolder(binding.root) {
private val imageLoader = ImageLoader.getInstance()
fun bind(product: Product) {
binding.name.text = product.name
// Cancel previous load to prevent wrong image showing
imageLoader.cancel(binding.image)
imageLoader.load(product.imageUrl, binding.image)
}
}
Memory Management
Bitmap Optimization:
object BitmapUtils {
fun decodeSampledBitmap(
filePath: String,
reqWidth: Int,
reqHeight: Int
): Bitmap? {
return BitmapFactory.Options().run {
inJustDecodeBounds = true
BitmapFactory.decodeFile(filePath, this)
inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)
inJustDecodeBounds = false
inPreferredConfig = Bitmap.Config.RGB_565 // Use lower color depth if acceptable
BitmapFactory.decodeFile(filePath, this)
}
}
private fun calculateInSampleSize(
options: BitmapFactory.Options,
reqWidth: Int,
reqHeight: Int
): Int {
val (height, width) = options.run { outHeight to outWidth }
var inSampleSize = 1
if (height > reqHeight || width > reqWidth) {
val halfHeight = height / 2
val halfWidth = width / 2
while (halfHeight / inSampleSize >= reqHeight &&
halfWidth / inSampleSize >= reqWidth) {
inSampleSize *= 2
}
}
return inSampleSize
}
}
// Usage
val bitmap = BitmapUtils.decodeSampledBitmap(
imagePath,
imageView.width,
imageView.height
)
imageView.setImageBitmap(bitmap)
Memory Leak Prevention:
// Bad: Static reference to Activity
object BadCache {
var activity: Activity? = null // Memory leak!
}
// Good: Use ApplicationContext and WeakReference
class ImageCache(context: Context) {
private val context = context.applicationContext
private val cache = LruCache<String, Bitmap>(
(Runtime.getRuntime().maxMemory() / 1024 / 8).toInt()
)
fun put(key: String, bitmap: Bitmap) {
cache.put(key, bitmap)
}
fun get(key: String): Bitmap? = cache.get(key)
}
// Lifecycle-aware cleanup
class ImageLoader(private val lifecycle: Lifecycle) {
private val activeLoads = mutableListOf<Job>()
init {
lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
activeLoads.forEach { it.cancel() }
activeLoads.clear()
}
})
}
}
Object Pooling:
class PaintPool {
private val pool = ArrayDeque<Paint>(10)
fun obtain(): Paint {
return pool.removeFirstOrNull() ?: Paint().apply {
isAntiAlias = true
}
}
fun recycle(paint: Paint) {
if (pool.size < 10) {
pool.addLast(paint)
}
}
}
// Usage in custom view
override fun onDraw(canvas: Canvas) {
val paint = paintPool.obtain()
try {
paint.color = Color.RED
canvas.drawCircle(x, y, radius, paint)
} finally {
paintPool.recycle(paint)
}
}
Threading Optimization
Coroutine Dispatchers:
// CPU-intensive: Use Default
coroutineScope.launch(Dispatchers.Default) {
val result = heavyComputation()
}
// IO operations: Use IO
suspend fun fetchData(): Data = withContext(Dispatchers.IO) {
api.fetchData()
}
// UI updates: Main (default for ViewModel scope)
viewModelScope.launch {
val data = withContext(Dispatchers.IO) { repository.fetch() }
updateUI(data) // Back on Main
}
WorkManager for Deferred Work:
class SyncWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
repository.syncData()
Result.success()
} catch (e: Exception) {
if (runAttemptCount < 3) {
Result.retry()
} else {
Result.failure()
}
}
}
}
// Schedule work
val syncWork = PeriodicWorkRequestBuilder<SyncWorker>(
15, TimeUnit.MINUTES
).setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresBatteryNotLow(true)
.build()
).build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"sync",
ExistingPeriodicWorkPolicy.KEEP,
syncWork
)
Battery Optimization
Batch Network Requests:
class BatchedUploader(context: Context) {
private val queue = mutableListOf<Event>()
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
fun addEvent(event: Event) {
queue.add(event)
scheduleUpload()
}
private fun scheduleUpload() {
scope.launch {
delay(5000) // Wait 5s to batch
val batch = queue.toList()
queue.clear()
if (batch.isNotEmpty()) {
uploadBatch(batch)
}
}
}
}
Avoid Wake Locks:
// Bad: Keeping device awake
wakeLock.acquire(10*60*1000L) // Don't do this
// Good: Use WorkManager or FCM high priority
val workRequest = OneTimeWorkRequestBuilder<UploadWorker>()
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
Examples (Input โ Output)
Optimizing RecyclerView
Input request: "My RecyclerView is laggy when scrolling, how do I optimize it?"
Output:
class OptimizedProductAdapter : ListAdapter<Product, ViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemProductBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getItem(position))
}
class ViewHolder(private val binding: ItemProductBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(product: Product) {
binding.name.text = product.name
// Use image loading library with caching
Glide.with(binding.image)
.load(product.imageUrl)
.placeholder(R.drawable.placeholder)
.centerCrop()
.into(binding.image)
}
}
class DiffCallback : DiffUtil.ItemCallback<Product>() {
override fun areItemsTheSame(old: Product, new: Product) =
old.id == new.id
override fun areContentsTheSame(old: Product, new: Product) =
old == new
}
}
// Fragment setup
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.recyclerView.apply {
setHasFixedSize(true) // Optimization if layout size is fixed
layoutManager = LinearLayoutManager(context)
adapter = productAdapter
// Enable view recycling
recycledViewPool.setMaxRecycledViews(0, 10)
// Use prefetch for smoother scrolling
(layoutManager as LinearLayoutManager).apply {
initialPrefetchItemCount = 4
}
}
// Submit list (diffutil handles updates efficiently)
viewModel.products.observe(viewLifecycleOwner) { list ->
productAdapter.submitList(list)
}
}
Finding Memory Leak
Input request: "App memory keeps growing, how do I find the leak?"
Output:
// Step 1: Check for static references
class BadExample {
companion object {
// Memory leak - static reference to Activity
var activityRef: MainActivity? = null
}
}
// Step 2: Fix with WeakReference or proper lifecycle
class GoodExample(lifecycle: Lifecycle) {
private val listeners = mutableListOf<WeakReference<Listener>>()
init {
lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
listeners.clear()
}
})
}
}
// Step 3: Use LeakCanary for detection
// In Application.onCreate()
if (BuildConfig.DEBUG) {
LeakCanary.install(this)
}
// Step 4: Common leak patterns to check
// - Anonymous inner classes referencing outer class
// - Handlers without WeakReference
// - Listeners not unregistered
// - RxJava/Coroutines not cancelled
class SafeHandler(
lifecycle: Lifecycle,
private val callback: () -> Unit
) : Handler(Looper.getMainLooper()) {
private val runnable = Runnable { callback() }
init {
lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
removeCallbacks(runnable)
}
})
}
fun postDelayed(delay: Long) {
postDelayed(runnable, delay)
}
}
Best Practices
- Profile before optimizing: Don't guess, measure with Profiler
- Use RecyclerView.ViewHolder: Proper recycling prevents object churn
- Image sizing: Load images at display size, not full resolution
- Avoid object allocation in onDraw: No
new Paint(),new Path()in draw loops - Lazy initialization: Initialize heavy objects only when needed
- Use ConstraintLayout: Reduces view hierarchy depth vs nested layouts
- Background work: Never block main thread, use coroutines/WorkManager
- Memory caches: Use LruCache for bitmaps and expensive objects
- Release resources: Cancel coroutines, unregister listeners, close cursors
- Test on low-end devices: Optimize for the worst case, not your flagship
Resources
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.