Pular para o conteúdo principal

Mind Group

Como otimizar memória e código DEX no Android para cumprir os requisitos do Google Play 2027

A partir de fevereiro de 2027, o Google Play exigirá que todos os apps e jogos cumpram limites rígidos de consumo de memória e otimização de código DEX. Apps que não se adequarem terão visibilidade reduzida e restrições de publicação na Play Store. Este tutorial técnico mostra, passo a passo, como implementar as otimizações necessárias — desde o profiling de memória até a configuração avançada do R8.

O conteúdo cobre três frentes: diagnóstico e redução do consumo de memória dinâmica (Anonymous RSS + Swap), otimização de bitmaps seguindo os princípios de reduce-reuse-recycle, e configuração completa do R8 para atingir os 25% mínimos de otimização de código DEX.

Entendendo os limites de memória do Google Play 2027

Antes de otimizar, é fundamental entender o que o Google vai medir. O Play Console monitora o consumo de memória em quatro estados de processo:

Foreground: app visível e interativo. Em um dispositivo com 8 GB de RAM, o limite é de aproximadamente 2,25 GB. User-perceived services: foreground services, expedited jobs e data transfer jobs — consumo deve ser significativamente menor que foreground. Background: app em segundo plano — deve liberar caches e recursos pesados. Cached: app em cache, pronto para ser encerrado pelo LMKD — consumo deve ser mínimo.

O Google avalia o percentil 90 (P90) dos seus usuários. Isso significa que 90% dos seus usuários devem estar abaixo do limite. Se a proporção P99/P50 for superior a 3,5x, provavelmente há um memory leak a investigar.

Passo 1: Diagnosticar o consumo atual de memória

Usando o Android Vitals no Play Console

O primeiro passo é entender a situação atual. Acesse o Play Console, navegue até Android Vitals > Memory Usage e analise os gráficos de consumo por estado de processo. Identifique se o consumo está próximo dos limites e se há tendência de crescimento.

Preste atenção especial ao campo Bitmap Memory Usage, que mostra o consumo específico de bitmaps separado do consumo geral. Bitmaps são frequentemente o maior componente individual do consumo de memória.

Profiling local com Android Studio Memory Profiler

Para diagnóstico detalhado, use o Memory Profiler do Android Studio. Conecte o dispositivo de teste, inicie o profiler e execute os fluxos principais do app (telas com muitas imagens, navegação entre activities, rotação de tela).

Capture um heap dump para analisar a composição da memória. No heap dump, use o filtro Duplicate Bitmaps para identificar imagens carregadas mais de uma vez na memória — um dos problemas mais comuns e mais fáceis de corrigir.

// No Android Studio:
// 1. Abra o Profiler (View > Tool Windows > Profiler)
// 2. Selecione a sessão do app
// 3. Clique em "Capture heap dump"
// 4. No resultado, filtre por "Duplicate Bitmaps"
// 5. Clique em cada entrada para ver o Bitmap Preview
// 6. Identifique qual código está carregando a mesma imagem duas vezes

Detecção automática de leaks com LeakCanary

O LeakCanary detecta automaticamente objetos que deveriam ter sido coletados pelo garbage collector mas ainda estão retidos na memória. A instalação é simples — uma linha no build.gradle.kts:

// build.gradle.kts (módulo app)
dependencies {
    // Apenas no build de debug — não incluir em release
    debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
}

// Nenhuma configuração adicional necessária.
// O LeakCanary se inicializa automaticamente via ContentProvider.
// Quando detectar um leak, exibirá uma notificação com o leak trace completo.

A partir do Android Studio Panda, o Profiler também oferece integração nativa com o LeakCanary como uma tarefa dedicada, unificando detecção de leaks e análise de heap dumps em uma interface única.

Passo 2: Otimizar o consumo de memória de bitmaps

O Google define três princípios para otimização de bitmaps: Reduce (reduzir o footprint inicial), Reuse (reutilizar via cache) e Recycle (liberar recursos proativamente). Vamos implementar cada um.

Reduce: usar biblioteca de carregamento de imagens

