bazel mobile-install

Informar um problema Acessar a origem

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 comparação com os 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:

  1. Encontre a regra android_binary do app que você quer instalar.
  2. Desative o Proguard removendo o atributo proguard_specs.
  3. Defina o atributo multidex como native.
  4. Defina o atributo dex_shards como 10.
  5. Conecte o dispositivo que executa ART (não Dalvik) via USB e ative a depuração USB nele.
  6. Execute bazel mobile-install :your_target. A inicialização do app será um pouco mais lenta do que o normal.
  7. Edite o código ou os recursos do Android.
  8. Execute bazel mobile-install --incremental :your_target.
  9. 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 de adb. Uma aplicação útil disso é selecionar em qual dispositivo você quer instalar se houver vários dispositivos conectados à estação de trabalho: bazel mobile-install --adb_arg=-s --adb_arg=<SERIAL> :your_target
  • O app --start_app inicia o app automaticamente

Em caso de dúvida, veja o exemplo ou entre em contato.

Introdução

Um dos atributos mais importantes do conjunto de ferramentas de um desenvolvedor é a velocidade: há uma grande diferença entre mudar o código e vê-lo executado em um segundo e ter que esperar minutos, às vezes horas, antes de receber feedback para saber se as mudanças fazem o que você espera.

Infelizmente, o conjunto de ferramentas tradicional do Android para criar um arquivo .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 linha única 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 remoção de mudanças, fragmentação de trabalho e manipulação inteligente dos elementos internos do Android, tudo sem mudar o código do app.

Problemas com a instalação tradicional de apps

Criar um app Android tem alguns problemas, incluindo:

  • Dexação. Por padrão, "dx" é invocado exatamente uma vez no build e não sabe como reutilizar o trabalho de builds anteriores: ele dexa todos os métodos novamente, mesmo que apenas um deles tenha sido modificado.

  • 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 o upload de apps maiores pode levar muito tempo. O upload do app inteiro é feito mesmo que apenas pequenas partes tenham sido alteradas, por exemplo, em um recurso ou um único método. Portanto, isso pode gerar 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 com antecedência em vez de compilá-los no momento certo, como o Dalvik. Isso torna os apps muito mais rápidos ao custo de um tempo de instalação maior. Essa é uma boa vantagem para os usuários, porque eles normalmente instalam um app e o usam muitas vezes, mas resulta em desenvolvimento mais lento, em que o app é instalado muitas vezes e cada versão é executada no máximo algumas vezes.

A abordagem de bazel mobile-install

bazel mobile-installfaz as seguintes melhorias:

  • Dexação fragmentada. Depois de criar o código Java do app, o Bazel fragmenta os arquivos de classe em partes de tamanho aproximadamente igual e invoca dx separadamente neles. dx não é invocado em fragmentos que não mudaram desde o último build.

  • Transferência incremental de arquivos. Recursos do Android, arquivos .dex e bibliotecas nativas são removidos do .apk principal e armazenados em um diretório separado de instalação para dispositivos móveis. Isso permite atualizar o código e os recursos do Android de maneira independente, sem reinstalar todo o app. Assim, a transferência dos arquivos leva menos tempo e apenas os arquivos .dex que foram modificados são recompilados no dispositivo.

  • Carregar partes do app de fora do .apk. Um pequeno aplicativo stub é colocado no .apk que carrega recursos Android, código Java e código nativo do diretório de instalação de dispositivos móveis no dispositivo e, em seguida, transfere o controle para o app real. Tudo isso é transparente para o app, exceto em alguns casos específicos descritos abaixo.

Dexação fragmentada

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 naqueles que foram alterados desde o build anterior. A lógica que determina quais fragmentos dex não é específica do Android: ela usa apenas o algoritmo de remoção de mudanças gerais do Bazel.

