Configure aplicativos com mais de 64 K métodos

A plataforma Android continua crescendo, e o tamanho dos aplicativos para Android também. Quando um aplicativo e as bibliotecas às quais ele faz referência alcançam determinado tamanho, ocorrem erros de compilação que indicam que o aplicativo chegou ao limite da arquitetura de compilação de aplicativos Android. Versões anteriores do sistema de compilação reportam esse erro da seguinte forma:

Conversion to Dalvik format failed:
Unable to execute dex: method ID not in [0, 0xffff]: 65536

Versões mais recentes do sistema de compilação do Android exibem um erro diferente que indica o mesmo problema:

trouble writing output:
Too many field references: 131000; max is 65536.
You may try using --multi-dex option.

Ambas as condições de erro exibem um número em comum: 65.536. Esse número é importante, pois representa o total de referências que podem ser invocadas pelo código em um só arquivo de bytecode Dalvik Executable (DEX). Esta página explica como superar esse limite, ativando uma configuração de aplicativos denominada multidex, que permite que os aplicativos compilem e leiam vários arquivos DEX.

Sobre o limite de 64 K referências

Arquivos de aplicativo Android (APK) contêm arquivos de bytecode executáveis no formato de arquivos Dalvik Executable (DEX). Esses arquivos DEX são compostos pelo código compilado usado para executar o aplicativo. A especificação do formato Dalvik Executable limita o total de métodos que podem ser referenciados em um só arquivo DEX a 65.536 — inclusive os métodos de estrutura do Android, os métodos de biblioteca e os métodos do seu próprio código. No contexto da ciência da computação, o termo Kilo, K, denota 1.024 (ou 2^10). Como 65.536 equivale a 64 X 1.024, esse limite é chamado de “limite de 64 K referências”.

Compatibilidade com multidex antes do Android 5.0

Versões da plataforma anteriores ao Android 5.0 (nível de API 21) usam o tempo de execução da Dalvik para executar o código do aplicativo. Por padrão, a Dalvik limita os aplicativos a um único arquivo de bytecode classes.dex por APK. Para superar essa limitação, você pode usar a biblioteca de suporte a multidex, que se torna parte do arquivo DEX principal do seu aplicativo e gerencia o acesso aos arquivos DEX adicionais e ao código que eles contêm.

Observação: se o seu projeto estiver configurado para multidex com minSdkVersion 20 ou inferior e você implantá-lo em dispositivos de destino com o Android 4.4 (API de nível 20) ou uma versão anterior, o Android Studio desativará o Instant Run.

Compatibilidade com multidex para Android 5.0 e versões posteriores

O Android 5.0 (API de nível 21) e versões posteriores usam um tempo de execução chamado ART, que oferece suporte nativo para o carregamento de vários arquivos DEX de arquivos APK. O ART executa a pré-compilação no momento da instalação do aplicativo, que verifica a presença de arquivos classesN.dex e os compila em um único arquivo .oat para execução pelo dispositivo Android. Portanto, se o minSdkVersion for 21 ou posterior, a biblioteca de suporte multidex não será necessária.

Para obter mais informações sobre o tempo de execução do Android 5.0, leia ART e Dalvik.

Observação: ao usar o Instant Run, o Android Studio configurará automaticamente o aplicativo para multidex se minSdkVersion for definido como 21 ou posterior. Como o Instant Run só funciona com a versão de depuração do seu aplicativo, ainda será necessário configurar a compilação de lançamento para multidex para evitar o limite de 64 K.

Evitar o limite de 64 K

