Refactor high-complexity React components in Dify frontend. Use when `pnpm analyze-component...
npx skills add HuxleyMc/Android-Skills --skill "android-performance-profiler"
Install specific skill from multi-skill repository
# Description
Analyzes and optimizes Android app performance. Use when identifying UI jank, memory leaks, slow startup, high battery drain, or Compose recomposition issues. Covers profiling tools, benchmarks, and optimization techniques.
# SKILL.md
name: android-performance-profiler
description: Analyzes and optimizes Android app performance. Use when identifying UI jank, memory leaks, slow startup, high battery drain, or Compose recomposition issues. Covers profiling tools, benchmarks, and optimization techniques.
tags: ["android", "performance", "profiling", "benchmark", "compose", "memory", "optimization"]
difficulty: advanced
category: performance
version: "1.0.0"
last_updated: "2025-01-29"
Android Performance Profiler
Quick Start
Identify performance issue type:
1. UI Jank / Frame Drops
2. Memory Leaks
3. Slow Startup
4. High Battery Drain
5. Database Slowness
6. Compose Recomposition
7. Network Performance
Profiling Tools
Android Studio Profiler
// CPU Profiling - identify hotspots
// 1. Build with debuggable true
// 2. Run app with profiler attached
// 3. Record CPU trace during slow operation
// 4. Analyze call stack for long-running methods
// Memory Profiling
// 1. Use Memory Profiler to track allocations
// 2. Look for memory spikes
// 3. Capture heap dump for analysis
// 4. Identify retained objects
Macrobenchmark
// build.gradle
androidTestImplementation("androidx.benchmark:benchmark-macro-junit4:1.2.0")
// Startup benchmark
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()
}
}
// Frame metrics benchmark
class FrameBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun scrollList() = benchmarkRule.measureRepeated(
packageName = "com.example.app",
metrics = listOf(FrameTimingMetric(), JankMetric()),
iterations = 5
) {
val recycler = device.findObject(By.res("recycler"))
recycler.setGestureMargin(device.displayWidth / 5)
recycler.fling(Direction.DOWN, 5000)
}
}
Compose Metrics
// Enable in build.gradle
android {
buildTypes {
debug {
composeOptions {
metricsDestination = layout.buildDirectory.dir("compose_metrics")
}
}
}
}
// Runtime tracking
class CompositionLogger : CompositionObserver {
override fun onComposition(content: @Composable () -> Unit) {
val start = System.nanoTime()
content()
val duration = (System.nanoTime() - start) / 1_000_000
if (duration > 16) {
Log.w("Composition", "Slow composition: ${duration}ms")
}
}
}
Performance Patterns
UI Jank Prevention
// Bad: Heavy work on main thread
@Composable
fun BadList() {
val items = remember {
heavyComputation() // ❌ Blocks composition
}
}
// Good: Offload to background
@Composable
fun GoodList() {
val items by produceState<List<Item>>(initialValue = emptyList()) {
value = withContext(Dispatchers.Default) {
heavyComputation()
}
}
}
// Good: Lazy evaluation
LazyColumn {
items(data) { item ->
ListItem(item)
}
}
Compose Recomposition Optimization
// Bad: Unstable parameter causes recomposition
@Composable
fun UserCard(user: User) { // User is unstable
Column {
Text(user.name)
Text(user.email)
}
}
// Good: Make data class immutable and stable
@Immutable
data class User(
val id: String,
val name: String,
val email: String
)
// Good: Use stable keys
LazyColumn {
items(
items = users,
key = { it.id }
) { user ->
UserCard(user = user)
}
}
// Good: Remember expensive calculations
@Composable
fun ExpensiveCalculation(data: List<Int>) {
val sum = remember(data) {
data.sum()
}
Text("Sum: $sum")
}
// Good: Derive state to minimize recompositions
@Composable
fun SearchResults(query: String, items: List<Item>) {
val filteredItems by remember(query, items) {
derivedStateOf {
items.filter { it.name.contains(query, ignoreCase = true) }
}
}
LazyColumn {
items(filteredItems) { item ->
ListItem(item)
}
}
}
Memory Leak Prevention
// Bad: Holding Activity reference in ViewModel
class BadViewModel : ViewModel() {
var activity: MainActivity? = null // ❌ Memory leak
}
// Good: Use Application context
class GoodRepository(application: Application) {
private val context = application.applicationContext
}
// Bad: Not cleaning up callbacks
class BadManager {
fun register(listener: Listener) {
listeners.add(listener) // Never removed!
}
}
// Good: Proper cleanup with lifecycle awareness
class GoodManager {
fun register(owner: LifecycleOwner, listener: Listener) {
owner.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
listeners.remove(listener)
}
})
listeners.add(listener)
}
}
// Good: Use WeakReference when necessary
class WeakCallback<T>(callback: Callback<T>) {
private val weakRef = WeakReference(callback)
fun invoke(value: T) {
weakRef.get()?.onResult(value)
}
}
Startup Optimization
// Bad: Heavy initialization in Application.onCreate
class BadApp : Application() {
override fun onCreate() {
super.onCreate()
initDatabase() // ❌ Blocks startup
initAnalytics() // ❌ Blocks startup
initCrashReporter() // ❌ Blocks startup
}
}
// Good: Lazy initialization
class GoodApp : Application() {
val database: AppDatabase by lazy {
Room.databaseBuilder(this, AppDatabase::class.java, "db").build()
}
override fun onCreate() {
super.onCreate()
// Only critical initialization here
// Defer non-critical init
GlobalScope.launch(Dispatchers.Default) {
initAnalytics()
initCrashReporter()
}
}
}
// Good: ContentProvider for early init
class AnalyticsInitializer : Initializer<Analytics> {
override fun create(context: Context): Analytics {
return Analytics.initialize(context)
}
override fun dependencies(): List<Class<out Initializer<*>>> {
return emptyList()
}
}
// AndroidManifest.xml
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="com.example.AnalyticsInitializer"
android:value="androidx.startup" />
</provider>
Database Optimization
// Bad: Query in loop
fun getUsersWithOrders(userIds: List<String>): List<UserWithOrders> {
return userIds.map { id ->
val user = db.userDao().getUser(id) // ❌ N+1 query
val orders = db.orderDao().getOrdersForUser(id)
UserWithOrders(user, orders)
}
}
// Good: Single query with JOIN
@Query("""
SELECT * FROM users
INNER JOIN orders ON users.id = orders.userId
WHERE users.id IN (:userIds)
""
)
suspend fun getUsersWithOrders(userIds: List<String>): Map<User, List<Order>>
// Bad: Loading all data
@Query("SELECT * FROM logs")
suspend fun getAllLogs(): List<Log>
// Good: Pagination
@Query("SELECT * FROM logs ORDER BY timestamp DESC LIMIT :limit OFFSET :offset")
suspend fun getLogsPaged(limit: Int, offset: Int): List<Log>
// Bad: No index on queried columns
@Entity(tableName = "users")
data class User(
@PrimaryKey val id: String,
val email: String // Queried frequently
)
// Good: Add index
@Entity(
tableName = "users",
indices = [Index(value = ["email"])]
)
data class User(
@PrimaryKey val id: String,
val email: String
)
Battery Optimization
// Bad: Frequent polling
class BadLocationTracker {
fun startTracking() {
handler.postDelayed({
fetchLocation()
startTracking() // ❌ Continuous polling
}, 5000)
}
}
// Good: Use WorkManager for background work
class LocationWorker(context: Context, params: WorkerParameters) :
CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
fetchLocation()
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
companion object {
fun schedule() {
val request = PeriodicWorkRequestBuilder<LocationWorker>(
15, TimeUnit.MINUTES
).setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
).build()
WorkManager.getInstance().enqueueUniquePeriodicWork(
"location_work",
ExistingPeriodicWorkPolicy.KEEP,
request
)
}
}
}
// Good: Batch network requests
class RequestBatcher<T>(
private val scope: CoroutineScope,
private val timeout: Long = 100,
private val processor: suspend (List<T>) -> Unit
) {
private val channel = Channel<T>(Channel.UNLIMITED)
init {
scope.launch {
val batch = mutableListOf<T>()
whileSelect {
channel.onReceive {
batch.add(it)
batch.size < 100
}
onTimeout(timeout) { false }
}
if (batch.isNotEmpty()) {
processor(batch)
}
}
}
fun add(item: T) {
channel.trySend(item)
}
}
Examples (Input → Output)
Analyze Compose Recomposition
Input: "This screen has jank during scrolling"
@Composable
fun ProductList(products: List<Product>) {
LazyColumn {
items(products) { product ->
ProductCard(product = product)
}
}
}
@Composable
fun ProductCard(product: Product) {
Card {
Column {
AsyncImage(model = product.imageUrl)
Text(product.name)
Text("$${product.price}")
RatingBar(rating = calculateRating(product.reviews))
}
}
}
Output:
// Issues identified:
// 1. Product is likely unstable (mutable or non-@Immutable)
// 2. calculateRating called during composition
// 3. No keys for LazyColumn items
// Optimized version:
@Immutable
data class Product(
val id: String,
val name: String,
val price: Double,
val imageUrl: String,
val rating: Float // Pre-calculated
)
@Composable
fun ProductList(products: List<Product>) {
LazyColumn {
items(
items = products,
key = { it.id }
) { product ->
ProductCard(product = product)
}
}
}
@Composable
fun ProductCard(product: Product) {
Card {
Column {
AsyncImage(
model = product.imageUrl,
contentDescription = null
)
Text(product.name)
Text("$${product.price}")
RatingBar(rating = product.rating)
}
}
}
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.