Se o seu app carrega imagens manualmente com BitmapFactory.decodeResource() ou BitmapFactory.decodeStream(), você está desperdiçando memória. Bibliotecas como Coil (Kotlin-first) e Glide (Java/Kotlin) fazem downsampling automático, cache em memória e disco, e reciclagem de bitmaps.

// ===== OPÇÃO 1: Coil (recomendado para projetos Kotlin/Compose) =====

// build.gradle.kts
dependencies {
    implementation("io.coil-kt:coil-compose:2.7.0")
}

// No Composable:
import coil.compose.AsyncImage
import coil.request.ImageRequest
import coil.size.Scale

@Composable
fun ProductImage(imageUrl: String) {
    AsyncImage(
        model = ImageRequest.Builder(LocalContext.current)
            .data(imageUrl)
            .crossfade(true)
            .scale(Scale.FIT)
            .memoryCachePolicy(CachePolicy.ENABLED)
            .diskCachePolicy(CachePolicy.ENABLED)
            .build(),
        contentDescription = "Imagem do produto",
        modifier = Modifier.size(200.dp),
        contentScale = ContentScale.Crop
    )
}

// ===== OPÇÃO 2: Glide (projetos Java ou View-based) =====

// build.gradle.kts
dependencies {
    implementation("com.github.bumptech.glide:glide:4.16.0")
    ksp("com.github.bumptech.glide:ksp:4.16.0")
}

// No código:
Glide.with(context)
    .load(imageUrl)
    .override(Target.SIZE_ORIGINAL)
    .format(DecodeFormat.PREFER_RGB_565)
    .diskCacheStrategy(DiskCacheStrategy.ALL)
    .into(imageView)

Reduce: usar RGB_565 para imagens sem transparência

O formato padrão ARGB_8888 usa 4 bytes por pixel. Para imagens que não precisam de canal alpha (transparência), use RGB_565 que usa apenas 2 bytes por pixel — uma redução de 50% no consumo de memória.

// Decodificação manual com RGB_565
val options = BitmapFactory.Options().apply {
    inPreferredConfig = Bitmap.Config.RGB_565
    inJustDecodeBounds = true
}

BitmapFactory.decodeResource(resources, R.drawable.background, options)

val reqWidth = imageView.width
val reqHeight = imageView.height
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)

options.inJustDecodeBounds = false
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.background, options)

fun calculateInSampleSize(
    options: BitmapFactory.Options,
    reqWidth: Int,
    reqHeight: Int
): Int {
    val (height, width) = options.outHeight to options.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
}

Reduce: preferir VectorDrawables para ícones

Ícones e gráficos simples devem usar VectorDrawable em vez de PNGs. Vetores escalam sem perda de qualidade e ocupam significativamente menos memória. No Compose, use Icon com painter = painterResource(R.drawable.ic_icon).

Reuse: configurar cache de bitmaps adequadamente

val imageLoader = ImageLoader.Builder(context)
    .memoryCache {
        MemoryCache.Builder(context)
            .maxSizePercent(0.25)
            .build()
    }
    .diskCache {
        DiskCache.Builder()
            .directory(context.cacheDir.resolve("image_cache"))
            .maxSizePercent(0.02)
            .build()
    }
    .build()

class MyApplication : Application(), ImageLoaderFactory {
    override fun newImageLoader(): ImageLoader = imageLoader
}

Recycle: liberar recursos quando o app vai para background

Implemente os callbacks de onTrimMemory() para liberar caches de imagens quando o app não está visível. Isso é crítico para cumprir os limites de background e cached do Google Play.

class MyApplication : Application() {
    override fun onTrimMemory(level: Int) {
        super.onTrimMemory(level)
        when (level) {
            ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
                imageLoader.memoryCache?.clear()
            }
            ComponentCallbacks2.TRIM_MEMORY_BACKGROUND,
            ComponentCallbacks2.TRIM_MEMORY_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> {
                imageLoader.memoryCache?.clear()
                clearHeavyResources()
            }
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
                imageLoader.memoryCache?.clear()
                clearHeavyResources()
                System.gc()
            }
        }
    }
    private fun clearHeavyResources() { /* Limpar caches */ }
}