Antes de configurar seu aplicativo para permitir o uso de 64 K ou mais referências a métodos, tome providências para reduzir o total de referências chamadas pelo código do seu aplicativo, incluindo métodos definidos pelo código do aplicativo ou pelas bibliotecas incluídas. As seguintes estratégias podem ajudar a não atingir o limite de referências do DEX:

  • Verifique as dependências diretas e transitivas do aplicativo – garanta que todas as dependências de bibliotecas grandes incluídas no aplicativo sejam usadas de forma a compensar a quantidade de código adicionada ao aplicativo. Um antipadrão comum é incluir uma biblioteca muito grande porque alguns métodos utilitários foram úteis. Muitas vezes, a redução das dependências de código do aplicativo pode ajudar a evitar o limite de referências do DEX.
  • Remova o código não utilizado com o ProGuardative a redução de código para executar o ProGuard nas compilações das versões. Ativar a redução garante que você não esteja incluindo códigos não utilizados em seus APKs.

O uso dessas técnicas pode ajudar a necessidade de ativar multidex no aplicativo, além de diminuir o tamanho geral do APK.

Configurar o aplicativo para multidex

Para definir o uso de uma configuração multidex no aplicativo, é necessário fazer as seguintes modificações no projeto do aplicativo, dependendo da versão mínima de Android compatível com o aplicativo.

Se minSdkVersion for definido como 21 ou posterior, bastará definir multiDexEnabled como true no arquivo build.gradle do módulo, como mostrado aqui:

android {
    defaultConfig {
        ...
        minSdkVersion 21 
        targetSdkVersion 26
        multiDexEnabled true
    }
    ...
}

No entanto, se minSdkVersion for definido como 20 ou inferior, será necessário usar a biblioteca de suporte do multidex da seguinte maneira:

  • Modifique o arquivo build.gradle do módulo para ativar o multidex e adicionar a biblioteca multidex como dependência, como mostrado a seguir:

    android {
        defaultConfig {
            ...
            minSdkVersion 15 
            targetSdkVersion 26
            multiDexEnabled true
        }
        ...
    }
    
    dependencies {
      compile 'com.android.support:multidex:1.0.3'
    }
    
  • Dependendo de você modificar ou não a classe Application, execute uma das seguintes ações:
    • Se você não modificar a classe Application, edite o arquivo de manifesto para definir android:name na tag <application> desta forma:

      <?xml version="1.0" encoding="utf-8"?>
      <manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example.myapp">
          <application
                  android:name="android.support.multidex.MultiDexApplication" >
              ...
          </application>
      </manifest>
      
    • Se você modificar a classe Application, altere-a para estender MultiDexApplication (se possível) desta forma:

      public class MyApplication extends MultiDexApplication { ... }
      
    • Ou então, se você modificar a classe Application, mas não for possível alterar a classe base, poderá modificar o método attachBaseContext() e chamar MultiDex.install(this) para ativar o multidex:

      public class MyApplication extends SomeOtherApplication {
        @Override
        protected void attachBaseContext(Context base) {
           super.attachBaseContext(base);
           MultiDex.install(this);
        }
      }
      

Agora, ao compilar o aplicativo, as ferramentas de compilação do Android construirão um arquivo DEX principal (classes.dex) e os arquivos de apoio DEX (classes2.dex, classes3.dex e assim por diante) conforme a necessidade. Em seguida, o sistema de compilação empacotará todos os arquivos DEX no APK.

Em tempo de execução, as APIs do multidex usam um carregador de classes especial para pesquisar todos os arquivos DEX disponíveis para os métodos (em vez de pesquisar apenas o arquivo principal classes.dex).

Limitações de biblioteca de suporte a multidex

A biblioteca de suporte a multidex tem algumas limitações que você deve conhecer e testar quando incorporá-la na configuração de compilação do seu aplicativo:

  • A instalação de arquivos DEX durante a inicialização na partição de dados de um dispositivo é complexa e poderá resultar em erros Application Not Responding (ANR) se os arquivos DEX secundários forem grandes. Nesse caso, aplique técnicas de redução de código com o ProGuard para minimizar o tamanho dos arquivos DEX e remover as partes não utilizadas do código.
  • Não é possível iniciar aplicativos que usam multidex em dispositivos com versões da plataforma anteriores ao Android 4.0 (API de nível 14) devido a um bug em linearAlloc da Dalvik (problema 22586). Se seu aplicativo for direcionado para níveis de API anteriores ao 14, teste-o com essas versões da plataforma, pois ele poderá apresentar problemas de inicialização ou carga de determinados grupos de classes. A redução de código pode diminuir ou possivelmente eliminar esses problemas em potencial.
  • Aplicativos que usam uma configuração de multidex que faz solicitações de alocação de grandes quantidades de memória podem falhar durante o tempo de execução devido ao limite linearAlloc da Dalvik (problema 78035). O limite de alocação foi aumentado no Android 4.0 (nível de API 14), mas os aplicativos ainda poderão se deparar com esse limite em versões do Android anteriores ao Android 5.0 (nível de API 21).

