Desenvolvimento iterativo rápido para Android
Nesta página, descrevemos como o bazel mobile-install
acelera muito o desenvolvimento iterativo para Android. Ele descreve os benefícios dessa abordagem em comparação com os desafios do método tradicional de instalação de apps.
Resumo
Para instalar pequenas mudanças em um app Android rapidamente, faça o seguinte:
- Encontre a regra
android_binary
do app que você quer instalar. - Remova o atributo
proguard_specs
para desativar o Proguard. - Defina o atributo
multidex
comonative
. - Defina o atributo
dex_shards
como10
. - Conecte o dispositivo que executa o ART (não o Dalvik) por USB e ative a depuração USB nele.
- Execute
bazel mobile-install :your_target
. A inicialização do app vai ser um pouco mais lenta do que o normal. - Edite o código ou os recursos do Android.
- Execute
bazel mobile-install --incremental :your_target
. - Aproveite para não ter que esperar muito.
Algumas opções de linha de comando para o Bazel que podem ser úteis:
--adb
informa ao Bazel qual binário adb usar.--adb_arg
pode ser usado para adicionar argumentos extras à linha de comando deadb
. Uma aplicação útil disso é selecionar em qual dispositivo você quer instalar se tiver vários dispositivos conectados à sua estação de trabalho:bazel mobile-install --adb_arg=-s --adb_arg=<SERIAL> :your_target
- O
--start_app
inicia o app automaticamente
Em caso de dúvidas, consulte o exemplo ou entre em contato.
Introdução
Um dos atributos mais importantes de uma cadeia de ferramentas de desenvolvedor é a velocidade: há uma grande diferença entre mudar o código e vê-lo ser executado em um segundo e ter que esperar minutos, às vezes horas, antes de receber feedback sobre se as mudanças fazem o que você espera.
Infelizmente, a cadeia de ferramentas tradicional do Android para criar um .apk envolve muitas etapas monolíticas e sequenciais, e todas elas precisam ser realizadas para criar um app Android. No Google, esperar cinco minutos para criar uma mudança de uma única linha não era incomum em projetos maiores, como o Google Maps.
O bazel mobile-install
acelera muito o desenvolvimento iterativo para Android usando uma combinação de remoção de mudanças, fragmentação de trabalho e manipulação inteligente de elementos internos do Android, tudo sem mudar o código do app.
Problemas com a instalação tradicional de apps
A criação de um app Android tem alguns problemas, incluindo:
Dexing. Por padrão, "dx" é invocado exatamente uma vez no build e não sabe como reutilizar o trabalho de builds anteriores. Ele dexes todos os métodos novamente, mesmo que apenas um tenha sido alterado.
Fazer upload de dados para o dispositivo. O adb não usa toda a largura de banda de uma conexão USB 2.0, e apps maiores podem levar muito tempo para serem enviados. Todo o app é enviado, mesmo que apenas pequenas partes tenham mudado, por exemplo, um recurso ou um único método. Isso pode ser um grande gargalo.
Compilação para código nativo. O Android L introduziu o ART, um novo ambiente de execução do Android, que compila apps antecipadamente em vez de compilar no momento certo, como o Dalvik. Isso torna os apps muito mais rápidos, mas aumenta o tempo de instalação. Essa é uma boa troca para os usuários, porque eles normalmente instalam um app uma vez e o usam muitas vezes. No entanto, isso resulta em um desenvolvimento mais lento, em que um app é instalado muitas vezes e cada versão é executada no máximo algumas vezes.
A abordagem de bazel mobile-install
O bazel mobile-install
faz as seguintes melhorias:
Dexing fragmentado. Depois de criar o código Java do app, o Bazel divide os arquivos de classe em partes de tamanho aproximadamente igual e invoca
dx
separadamente nelas.dx
não é invocado em fragmentos que não mudaram desde a última build.Transferência incremental de arquivos. Os recursos do Android, os arquivos .dex e as bibliotecas nativas são removidos do .apk principal e armazenados em um diretório mobile-install separado. Isso permite atualizar o código e os recursos do Android de forma independente sem reinstalar todo o app. Assim, a transferência dos arquivos leva menos tempo, e apenas os arquivos .dex que foram alterados são recompilados no dispositivo.
Carregar partes do app de fora do .apk. Um pequeno aplicativo stub é colocado no .apk que carrega recursos do Android, código Java e código nativo do diretório de instalação móvel no dispositivo e transfere o controle para o app real. Isso é transparente para o app, exceto em alguns casos extremos descritos abaixo.
Dexing fragmentado
O dexing fragmentado é razoavelmente simples: depois que os arquivos .jar são criados, uma ferramenta os fragmenta em arquivos .jar separados de tamanho aproximadamente igual e invoca dx
naqueles que foram alterados desde o build anterior. A lógica que determina quais fragmentos serão dexados não é específica do Android: ela apenas usa o algoritmo geral de remoção de mudanças do Bazel.
A primeira versão do algoritmo de fragmentação simplesmente ordenava os arquivos .class em ordem alfabética e dividia a lista em partes de tamanho igual. No entanto, isso se mostrou subótimo: se uma classe fosse adicionada ou removida (mesmo uma aninhada ou anônima), todas as classes em ordem alfabética depois dela seriam deslocadas em uma posição, resultando na indexação desses fragmentos novamente. Por isso, decidimos fragmentar pacotes Java em vez de classes individuais. É claro que isso ainda resulta na indexação de muitos fragmentos se um novo pacote for adicionado ou removido, mas isso é muito menos frequente do que adicionar ou remover uma única classe.
O número de fragmentos é controlado pelo arquivo BUILD (usando o atributo android_binary.dex_shards
). Em um mundo ideal, o Bazel determinaria automaticamente quantos fragmentos são melhores, mas atualmente ele precisa saber o conjunto de ações (por exemplo, comandos a serem executados durante o build) antes de executar qualquer uma delas. Assim, não é possível determinar o número ideal de fragmentos porque ele não sabe quantas classes Java haverá no app. Em geral, quanto mais fragmentos, mais rápido será o build e a instalação, mas mais lento será a inicialização do app, porque o vinculador dinâmico precisa fazer mais trabalho. O ideal é entre 10 e 50 fragmentos.
Transferência incremental de arquivos
Depois de criar o app, a próxima etapa é instalá-lo, de preferência com o mínimo de esforço possível. A instalação consiste nas seguintes etapas:
- Instalar o .apk (normalmente usando
adb install
) - Fazer upload dos arquivos .dex, recursos do Android e bibliotecas nativas para o diretório mobile-install
Não há muita incrementalidade na primeira etapa: o app é instalado ou não. No momento, o Bazel depende de o usuário indicar se ele deve realizar essa etapa
usando a opção de linha de comando --incremental
, porque não é possível determinar em
todos os casos se isso é necessário.
Na segunda etapa, os arquivos do app do build são comparados a um arquivo de manifesto no dispositivo que lista quais arquivos do app estão no dispositivo e seus checksums. Os novos arquivos são enviados para o dispositivo, os arquivos que foram alterados são atualizados e os arquivos removidos são excluídos do dispositivo. Se o manifesto não estiver presente, será considerado que todos os arquivos precisam ser enviados.
É possível enganar o algoritmo de instalação incremental mudando um arquivo no dispositivo, mas não o checksum no manifesto. Isso poderia ter sido evitado com o cálculo do checksum dos arquivos no dispositivo, mas isso não foi considerado válido devido ao aumento no tempo de instalação.
O aplicativo Stub
O aplicativo stub é onde acontece a mágica para carregar os dexes, o código nativo e os
recursos do Android do diretório mobile-install
no dispositivo.
O carregamento real é implementado por subclasse BaseDexClassLoader
e é uma
técnica razoavelmente bem documentada. Isso acontece antes que qualquer uma das classes do app
seja carregada. Assim, as classes do aplicativo que estão no APK podem ser
colocadas no diretório mobile-install
do dispositivo para que possam ser atualizadas
sem adb install
.
Isso precisa acontecer antes que qualquer uma das classes do app seja carregada. Assim, nenhuma classe de aplicativo precisa estar no .apk, o que significa que as mudanças nessas classes exigem uma reinstalação completa.
Isso é feito substituindo a classe Application
especificada em
AndroidManifest.xml
pelo
aplicativo stub. Isso
assume o controle quando o app é iniciado e ajusta o carregador de classes e o
gerenciador de recursos adequadamente no momento mais cedo (o construtor) usando
reflexão Java nos internos do framework Android.
Outra coisa que o aplicativo stub faz é copiar as bibliotecas nativas
instaladas pelo mobile-install para outro local. Isso é necessário porque o
linker dinâmico precisa que o bit X
seja definido nos arquivos, o que não é possível
fazer em nenhum local acessível por um adb
não raiz.
Depois que tudo isso for feito, o aplicativo stub vai instanciar a classe Application
real, mudando todas as referências a si mesmo para o aplicativo real no framework do Android.
Resultados
Desempenho
Em geral, bazel mobile-install
resulta em uma aceleração de 4 a 10 vezes na criação e instalação de apps grandes após uma pequena mudança.
Os números a seguir foram calculados para alguns produtos do Google:
Isso, é claro, depende da natureza da mudança: a recompilação depois de mudar uma biblioteca de base leva mais tempo.
Limitações
Os truques do aplicativo stub não funcionam em todos os casos. Os casos a seguir destacam situações em que ele não funciona como esperado:
Quando
Context
é transmitido para a classeApplication
emContentProvider#onCreate()
. Esse método é chamado durante a inicialização do aplicativo antes de termos a chance de substituir a instância da classeApplication
. Portanto,ContentProvider
ainda vai se referir ao aplicativo stub em vez do real. Talvez isso não seja um bug, já que não é recomendável fazer o downcast deContext
dessa forma, mas parece que isso acontece em alguns apps do Google.Os recursos instalados pelo
bazel mobile-install
só estão disponíveis no app. Se outros apps acessarem recursos peloPackageManager#getApplicationResources()
, eles serão da última instalação não incremental.Dispositivos que não estão executando o ART. Embora o aplicativo stub funcione bem no Froyo e em versões mais recentes, o Dalvik tem um bug que faz com que ele pense que o app está incorreto se o código for distribuído em vários arquivos .dex em determinados casos, por exemplo, quando as anotações Java são usadas de uma maneira específica. Desde que o app não acione esses bugs, ele também vai funcionar com o Dalvik. No entanto, o suporte para versões antigas do Android não é nosso foco principal.