segunda-feira, 27 de março de 2017

Resolvendo problemas de MultiDex em Androids pre-Lollipop

por  | mar 27, 2017 
Estou trabalhando em um projeto Xamarin Android que precisa utilizar muitas bibliotecas como dependências, isso faz o aplicativo crescer muito. Se você já passou por isso deve ter caído na limitação da plataforma Android de permitir no máximo 65.536 “fields” declarados na sua aplicação. Isso não acontece porque o projeto é feito com Xamarin, todo aplicativo Android que cresça muito passará por isso. E agora? Como solucionar isso? Vamos por partes para entendermos tudo que precisa ser feito, porque a solução depende de uma série de detalhes.

A limitação dos fields

Primeiro, o que é essa limitação de 65k fields? O que são fields? O que isso significa pro meu fluxo de desenvolvimento?
Este número não é arbitrário, ele representa a quantidade máxima de referências que podem existir em arquivos de bytecode Dalvik Executables (DEX) (64×1024). De forma resumida, esses arquivos são usados na execução do nosso aplicativo e estão presentes no nosso .APK. Qualquer parte referenciável do código entra na conta desse limite (métodos, properties, classes, etc.), esses são os “fields”.
Quando seu aplicativo atinge esse limite você vai receber essa mensagem de erro:
Conversion to Dalvik format failed:
Unable to execute dex: method ID not in [0, 0xffff]: 65536
Em versões mais novas você pode ver isso:
trouble writing output:
Too many field references: 131000; max is 65536.
You may try using --multi-dex option.
Nós tentamos evitar atingir esse limite usando Linker/Proguard, mas em alguns casos esse limite é inevitavelmente atingido, e aí o Android precisa ser capaz de criar mais de um arquivo DEX. O problema é que a plataforma Android pré-Lollipop (versões anteriores à 5.0) não sabem procurar as referências em múltiplos arquivos .DEX.

MultiDex

Pensando em solucionar esse problema, foi criado o MultiDex. Como o nome sugere, ele permite que código de múltiplos arquivos DEX possam ser lidos e referenciados. Nós só precisamos habilitar o MultiDex em nossa aplicação e toda mágica deve acontecer.
Em versões pós-Lollipop (5.0+) o próprio runtime já sabe interpretar múltiplos arquivos DEX e tudo deve funcionar normalmente.
Em versões pré-Lollipop (5.0-), é utilizado uma Support Library que faz com que sua aplicação passe a ser uma MultiDexApplication, que fica sempre no arquivo DEX principal. A partir daí, ela garante que as referências sempre serão encontradas quando nossa aplicação pedir por elas.
Habilitar isso num projeto Xamarin Android significa apenas marcar um checkbox nas opções do Android:
xamarin android options enable multidex
Com isso agora tudo deveria funcionar. Testei o aplicativo em um aparelho com Android Nougat (7.0), funcionou e fiquei feliz, mas quando testei em um aparelho KitKat (4.4) o aplicativo crashou logo após abrir com um novo erro de ClassNotFoundException:
[AndroidRuntime] java.lang.RuntimeException: Unable to instantiate application
md547fd225c67849457194a3f0f19c36fe3.DvApp: java.lang.ClassNotFoundException:
Didn't find class "md547fd225c67849457194a3f0f19c36fe3.DvApp" on path...
A classe que ele reclama é a classe principal de entrada do aplicativo. Mas aí você pode estar se perguntando: o MultiDex não foi usado justamente para garantir que essa classe esteja no arquivo DEX principal para que tudo funcione? Sim. E é aí que começa o próximo capítulo dessa saga.

Conferindo os arquivos DEX