Declarar as classes necessárias no arquivo DEX principal

Ao criar cada arquivo DEX de um aplicativo multidex, as ferramentas de compilação tomam decisões complexas para determinar as classes necessárias no arquivo DEX principal para que o aplicativo seja iniciado corretamente. Se qualquer classe necessária durante a inicialização não for fornecida no arquivo DEX principal, o aplicativo falhará com o erro java.lang.NoClassDefFoundError.

Isso não deve ocorrer para código acessado diretamente no código do aplicativo, porque as ferramentas de compilação reconhecem esses caminhos de código. Mas é possível que isso ocorra quando os caminhos de compilação são menos visíveis, como quando uma biblioteca usada tem dependências complexas. Por exemplo, se o código usar introspecção ou invocação de métodos Java no código nativo, essas classes poderão não ser reconhecidas como necessárias no arquivo DEX principal.

Portanto, se você receber java.lang.NoClassDefFoundError, deverá especificar manualmente essas classes adicionais no arquivo DEX principal, declarando-as com a propriedade multiDexKeepFile ou multiDexKeepProguard no tipo de compilação. Se uma classe for correspondida no arquivo multiDexKeepFile ou multiDexKeepProguard, ela será adicionada ao arquivo DEX principal.

Propriedade multiDexKeepFile

O arquivo especificado em multiDexKeepFile deve conter uma classe por linha no formato com/example/MyClass.class. Por exemplo, você pode criar um arquivo denominado multidex-config.txt como este:

com/example/MyClass.class
com/example/MyOtherClass.class

Em seguida, você pode declarar esse arquivo para um tipo de compilação da seguinte forma:

android {
    buildTypes {
        release {
            multiDexKeepFile file 'multidex-config.txt'
            ...
        }
    }
}

Lembre-se de que o Gradle lê caminhos relativos ao arquivo build.gradle. Portanto, o exemplo acima funcionará se multidex-config.txt estiver no mesmo diretório que o arquivo build.gradle.

Propriedade multiDexKeepProguard

O arquivo multiDexKeepProguard usa o mesmo formato que o ProGuard e é compatível com toda a gramática do ProGuard. Para obter mais informações sobre o formato e a gramática do ProGuard, consulte a seção Keep Options no manual do ProGuard.

O arquivo especificado em multiDexKeepProguard deve conter opções -keep em qualquer sintaxe válida do ProGuard. Por exemplo: -keep com.example.MyClass.class. Você pode criar um arquivo denominado multidex-config.pro como este:

-keep class com.example.MyClass
-keep class com.example.MyClassToo

Ou, se você quiser especificar todas as classes de um pacote, o arquivo será assim:

-keep class com.example.** { *; } // All classes in the com.example package

Em seguida, você pode declarar esse arquivo para um tipo de compilação da seguinte forma:

android {
    buildTypes {
        release {
            multiDexKeepProguard 'multidex-config.pro'
            ...
        }
    }
}

Otimizar o multidex em compilações de desenvolvimento

Uma configuração de multidex exige um tempo de processamento de compilação significativamente maior, pois o sistema de compilação deve tomar decisões complexas sobre quais classes devem ser incluídas no arquivo DEX principal e quais podem ser incluídas nos arquivos DEX secundários. Isso significa que compilações incrementais usando multidex são normalmente mais demoradas e podem retardar o processo de desenvolvimento.