Passo 3: Otimizar código DEX com R8

O R8 é o otimizador padrão integrado ao Android Gradle Plugin. Ele combina tree shaking, code optimization, obfuscation e resource shrinking para reduzir o tamanho do código DEX e melhorar a performance.

Habilitar R8 no build.gradle.kts

// build.gradle.kts (módulo app)
android {
    buildTypes {
        release {
            isMinifyEnabled = true
            isShrinkResources = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
}

Atenção: use proguard-android-optimize.txt e não proguard-android.txt. A versão otimizada habilita otimizações adicionais como method inlining e class merging que contribuem significativamente para atingir os 25% exigidos.

Configurar regras do ProGuard/R8

O arquivo proguard-rules.pro define o que o R8 deve preservar. A estratégia é manter o mínimo necessário para que o app funcione corretamente em runtime:

# proguard-rules.pro

# ===== REGRAS ESSENCIAIS =====
-keepclassmembers class * {
    @com.google.gson.annotations.SerializedName ;
}

-keepattributes *Annotation*
-keep class kotlinx.serialization.** { *; }

-keepclasseswithmembernames class * {
    native ;
}

-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

# ===== REGRAS PARA BIBLIOTECAS COMUNS =====
-keepattributes Signature
-keepattributes Exceptions
-keep class retrofit2.** { *; }
-keepclasseswithmembers class * {
    @retrofit2.http.* ;
}

-keep class * extends androidx.room.RoomDatabase
-keep @androidx.room.Entity class *

# ===== DIAGNÓSTICO =====
-printmapping mapping.txt
-printusage usage.txt

Habilitar o resource shrinking otimizado (AGP 9.0+)

A partir do Android Gradle Plugin 9.0, o resource shrinking otimizado se torna o comportamento padrão. Ele vai além do shrinking tradicional: remove recursos E o código DEX que os referencia, cruzando a fronteira entre DEX e resources.

Verificar os resultados da otimização

Após habilitar o R8, verifique os resultados para garantir que está atingindo os 25% mínimos:

// No terminal, após o build de release:
// 1. Verificar tamanho do APK/AAB antes e depois
// ./gradlew assembleRelease

// 2. Usar o APK Analyzer do Android Studio
// Build > Analyze APK... Compare DEX size, resource size, etc.

// 3. Verificar o Play Console
// No Play Console > App size > veja a métrica de download size

// 4. Usar o R8 Configuration Analyzer (AGP 9.0+)
// ./gradlew :app:analyzeReleaseR8Configuration

Usar o R8 Configuration Analyzer

O R8 Configuration Analyzer, introduzido em 2026, analisa as regras de keep do projeto e sugere otimizações. O Tinder usou essa ferramenta para identificar que regras excessivamente amplas estavam impedindo o R8 de otimizar classes inteiras.

Passo 4: Implementar Baseline Profiles para performance adicional

Embora não seja um requisito obrigatório do Google Play 2027, Baseline Profiles complementam as otimizações de memória e R8 ao pré-compilar os caminhos críticos do app durante a instalação, reduzindo o uso de memória em runtime:

// build.gradle.kts
plugins {
    id("androidx.baselineprofile")
}
dependencies {
    baselineProfile(project(":baselineprofile"))
}

@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
    @get:Rule
    val rule = BaselineProfileRule()

    @Test
    fun generateBaselineProfile() {
        rule.collect(
            packageName = "com.seuapp.package",
            stableIterations = 3,
            maxIterations = 10
        ) {
            pressHome()
            startActivityAndWait()
            device.findObject(By.text("Produtos")).click()
            device.waitForIdle()
        }
    }
}

Passo 5: Monitorar e validar continuamente

Otimizar uma vez não é suficiente. Configure monitoramento contínuo para garantir que o consumo não volte a crescer:

Verificação automatizada no CI/CD

#!/bin/bash
# Verifica se o tamanho do APK não cresceu mais que 5%
PREVIOUS_SIZE=$(cat .baseline-apk-size 2>/dev/null || echo "0")
CURRENT_SIZE=$(stat -f%z app/build/outputs/apk/release/app-release.apk)
if [ "$PREVIOUS_SIZE" -gt "0" ]; then
    GROWTH=$(( (CURRENT_SIZE - PREVIOUS_SIZE) * 100 / PREVIOUS_SIZE ))
    if [ "$GROWTH" -gt "5" ]; then
        echo "ALERTA: APK cresceu anterior por mais de 5%"
        exit 1
    fi
fi
echo "$CURRENT_SIZE" > .baseline-apk-size

Monitoramento via Android Vitals

Configure alertas no Play Console para ser notificado quando métricas de memória ultrapassarem thresholds definidos. Revise o dashboard de Android Vitals semanalmente, comparando o consumo de memória entre versões do app.

Como a Mind Group implementa essas otimizações

Na Mind Group, incorporamos otimização de performance como parte do ciclo de desenvolvimento, não como uma ação emergencial de última hora. Nossa equipe em São Paulo e Sorocaba aplica essas técnicas desde o início de cada projeto Android.

Em projetos recentes, alcançamos reduções de 35-50% no consumo de memória de bitmaps ao migrar para Coil com cache configurado por perfil de dispositivo. A configuração avançada de R8 com regras customizadas por módulo nos permite atingir consistentemente mais de 30% de otimização DEX — bem acima dos 25% mínimos exigidos.

Se o seu app precisa de otimização para cumprir os prazos do Google Play 2027, fale com a Mind Group. Podemos realizar uma auditoria técnica completa e implementar as otimizações necessárias.

Perguntas frequentes sobre otimização de memória e R8 no Android

Quanto de redução de memória posso esperar ao usar Coil ou Glide?

A redução varia conforme o app, mas em apps com muitas imagens, a combinação de downsampling automático, cache eficiente e reciclagem de bitmaps pode reduzir o consumo de memória de imagens em 40-60%. O uso de RGB_565 adiciona 50% de redução sobre o ARGB_8888 para imagens sem transparência.

O R8 pode quebrar meu app em produção?

Sim, se as regras de keep não estiverem configuradas corretamente. Classes acessadas via reflection (JSON parsing, Room entities, JNI) devem ser explicitamente preservadas. Sempre teste o build de release extensivamente antes de publicar. Use o mapping.txt para desobfuscar crash reports.

Qual a diferença entre isMinifyEnabled e isShrinkResources?

O isMinifyEnabled ativa o R8 para otimizar, ofuscar e reduzir o código DEX. O isShrinkResources remove recursos (imagens, layouts, strings) que não são referenciados no código. Ambos devem ser habilitados juntos para máxima redução — o shrinking de recursos depende do R8 estar ativo.

Como sei se meu app já atinge os 25% de otimização exigidos?

No Play Console, acesse Android Vitals e verifique a métrica de DEX Code Optimization. O Console mostra a porcentagem de otimização, ofuscação e shrinking do seu código DEX. Se o valor estiver abaixo de 25%, habilite o R8 com as configurações descritas neste tutorial.

O LeakCanary afeta a performance do app em produção?

Não, se configurado corretamente. Use debugImplementation (não implementation) para garantir que o LeakCanary só seja incluído no build de debug. Em release, a dependência é completamente removida e não há impacto algum na performance.

Preciso implementar onTrimMemory mesmo usando Coil/Glide?

Sim. Embora essas bibliotecas gerenciem cache interno, o callback onTrimMemory() permite que você instrua explicitamente a biblioteca a liberar seu cache quando o sistema está com pouca memória. Sem isso, o cache pode persistir quando o app está em background, consumindo memória desnecessariamente.

Escrito por José Gonçalves

CEO e fundador da Mind Group (fundada em 2016), software house brasileira sediada em Sorocaba/SP. Lidera o desenvolvimento de sistemas, aplicativos, IA e automações para clientes como Itaipu Binacional, Fisk, Lojas Torra, Febracis e Vertuz. Especialista em arquitetura de software, squads ágeis e integração de Inteligência Artificial em operações B2B.

LinkedIn →
WhatsApp Especialista
Falar com especialista