O checkbox do MultiDex em um projeto Xamarin Android executa o processo de multidexing de maneira um pouco “mágica”. Ele faz com que sua classe Application passe a herdar de MultiDexApplication (no momento de compilação), caso seu projeto referencie versões pre-Lollipop do Android.
O processo de multidexing gera um arquivo multidex.keep na pasta obj\Debug que será usado para definir quais são as referências que precisam estar no arquivo DEX principal. Olhando esse arquivo percebi que ele estava vazio, então algo estava errado.
Procurei uma maneira de verificar se os arquivos tinham sido gerados corretamente e se estavam no DEX principal, para isso utilizei uma ferramenta open source chamada ClassyShark que abre um arquivo .APK e exibe os arquivos DEX presentes. Pesquisei pelo nome da classe que não era encontrada, que estava na ClassNotFoundException acima, e ele mostrou que, de fato, a classe não estava no arquivo de DEX principal, e sim em um terceiro arquivo classes3.dex:
classyshark dex
Como vimos, isso faz com que o aplicativo não funcione em versões pre-Lollipop, então precisamos solucionar esse problema.

Bug no Android build-tools

Pesquisando o problema encontrei alguns links com relatos semelhantes [1],[2],[3] e percebi que trata-se de um bug no build-tools do Android (mais um dos bugs eternos do Android).
No fim das contas o problema só ocorre se o build for feito numa máquina Windows, que é a versão do build-tools afetada pelo bug, e apesar da equipe do Android ainda não ter solucionado o problema, existe um workaround.
O arquivo do build-tools que causa o problema é o arquivo mainDexClasses.bat que fica na pasta “android-sdk\build-tools\VERSAO_DO_BUILD_TOOLS”. O papel deste script é identificar todas as classes que precisam ficar no arquivo DEX principal, mas devido ao bug reportado acima, este script não está funcionando no Windows. Para que ele funcione corretamente, precisamos editar este trecho, no final do arquivo:
if DEFINED output goto redirect
call "%java_exe%" -Djava.ext.dirs="%frameworkdir%" com.android.multidex.MainDexListBuilder "%disableKeepAnnotated%" "%tmpJar%" "%params%"
goto afterClassReferenceListBuilder :redirect
call "%java_exe%" -Djava.ext.dirs="%frameworkdir%" com.android.multidex.MainDexListBuilder "%disableKeepAnnotated%" "%tmpJar%" "%params%" 1>"%output%"
:afterClassReferenceListBuilder
Para:
SET params=%params:'=%
if DEFINED output goto redirect
call "%java_exe%" -Djava.ext.dirs="%frameworkdir%" com.android.multidex.MainDexListBuilder %disableKeepAnnotated% "%tmpJar%" %params%
goto afterClassReferenceListBuilder :redirect
call "%java_exe%" -Djava.ext.dirs="%frameworkdir%" com.android.multidex.MainDexListBuilder %disableKeepAnnotated% "%tmpJar%" %params% 1>"%output%"
:afterClassReferenceListBuilder
Um ponto importante é que é possível que tenhamos mais de uma versão do build-tools instalada na máquina, então precisamos nos atentar para editar o arquivo correto. Na dúvida você pode manter apenas a versão do build-tools mais atualizada.
Lembre-se também que sempre que você atualizar seu build-tools, você terá que editar o arquivo novamente, até que a equipe do Android resolva o problema, pois ele será sobrescrito pela atualização.
Agora o processo de multidexing faz tudo corretamente, preenche o arquivo multidex.keep, coloca o arquivo de Application no arquivo DEX principal e o deploy ocorre com sucesso!
classydex

Conclusão

Bugs assim numa plataforma tão usada mostram que desenvolvimento mobile ainda tem muito a amadurecer, e a busca pela solução, ainda mais quando envolve várias camadas e bugs em ferramentas que normalmente julgamos estáveis, pode ser bem complicada. Nesse caso a solução não é tão boa quanto eu gostaria, todo desenvolvedor precisa lembrar de fazer isso em sua máquina caso queira testar em aparelhos pre-Lollipop. Vale lembrar que nosso servidor de build também precisa receber essa correção para gerar um APK válido para todas as plataformas.
Foto usada no post: Patrick Tomasso

Nenhum comentário:

Postar um comentário