A primeira versão do algoritmo de fragmentação simplesmente ordenava os arquivos .class em ordem alfabética e depois cortou a lista em partes de tamanhos iguais, mas isso não era ideal: se uma classe fosse adicionada ou removida (mesmo uma aninhada ou anônima), isso faria com que todas as classes depois dela mudassem por um, resultando na dexação desses fragmentos novamente. Por isso, foi decidido fragmentar pacotes Java em vez de classes individuais. Obviamente, 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). Idealmente, o Bazel determina automaticamente quantos fragmentos são os melhores, mas atualmente ele precisa conhecer o conjunto de ações (por exemplo, comandos a serem executados durante a criação) 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 vão existir no app. De modo geral, quanto mais fragmentos e a instalação fica, mais rápido será o trabalho. O ideal geralmente fica 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 esforço possível. A instalação consiste nas seguintes etapas:

  1. Como instalar o .apk (geralmente usando adb install)
  2. Fazer upload dos arquivos .dex, recursos do Android e bibliotecas nativas para o diretório de instalação para dispositivos móveis

Não há muita incrementabilidade na primeira etapa: o app está instalado ou não. No momento, o Bazel depende do usuário para indicar se precisa fazer essa etapa usando a opção de linha de comando --incremental, porque não é possível determinar em todos os casos se é necessário.

Na segunda etapa, os arquivos do app do build são comparados a um arquivo de manifesto no dispositivo que lista os arquivos do app que estão no dispositivo e as somas de verificação. Todos os novos arquivos são enviados para o dispositivo, os que foram modificados são atualizados, e os arquivos removidos são excluídos do dispositivo. Se o manifesto não estiver presente, será presumido que todos os arquivos precisam ser enviados.

É possível enganar o algoritmo de instalação incremental ao alterar um arquivo no dispositivo, mas não a soma de verificação no manifesto. Isso poderia ser evitado com o cálculo da soma de verificação dos arquivos no dispositivo, mas isso não compensa o aumento no tempo de instalação.

O aplicativo Stub

É no aplicativo stub que a mágica para carregar os dexes, o código nativo e os recursos Android do diretório mobile-install no dispositivo acontece.

O carregamento real é implementado pela subclasse BaseDexClassLoader e é uma técnica bem documentada. Isso acontece antes que qualquer uma das classes do app seja carregada. Assim, todas as classes de aplicativo que estão no apk podem 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 para que nenhuma classe de aplicativo precise 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 de stub. Isso assume o controle quando o app é iniciado e ajusta o carregador de classes e o gerenciador de recursos de forma adequada no primeiro momento (o construtor dele) usando reflexão Java sobre os componentes internos do framework do Android.

Outra coisa que o aplicativo stub faz é copiar as bibliotecas nativas instaladas pela instalação em dispositivos móveis 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 para nenhum local acessível por um adb não raiz.

Depois que tudo isso for feito, o aplicativo stub instancia a classe Application real, mudando todas as referências a si mesmo para o aplicativo real no framework do Android.

Resultados

Performance

Em geral, bazel mobile-install resulta em uma velocidade de criação e instalação de apps grandes de 4 a 10 vezes após uma pequena mudança.

Os números a seguir foram calculados para alguns produtos do Google:

Obviamente, isso depende da natureza da mudança: a recompilação depois de mudar uma biblioteca de base leva mais tempo.

Limitações

Os truques que o aplicativo stub usa não funcionam em todos os casos. Os seguintes casos destacam quando ele não funciona como esperado:

  • Quando Context é transmitido para a classe Application em ContentProvider#onCreate(). Esse método é chamado durante a inicialização do aplicativo antes que seja possível substituir a instância da classe Application. Portanto, ContentProvider ainda vai fazer referência ao aplicativo stub em vez do aplicativo real. Pode-se argumentar que isso não é um bug, já que não é possível diminuir o Context dessa forma, mas isso parece acontecer em alguns apps no Google.

  • Os recursos instalados por bazel mobile-install só estão disponíveis dentro do app. Se os recursos forem acessados por outros apps via PackageManager#getApplicationResources(), esses recursos serão da última instalação não incremental.

  • Dispositivos que não executam o ART. Embora o aplicativo stub funcione bem no Froyo e em versões mais recentes, o Dalvik tem um bug que o faz pensar que o app está incorreto se o código estiver distribuído em vários arquivos .dex em determinados casos, por exemplo, quando anotações Java são usadas de uma forma específica. Desde que seu app não acorde esses bugs, ele também deve funcionar com o Dalvik (observação, no entanto, que o suporte a versões antigas do Android não é exatamente nosso foco)