Android Performance Optimization: Complete Guide to Building Fast Apps
Learn essential techniques for optimizing Android app performance, including memory management, UI optimization, network efficiency, and battery life improvements.
Performance optimization is crucial for creating Android applications that provide a smooth user experience. Poor performance can lead to user frustration, negative reviews, and decreased engagement. This comprehensive guide will walk you through essential techniques for optimizing your Android applications.
Performance Metrics
Key Performance Indicators (KPIs)
- App Launch Time: Time from tap to first frame
- Frame Rate: Maintain 60 FPS for smooth animations
- Memory Usage: Efficient memory management
- Battery Consumption: Minimize battery drain
- Network Efficiency: Optimize data usage
- APK Size: Reduce app size for faster downloads
Performance Profiling Tools
- Android Profiler: Built-in profiling in Android Studio
- Systrace: System-level performance analysis
- Perfetto: Advanced tracing and analysis
- LeakCanary: Memory leak detection
- Firebase Performance: Real-world performance monitoring
Memory Management
Memory Leaks Prevention
class MainActivity : AppCompatActivity() {
// ❌ Bad: Potential memory leak
private val handler = Handler()
// ✅ Good: Use weak references
private val handler = Handler(Looper.getMainLooper())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// ❌ Bad: Anonymous inner class holds activity reference
handler.postDelayed(object : Runnable {
override fun run() {
updateUI() // This can cause memory leaks
}
}, 1000)
// ✅ Good: Use weak reference or clear handler
handler.postDelayed({
if (!isFinishing) {
updateUI()
}
}, 1000)
}
override fun onDestroy() {
super.onDestroy()
// ✅ Good: Remove callbacks to prevent memory leaks
handler.removeCallbacksAndMessages(null)
}
}
Efficient Data Structures
// ❌ Bad: Inefficient for large datasets
val userList = ArrayList<User>()
// ✅ Good: Use appropriate data structures
val userMap = HashMap<Int, User>() // For frequent lookups
val userSet = HashSet<User>() // For unique collections
// ✅ Good: Use LazyColumn for large lists
@Composable
fun UserList(users: List<User>) {
LazyColumn {
items(users) { user ->
UserItem(user = user)
}
}
}
Object Pooling
class ObjectPool<T>(
private val maxSize: Int,
private val factory: () -> T
) {
private val pool = ConcurrentLinkedQueue<T>()
fun acquire(): T {
return pool.poll() ?: factory()
}
fun release(obj: T) {
if (pool.size < maxSize) {
pool.offer(obj)
}
}
}
// Usage
val bitmapPool = ObjectPool(10) { Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888) }
UI Performance Optimization
View Optimization
// ❌ Bad: Nested layouts cause performance issues
<LinearLayout>
<LinearLayout>
<LinearLayout>
<TextView />
</LinearLayout>
</LinearLayout>
</LinearLayout>
// ✅ Good: Use ConstraintLayout for complex layouts
<androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/textView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
RecyclerView Optimization
class OptimizedAdapter : RecyclerView.Adapter<ViewHolder>() {
// ✅ Good: ViewHolder pattern
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val textView: TextView = itemView.findViewById(R.id.textView)
private val imageView: ImageView = itemView.findViewById(R.id.imageView)
fun bind(user: User) {
textView.text = user.name
// Use image loading library
Glide.with(itemView.context)
.load(user.avatar)
.into(imageView)
}
}
// ✅ Good: DiffUtil for efficient updates
private val diffCallback = object : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem == newItem
}
}
private val differ = AsyncListDiffer(this, diffCallback)
}
Compose Performance
// ✅ Good: Use remember and derivedStateOf
@Composable
fun OptimizedUserList(users: List<User>) {
val filteredUsers by remember(users) {
derivedStateOf {
users.filter { it.isActive }
}
}
LazyColumn {
items(filteredUsers) { user ->
UserItem(user = user)
}
}
}
// ✅ Good: Avoid expensive operations in composables
@Composable
fun ExpensiveComposable(data: List<String>) {
val processedData = remember(data) {
// Expensive processing
data.map { it.uppercase() }
}
LazyColumn {
items(processedData) { item ->
Text(text = item)
}
}
}
Network Optimization
Efficient API Calls
class NetworkOptimizer {
// ✅ Good: Use pagination
suspend fun getUsers(page: Int, limit: Int = 20): List<User> {
return apiService.getUsers(page = page, limit = limit)
}
// ✅ Good: Implement caching
suspend fun getUsersWithCache(): List<User> {
return try {
val users = apiService.getUsers()
userDao.insertUsers(users) // Cache
users
} catch (e: Exception) {
userDao.getAllUsers() // Fallback to cache
}
}
// ✅ Good: Use appropriate timeouts
private val okHttpClient = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
}
Image Loading Optimization
// ✅ Good: Efficient image loading
object ImageLoader {
fun loadImage(
imageView: ImageView,
url: String,
placeholder: Int = R.drawable.placeholder
) {
Glide.with(imageView.context)
.load(url)
.placeholder(placeholder)
.error(placeholder)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(imageView)
}
fun preloadImages(context: Context, urls: List<String>) {
urls.forEach { url ->
Glide.with(context)
.load(url)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.preload()
}
}
}
Battery Optimization
Background Processing
// ✅ Good: Use WorkManager for background tasks
class DataSyncWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
// Perform background work
syncData()
Result.success()
} catch (e: Exception) {
if (runAttemptCount < 3) {
Result.retry()
} else {
Result.failure()
}
}
}
}
// Schedule background work
val syncRequest = PeriodicWorkRequestBuilder<DataSyncWorker>(
15, TimeUnit.MINUTES
).setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
).build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"data_sync",
ExistingPeriodicWorkPolicy.KEEP,
syncRequest
)
Location Services Optimization
class LocationManager(private val context: Context) {
private val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
fun requestLocationUpdates() {
val locationRequest = LocationRequest.create().apply {
priority = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY
interval = 30000 // 30 seconds
fastestInterval = 10000 // 10 seconds
}
fusedLocationClient.requestLocationUpdates(
locationRequest,
locationCallback,
Looper.getMainLooper()
)
}
private val locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
locationResult.lastLocation?.let { location ->
// Handle location update
}
}
}
}
APK Size Optimization
Code Optimization
// ✅ Good: Enable R8/ProGuard
android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
// ProGuard rules
-keep class com.example.model.** { *; }
-keepclassmembers class com.example.model.** {
<fields>;
}
Resource Optimization
// ✅ Good: Use vector drawables
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2z"/>
</vector>
// ✅ Good: Use WebP images
// Convert PNG/JPG to WebP for smaller file sizes
Database Optimization
Room Database Optimization
@Database(
entities = [User::class, Post::class],
version = 1,
exportSchema = false
)
abstract class OptimizedDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun postDao(): PostDao
companion object {
@Volatile
private var INSTANCE: OptimizedDatabase? = null
fun getDatabase(context: Context): OptimizedDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
OptimizedDatabase::class.java,
"optimized_database"
)
.setQueryCallback({ sqlQuery, bindArgs ->
// Log slow queries
if (sqlQuery.contains("SELECT") && sqlQuery.length > 100) {
Log.d("Database", "Slow query: $sqlQuery")
}
}, Executors.newSingleThreadExecutor())
.build()
INSTANCE = instance
instance
}
}
}
}
Efficient Queries
@Dao
interface OptimizedUserDao {
// ✅ Good: Use indices for frequently queried columns
@Query("SELECT * FROM users WHERE email = :email LIMIT 1")
suspend fun getUserByEmail(email: String): User?
// ✅ Good: Use pagination for large datasets
@Query("SELECT * FROM users LIMIT :limit OFFSET :offset")
suspend fun getUsersPaginated(limit: Int, offset: Int): List<User>
// ✅ Good: Use transactions for multiple operations
@Transaction
suspend fun insertUserWithPosts(user: User, posts: List<Post>) {
insertUser(user)
insertPosts(posts)
}
}
Startup Optimization
Application Startup
@HiltAndroidApp
class OptimizedApplication : Application() {
override fun onCreate() {
super.onCreate()
// ✅ Good: Initialize heavy components lazily
initializeComponents()
}
private fun initializeComponents() {
// Use background thread for heavy initialization
CoroutineScope(Dispatchers.IO).launch {
// Initialize database
// Initialize network components
// Initialize analytics
}
}
}
Activity Startup
class OptimizedActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// ✅ Good: Enable hardware acceleration
window.setFlags(
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// ✅ Good: Defer non-critical initialization
lifecycleScope.launch {
initializeNonCriticalComponents()
}
}
private suspend fun initializeNonCriticalComponents() {
withContext(Dispatchers.IO) {
// Initialize analytics
// Load cached data
// Preload images
}
}
}
Memory Profiling
Memory Leak Detection
// ✅ Good: Use LeakCanary for memory leak detection
dependencies {
debugImplementation "com.squareup.leakcanary:leakcanary-android:2.12"
}
// Custom memory leak detection
class MemoryLeakDetector {
private val weakReferences = mutableListOf<WeakReference<Any>>()
fun trackObject(obj: Any) {
weakReferences.add(WeakReference(obj))
}
fun checkForLeaks() {
val leakedObjects = weakReferences.filter { it.get() != null }
if (leakedObjects.isNotEmpty()) {
Log.w("MemoryLeak", "Potential memory leaks detected: ${leakedObjects.size}")
}
}
}
Performance Monitoring
Custom Performance Tracking
class PerformanceTracker {
private val metrics = mutableMapOf<String, Long>()
fun startTimer(tag: String) {
metrics[tag] = System.currentTimeMillis()
}
fun endTimer(tag: String) {
val startTime = metrics[tag]
if (startTime != null) {
val duration = System.currentTimeMillis() - startTime
Log.d("Performance", "$tag took ${duration}ms")
metrics.remove(tag)
}
}
fun trackMemoryUsage() {
val runtime = Runtime.getRuntime()
val usedMemory = runtime.totalMemory() - runtime.freeMemory()
val maxMemory = runtime.maxMemory()
val memoryUsage = (usedMemory.toFloat() / maxMemory.toFloat()) * 100
Log.d("Performance", "Memory usage: ${memoryUsage}%")
}
}
Best Practices
1. UI Thread Optimization
- Keep UI thread free for rendering
- Use background threads for heavy operations
- Avoid blocking operations on main thread
2. Memory Management
- Use appropriate data structures
- Implement object pooling for frequently created objects
- Avoid memory leaks with proper lifecycle management
3. Network Optimization
- Implement caching strategies
- Use pagination for large datasets
- Optimize image loading and caching
4. Battery Optimization
- Use WorkManager for background tasks
- Implement efficient location services
- Minimize wake locks and background processing
5. APK Size Optimization
- Enable code shrinking and obfuscation
- Use vector drawables and WebP images
- Remove unused resources and dependencies
6. Database Optimization
- Use appropriate indices
- Implement efficient queries
- Use transactions for multiple operations
7. Startup Optimization
- Defer non-critical initialization
- Use lazy loading for heavy components
- Enable hardware acceleration
Performance Testing
Benchmark Testing
@RunWith(AndroidJUnit4::class)
class PerformanceTest {
@Test
fun testAppLaunchTime() {
val startTime = System.currentTimeMillis()
// Launch app
val scenario = ActivityScenario.launch(MainActivity::class.java)
scenario.onActivity { activity ->
// Wait for app to be fully loaded
Thread.sleep(1000)
}
val endTime = System.currentTimeMillis()
val launchTime = endTime - startTime
// Assert launch time is acceptable
assertThat(launchTime).isLessThan(3000) // 3 seconds
}
@Test
fun testMemoryUsage() {
val runtime = Runtime.getRuntime()
val initialMemory = runtime.totalMemory() - runtime.freeMemory()
// Perform memory-intensive operation
repeat(1000) {
// Create objects
}
val finalMemory = runtime.totalMemory() - runtime.freeMemory()
val memoryIncrease = finalMemory - initialMemory
// Assert memory increase is reasonable
assertThat(memoryIncrease).isLessThan(50 * 1024 * 1024) // 50MB
}
}
Conclusion
Performance optimization is an ongoing process that requires continuous monitoring and improvement. By implementing these techniques, you can create Android applications that:
- Launch quickly and respond to user interactions
- Use memory efficiently without leaks
- Conserve battery through optimized background processing
- Load data efficiently with proper caching and pagination
- Maintain smooth animations with optimized UI rendering
Remember to:
- Profile your app regularly using Android Profiler
- Monitor real-world performance with Firebase Performance
- Test on low-end devices to ensure broad compatibility
- Optimize incrementally based on user feedback and metrics
- Balance performance with code maintainability
By following these optimization strategies, you can build fast, efficient Android applications that provide an excellent user experience across all devices.