bazel mobile-install

Informar um problema Ver fonte Nightly · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

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:

  1. Encontre a regra android_binary do app que você quer instalar.
  2. Remova o atributo proguard_specs para desativar o Proguard.
  3. Defina o atributo multidex como native.
  4. Defina o atributo dex_shards como 10.
  5. Conecte o dispositivo que executa o ART (não o Dalvik) por USB e ative a depuração USB nele.
  6. Execute bazel mobile-install :your_target. A inicialização do app vai 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 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:

  1. Instalar o .apk (normalmente usando adb install)
  2. 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 classe Application em ContentProvider#onCreate(). Esse método é chamado durante a inicialização do aplicativo antes de termos a chance de substituir a instância da classe Application. 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 de Context 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 pelo PackageManager#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.