Desenvolvimento iterativo rápido para Android
Esta página descreve como o bazel mobile-install
torna o desenvolvimento iterativo
para Android muito mais rápido. Ele descreve os benefícios dessa abordagem em relação aos
desafios do método tradicional de instalação de apps.
Resumo
Para instalar pequenas mudanças em um app Android muito rapidamente, faça o seguinte:
- Encontre a regra
android_binary
do app que você quer instalar. - Desative o Proguard removendo o atributo
proguard_specs
. - Defina o atributo
multidex
comonative
. - Defina o atributo
dex_shards
como10
. - Conecte o dispositivo que usa 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 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 não ter que esperar muito.
Algumas opções de linha de comando do Bazel que podem ser úteis:
--adb
informa ao Bazel qual binário do adb usar--adb_arg
pode ser usado para adicionar outros argumentos à linha de comando deadb
. Uma aplicação útil disso é selecionar em qual dispositivo você quer instalar se você tiver vários dispositivos conectados à estação de trabalho:bazel mobile-install --adb_arg=-s --adb_arg=<SERIAL> :your_target
--start_app
inicia o app automaticamente
Em caso de dúvida, consulte o exemplo ou entre em contato conosco.
Introdução
Um dos atributos mais importantes da cadeia de ferramentas de um 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 atendem às suas expectativas.
Infelizmente, a cadeia de ferramentas tradicional do Android para criar um .apk envolve muitas etapas sequenciais monolíticas, e todas elas precisam ser feitas para criar um app Android. No Google, esperar cinco minutos para fazer uma mudança de linha não era incomum em projetos maiores, como o Google Maps.
O bazel mobile-install
torna o desenvolvimento iterativo para Android muito mais rápido
usando uma combinação de poda 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, o "dx" é invocado exatamente uma vez no build e não sabe como reutilizar o trabalho de builds anteriores: ele dexifica todos os métodos novamente, mesmo que apenas um método 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 fazer upload. O app inteiro é enviado, mesmo que apenas pequenas partes tenham mudado, por exemplo, um recurso ou um método único. Isso pode ser um gargalo.
Compilação para código nativo. O Android L introduziu o ART, um novo tempo de execução do Android, que compila apps com antecedência, em vez de fazer isso no momento, 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 geralmente instalam um app uma vez e usam muitas vezes, mas 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 fragmenta os arquivos de classe em partes de tamanho aproximado e invoca
dx
separadamente nelas.dx
não é invocado em fragmentos que não mudaram desde o último build.Transferência de arquivos incrementais. Os recursos do Android, os arquivos .dex e as bibliotecas nativas são removidos do .apk principal e armazenados em um diretório de instalação para dispositivos móveis separado. Isso permite atualizar o código e os recursos do Android de forma independente sem reinstalar o app inteiro. 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 arquivo .apk que carrega recursos 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. Tudo isso é transparente para o app, exceto em alguns casos descritos abaixo.
Dexing fragmentado
A dexação fragmentada é razoavelmente simples: depois que os arquivos .jar são criados, uma
ferramenta
os fragmenta em arquivos .jar separados de tamanho aproximadamente igual e, em seguida, invoca
dx
nos que foram alterados desde o build anterior. A lógica que
determina quais fragmentos serão dex não é específica do Android: ela apenas usa o
algoritmo de poda de mudanças geral do Bazel.
A primeira versão do algoritmo de fragmentação simplesmente ordenava os arquivos .class em ordem alfabética e depois dividia a lista em partes de tamanhos iguais, mas isso não era ótimo: se uma classe fosse adicionada ou removida (mesmo que anônima ou aninhada), todas as classes depois dela seriam deslocadas em uma posição, resultando na indexação dos fragmentos novamente. Portanto, foi decidido dividir os pacotes Java em vez de classes individuais. É claro que isso ainda resulta na dexaçã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. Portanto, ele não pode determinar o número ideal de fragmentos
porque não sabe quantas classes Java haverá no
app. De modo geral, quanto mais fragmentos, mais rápido o build e a
instalação serão, mas a inicialização do app fica mais lenta, porque o vinculador
dinâmico precisa fazer mais trabalho. O ponto ideal geralmente fica entre 10 e 50 fragmentos.
Transferência de arquivos incrementais
Depois de criar o app, a próxima etapa é instalá-lo, de preferência com o menor 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 de instalação em dispositivos móveis
Não há muita incrementalidade na primeira etapa: o app é instalado
ou não. Atualmente, o Bazel depende do usuário para indicar se ele precisa realizar essa etapa
com a opção de linha de comando --incremental
, porque não é possível determinar em
todos os casos se ela é necessária.
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 os checksums. Os arquivos novos são enviados por upload para o dispositivo, os arquivos 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 a soma de verificação dele no manifesto. Isso poderia ter sido protegido computando a soma de verificação dos arquivos no dispositivo, mas isso foi considerado não valer o aumento no tempo de instalação.
O aplicativo Stub
É no aplicativo stub que 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 pela subclassificação de BaseDexClassLoader
e é uma
técnica razoavelmente bem documentada. Isso acontece antes que qualquer uma das classes
do app seja carregada, para que todas as classes do aplicativo que estão no APK possam ser
colocadas no diretório mobile-install
no 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 app precisa estar no .apk, o que significa que as mudanças nessas classes exigiriam uma reinstalação completa.
Isso é feito substituindo a classe Application
especificada em
AndroidManifest.xml
pelo
aplicativo stub. Ele
assume o controle quando o app é iniciado e ajusta o carregador de classe e o
gerenciador de recursos de maneira adequada no momento mais cedo (o construtor) usando
a reflexão do Java nas partes internas do framework do Android.
Outra coisa que o aplicativo stub faz é copiar as bibliotecas nativas
instaladas pela instalação móvel para outro local. Isso é necessário porque o
vinculador dinâmico precisa que o bit X
seja definido nos arquivos, o que não é possível
fazer para qualquer local acessível por um adb
não raiz.
Depois de fazer tudo isso, o aplicativo de stub instancia a
classe Application
real, mudando todas as referências a si mesmo para o app
real no framework do Android.
Resultados
Desempenho
Em geral, o bazel mobile-install
resulta em um aumento de 4 a 10 vezes na velocidade de build
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 recompilation após mudar uma biblioteca base leva mais tempo.
Limitações
Os truques que o aplicativo stub usa não funcionam em todos os casos. Os casos a seguir destacam onde ele não funciona como esperado:
Quando
Context
é convertido para a classeApplication
emContentProvider#onCreate()
. Esse método é chamado durante a inicialização do aplicativo antes que tenhamos a chance de substituir a instância da classeApplication
. Portanto,ContentProvider
ainda vai fazer referência ao aplicativo stub em vez do real. Provavelmente, isso não é um bug, já que não é possível converterContext
dessa forma, mas isso parece acontecer em alguns apps do Google.Os recursos instalados por
bazel mobile-install
só ficam disponíveis no app. Se eles forem acessados por outros apps usandoPackageManager#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 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 forma específica. Se o app não apresentar esses bugs, ele também vai funcionar com o Dalvik. Observe, no entanto, que o suporte a versões antigas do Android não é exatamente o nosso foco.