Para reduzir os tempos de compilação mais longos das saídas multidex, crie duas variações de compilação usando productFlavors: uma de desenvolvimento e outra de lançamento, com valores diferentes para minSdkVersion.

Para a variação de desenvolvimento, defina minSdkVersion como 21. Essa configuração ativa um recurso de compilação denominado pre-dexing, que gera saída multidex muito mais rapidamente usando um formato ART disponível apenas no Android 5.0 (API de nível 21) ou posterior. Para a variação de lançamento defina minSdkVersion da forma adequada ao nível mínimo de compatibilidade real. Essa configuração gera um APK multidex que é compatível com mais dispositivos, mas que demora mais para ser compilado.

O exemplo de configuração de compilação a seguir demonstra como configurar essas variáveis em um arquivo de compilação do Gradle:

android {
    defaultConfig {
        ...
        multiDexEnabled true
    }
    productFlavors {
        dev {
            // Enable pre-dexing to produce an APK that can be tested on
            // Android 5.0+ without the time-consuming DEX build processes.
            minSdkVersion 21
        }
        prod {
            // The actual minSdkVersion for the production version.
            minSdkVersion 14
        }
    }
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                                                 'proguard-rules.pro'
        }
    }
}
dependencies {
    compile 'com.android.support:multidex:1.0.3'
}

Após concluir essa alteração de configuração, você poderá usar a variação devDebug do aplicativo para compilações incrementais, combinando os atributos da variação de produto dev com o tipo de compilação debug. Será criado um aplicativo depurável com multidex ativado e ProGuard desativado (porque minifyEnabled é false por padrão). Com essas configurações, o Android Plugin para Gradle fará o seguinte:

  1. Executar pre-dexing: compilar cada módulo de aplicativo e cada dependência como arquivo DEX separado.
  2. Incluir cada arquivo DEX no APK sem modificação (sem redução de código).
  3. O mais importante: os arquivos DEX do módulo não serão combinados. Portanto, o cálculo demorado para determinar o conteúdo do arquivo DEX principal será evitado.

Essas configurações proporcionam compilações rápidas e incrementais, pois somente os arquivos DEX dos módulos modificados serão calculados e empacotados novamente em compilações subsequentes. Contudo, o APK dessas compilações somente pode ser usado para testes em dispositivos com Android 5.0. No entanto, ao implementar a configuração como variação, você preserva a capacidade de executar compilações normais com o nível de API mínimo e redução de código do ProGuard apropriados para o lançamento.

Você também pode compilar as outras variações, incluindo a compilação de uma variação prodDebug, mais demorada, mas que pode ser usada para testes fora do ambiente de desenvolvimento. Na configuração mostrada, a variação prodRelease seria a versão final de teste e lançamento. Para obter mais informações sobre o uso de variações de compilação, consulte Configurar variações de compilação.

Dica: agora que já tem variações de compilação distintas para necessidades de multidex diferentes, você também pode oferecer um arquivo de manifesto diferente para cada variação (para que apenas o manifesto para a API de nível 20 ou anterior altere o nome da tag <application>) ou criar uma subclasse Application para cada variação (para que apenas o manifesto para a API de nível 20 ou anterior estenda a classe MultiDexApplication ou chame MultiDex.install(this)).

Testar aplicativos multidex

Nenhuma configuração adicional é necessária na criação de testes de instrumentação de aplicativos multidex. O AndroidJUnitRunner é fornecido já compatível com multidex, desde que você use MultiDexApplication ou modifique o método attachBaseContext() no seu objeto personalizado Application e chame MultiDex.install(this) para ativar o multidex.

Como alternativa, você pode modificar o método onCreate() em AndroidJUnitRunner:

public void onCreate(Bundle arguments) {
    MultiDex.install(getTargetContext());
    super.onCreate(arguments);
    ...
}

Observação: o uso de multidex para criar um APK de teste não é atualmente permitido.