Como reduzir o código - limite de método de 65k em dex

91

Tenho um aplicativo Android bastante grande que depende de muitos projetos de biblioteca. O compilador Android tem uma limitação de 65536 métodos por arquivo .dex e estou ultrapassando esse número.

Existem basicamente dois caminhos que você pode escolher (pelo menos que eu saiba) quando atinge o limite do método.

1) Reduza seu código

2) Crie vários arquivos dex ( veja esta postagem do blog )

Eu olhei em ambos e tentei descobrir o que estava fazendo com que minha contagem de métodos fosse tão alta. A API do Google Drive leva a maior parte com a dependência Guava em mais de 12.000. O total de libs para Drive API v2 chega a mais de 23.000!

Minha pergunta, eu acho, é: o que você acha que eu devo fazer? Devo remover a integração do Google Drive como um recurso do meu aplicativo? Existe uma maneira de reduzir a API (sim, eu uso o proguard)? Devo seguir a rota dex múltipla (que parece um tanto dolorosa, especialmente lidando com APIs de terceiros)?

Jared Rummler
fonte
2
Acontece que adoro seu aplicativo. Você já pensou em fazer um download obrigatório de todas as libs extras em uma apkforma pseudo ? Eu pessoalmente gostaria de ver a integração do Drive
JBirdVegas
8
O Facebook recentemente documentou sua solução alternativa para o que parece ser um problema quase idêntico em seu aplicativo Android. Pode ser útil: facebook.com/notes/facebook-engineering/…
Reuben Scratton
4
Começando a descer a rota múltipla dex. Criei com sucesso um arquivo dex secundário para trabalhar com o Google Drive. Me sinto mal por quem precisa de goiaba como dependência. : P Ainda é um problema muito grande para mim
Jared Rummler
4
como você conta os métodos?
Bri6ko
1
Algumas observações adicionais aqui: stackoverflow.com/questions/21490382 (incluindo um link para um utilitário que listará as referências de método em um APK). Observe que o limite de 64K não está relacionado ao problema do Facebook vinculado a alguns comentários.
fadden

Respostas:

69

Parece que o Google finalmente implementou uma solução alternativa / correção para ultrapassar o limite de método de 65K de arquivos dex.

Sobre o limite de referência de 65K

Os arquivos de aplicativos Android (APK) contêm arquivos de bytecode executáveis ​​na forma de arquivos Dalvik Executable (DEX), que contêm o código compilado usado para executar seu aplicativo. A especificação Dalvik Executable limita o número total de métodos que podem ser referenciados em um único arquivo DEX a 65.536, incluindo métodos de estrutura Android, métodos de biblioteca e métodos em seu próprio código. Ultrapassar esse limite requer que você configure o processo de construção de seu aplicativo para gerar mais de um arquivo DEX, conhecido como configuração multidex.

Suporte multidex antes do Android 5.0

As versões da plataforma anteriores ao Android 5.0 usam o tempo de execução Dalvik para executar o código do aplicativo. Por padrão, a Dalvik limita os aplicativos a um único arquivo de bytecode classes.dex por APK. Para contornar essa limitação, você pode usar a biblioteca de suporte multidex , que se torna parte do arquivo DEX principal do seu aplicativo e, em seguida, gerencia o acesso aos arquivos DEX adicionais e ao código que eles contêm.

Suporte multidex para Android 5.0 e superior

O Android 5.0 e superior usa um tempo de execução chamado ART, que suporta nativamente o carregamento de vários arquivos dex de arquivos APK do aplicativo. O ART realiza a pré-compilação no momento da instalação do aplicativo, que verifica os arquivos de classes (.. N) .dex e os compila em um único arquivo .oat para execução pelo dispositivo Android. Para obter mais informações sobre o tempo de execução do Android 5.0, consulte Apresentando ART .

Consulte: Criando aplicativos com mais de 65 mil métodos


Biblioteca de Suporte Multidex

Esta biblioteca fornece suporte para a construção de aplicativos com vários arquivos Dalvik Executable (DEX). Aplicativos que fazem referência a mais de 65536 métodos são necessários para usar configurações multidex. Para obter mais informações sobre como usar multidex, consulte Criando aplicativos com mais de 65 mil métodos .

Esta biblioteca está localizada no diretório / extras / android / support / multidex / após o download das bibliotecas de suporte do Android. A biblioteca não contém recursos de interface do usuário. Para incluí-lo em seu projeto de aplicativo, siga as instruções para Adicionar bibliotecas sem recursos.

O identificador de dependência do script de compilação do Gradle para esta biblioteca é o seguinte:

com.android.support:multidex:1.0.+ Esta notação de dependência especifica a versão de lançamento 1.0.0 ou superior.


Você ainda deve evitar atingir o limite de método de 65K usando ativamente o proguard e revisando suas dependências.

Jared Rummler
fonte
6
+1, Por que as pessoas não votam nas respostas corretas quando são respondidas pela mesma pessoa?
Pacerier
o nível mínimo da API torna-se 14!
Vihaan Verma
5
Escrevemos um pequeno plugin do Gradle para fornecer sua contagem de métodos atual em cada compilação. Foi útil para nós gerenciar bibliotecas - github.com/KeepSafe/dexcount-gradle-plugin
philipp
53

você pode usar a biblioteca de suporte multidex para isso, para habilitar multidex

1) inclua-o nas dependências:

dependencies {
  ...
  compile 'com.android.support:multidex:1.0.0'
}

2) Habilite em seu aplicativo:

defaultConfig {
    ...
    minSdkVersion 14
    targetSdkVersion 21
    ....
    multiDexEnabled true
}

3) se você tiver uma classe de aplicativo para seu aplicativo, substitua o método attachBaseContext como este:

package ....;
...
import android.support.multidex.MultiDex;

public class MyApplication extends Application {
  ....
   @Override
   protected void attachBaseContext(Context context) {
    super.attachBaseContext(context);
    MultiDex.install(this);
   }
}

4) se você não tiver uma classe de aplicativo para seu aplicativo, registre android.support.multidex.MultiDexApplication como seu aplicativo no arquivo de manifesto. como isso:

<application
    ...
    android:name="android.support.multidex.MultiDexApplication">
    ...
</application>

e deve funcionar bem!

Prakhar
fonte
31

Play Services6.5+ ajuda: http://android-developers.blogspot.com/2014/12/google-play-services-and-dex-method.html

"A partir da versão 6.5, do Google Play Services, você poderá escolher entre uma série de APIs individuais e poderá ver"

...

"isso incluirá transitivamente as bibliotecas 'básicas', que são usadas em todas as APIs."

Esta é uma boa notícia, para um jogo simples, por exemplo, você provavelmente só precisa do base, gamese talvez drive.

"A lista completa de nomes de APIs está abaixo. Mais detalhes podem ser encontrados no site Android Developer:

  • com.google.android.gms: play-services-base: 6.5.87
  • com.google.android.gms: play-services-ads: 6.5.87
  • com.google.android.gms: play-services-appindexing: 6.5.87
  • com.google.android.gms: play-services-maps: 6.5.87
  • com.google.android.gms: play-services-location: 6.5.87
  • com.google.android.gms: play-services-fitness: 6.5.87
  • com.google.android.gms: play-services-panorama: 6.5.87
  • com.google.android.gms: play-services-drive: 6.5.87
  • com.google.android.gms: play-services-games: 6.5.87
  • com.google.android.gms: play-services-wallet: 6.5.87
  • com.google.android.gms: play-services-identity: 6.5.87
  • com.google.android.gms: play-services-cast: 6.5.87
  • com.google.android.gms: play-services-plus: 6.5.87
  • com.google.android.gms: play-services-appstate: 6.5.87
  • com.google.android.gms: play-services-wearable: 6.5.87
  • com.google.android.gms: play-services-all-wear: 6.5.87
Csaba Toth
fonte
Alguma informação sobre como fazer isso em um projeto Eclipse?
Brian White
Não estou em posição de atualizar para essa versão ainda. Mas se seu projeto é baseado em Maven, então esperamos que você apenas tenha que resolver isso em seu pom maven.
Csaba Toth
@ webo80 Bem, isso só ajuda se você tiver a versão 6.5.87. Eu me pergunto sobre a resposta de Petey, que proguard retira funções não utilizadas. Eu me pergunto se isso envolve bibliotecas de 2ª parte também, ou apenas suas próprias coisas. Eu deveria ler mais sobre Proguard.
Csaba Toth
@BrianWhite A única solução por enquanto parece remover o arquivo .jar com alguma ferramenta externa.
milosmns
Acabei usando esta ferramenta: gist.github.com/dextorer/a32cad7819b7f272239b
Brian White
9

Nas versões do Google Play Services anteriores à 6.5, você tinha que compilar todo o pacote de APIs em seu aplicativo. Em alguns casos, isso tornava mais difícil manter o número de métodos em seu aplicativo (incluindo APIs de estrutura, métodos de biblioteca e seu próprio código) abaixo do limite de 65.536.

A partir da versão 6.5, você pode compilar seletivamente APIs de serviço do Google Play em seu aplicativo. Por exemplo, para incluir apenas as APIs do Google Fit e Android Wear, substitua a seguinte linha no arquivo build.gradle:

compile 'com.google.android.gms:play-services:6.5.87'

com estas linhas:

compile 'com.google.android.gms:play-services-fitness:6.5.87'
compile 'com.google.android.gms:play-services-wearable:6.5.87'

para mais referências, você pode clicar aqui

akshay
fonte
Como fazer isso em eclipse?
Hardik9850
7

Use o proguard para iluminar seu apk, pois os métodos que não são usados ​​não estarão em sua construção final. Verifique novamente se você tem o seguinte em seu arquivo de configuração do proguard para usar o proguard com goiaba (minhas desculpas se você já tiver isso, não era conhecido no momento da escrita):

# Guava exclusions (http://code.google.com/p/guava-libraries/wiki/UsingProGuardWithGuava)
-dontwarn sun.misc.Unsafe
-dontwarn com.google.common.collect.MinMaxPriorityQueue
-keepclasseswithmembers public class * {
    public static void main(java.lang.String[]);
} 

# Guava depends on the annotation and inject packages for its annotations, keep them both
-keep public class javax.annotation.**
-keep public class javax.inject.**

Além disso, se você estiver usando o ActionbarSherlock, mudar para a biblioteca de suporte appcompat v7 também reduzirá muito a contagem de métodos (com base na experiência pessoal). As instruções estão localizadas:

petey
fonte
isso parece promissor, mas consegui Warning: butterknife.internal.ButterKnifeProcessor: can't find superclass or interface javax.annotation.processing.AbstractProcessordurante a corrida./gradlew :myapp:proguardDevDebug
Ericn
1
Durante o desenvolvimento, porém, o proguard geralmente não é executado (pelo menos não com o Eclipse), portanto, você não pode se beneficiar da redução até fazer uma versão de lançamento.
Brian White
7

Você pode usar Jar Jar Links para reduzir enormes bibliotecas externas como o Google Play Services (métodos de 16K!)

No seu caso, você apenas copiará tudo do jar do Google Play Services, exceto common internale drivesubpacotes.

pixel
fonte
4

Para usuários do Eclipse que não usam o Gradle, existem ferramentas que quebram o jar do Google Play Services e o reconstroem apenas com as partes que você deseja.

Eu uso strip_play_services.sh de dextorer .

Pode ser difícil saber exatamente quais serviços incluir porque existem algumas dependências internas, mas você pode começar pequeno e adicionar algo à configuração se descobrir que algo necessário está faltando.

Brian White
fonte
3

Acho que, a longo prazo, quebrar seu aplicativo em dex múltiplo seria a melhor maneira.

prmottajr
fonte
2
Estou procurando uma maneira adequada de fazer isso com o Gradle: - / Alguma dica?
Ivan Morgillo 01 de
2

O suporte a Multi-dex será a solução oficial para este problema. Veja minha resposta aqui para os detalhes.

Alex Lipov
fonte
2

Se não usar multidex, o que torna o processo de construção muito lento. Você pode fazer o seguinte. Como yahska mencionou, use uma biblioteca de serviço específica do Google Play. Na maioria dos casos, apenas isso é necessário.

compile 'com.google.android.gms:play-services-base:6.5.+'

Aqui estão todos os pacotes disponíveis Compilando APIs seletivamente em seu executável

Se isso não for suficiente, você pode usar o script do Gradle. Coloque este código no arquivo 'strip_play_services.gradle'

def toCamelCase(String string) {
String result = ""
string.findAll("[^\\W]+") { String word ->
    result += word.capitalize()
}
return result
}

afterEvaluate { project ->
Configuration runtimeConfiguration = project.configurations.getByName('compile')
println runtimeConfiguration
ResolutionResult resolution = runtimeConfiguration.incoming.resolutionResult
// Forces resolve of configuration
ModuleVersionIdentifier module = resolution.getAllComponents().find {
    it.moduleVersion.name.equals("play-services")
}.moduleVersion


def playServicesLibName = toCamelCase("${module.group} ${module.name} ${module.version}")
String prepareTaskName = "prepare${playServicesLibName}Library"
File playServiceRootFolder = project.tasks.find { it.name.equals(prepareTaskName) }.explodedDir


def tmpDir = new File(project.buildDir, 'intermediates/tmp')
tmpDir.mkdirs()
def libFile = new File(tmpDir, "${playServicesLibName}.marker")

def strippedClassFileName = "${playServicesLibName}.jar"
def classesStrippedJar = new File(tmpDir, strippedClassFileName)

def packageToExclude = ["com/google/ads/**",
                        "com/google/android/gms/actions/**",
                        "com/google/android/gms/ads/**",
                        // "com/google/android/gms/analytics/**",
                        "com/google/android/gms/appindexing/**",
                        "com/google/android/gms/appstate/**",
                        "com/google/android/gms/auth/**",
                        "com/google/android/gms/cast/**",
                        "com/google/android/gms/drive/**",
                        "com/google/android/gms/fitness/**",
                        "com/google/android/gms/games/**",
                        "com/google/android/gms/gcm/**",
                        "com/google/android/gms/identity/**",
                        "com/google/android/gms/location/**",
                        "com/google/android/gms/maps/**",
                        "com/google/android/gms/panorama/**",
                        "com/google/android/gms/plus/**",
                        "com/google/android/gms/security/**",
                        "com/google/android/gms/tagmanager/**",
                        "com/google/android/gms/wallet/**",
                        "com/google/android/gms/wearable/**"]

Task stripPlayServices = project.tasks.create(name: 'stripPlayServices', group: "Strip") {
    inputs.files new File(playServiceRootFolder, "classes.jar")
    outputs.dir playServiceRootFolder
    description 'Strip useless packages from Google Play Services library to avoid reaching dex limit'

    doLast {
        def packageExcludesAsString = packageToExclude.join(",")
        if (libFile.exists()
                && libFile.text == packageExcludesAsString
                && classesStrippedJar.exists()) {
            println "Play services already stripped"
            copy {
                from(file(classesStrippedJar))
                into(file(playServiceRootFolder))
                rename { fileName ->
                    fileName = "classes.jar"
                }
            }
        } else {
            copy {
                from(file(new File(playServiceRootFolder, "classes.jar")))
                into(file(playServiceRootFolder))
                rename { fileName ->
                    fileName = "classes_orig.jar"
                }
            }
            tasks.create(name: "stripPlayServices" + module.version, type: Jar) {
                destinationDir = playServiceRootFolder
                archiveName = "classes.jar"
                from(zipTree(new File(playServiceRootFolder, "classes_orig.jar"))) {
                    exclude packageToExclude
                }
            }.execute()
            delete file(new File(playServiceRootFolder, "classes_orig.jar"))
            copy {
                from(file(new File(playServiceRootFolder, "classes.jar")))
                into(file(tmpDir))
                rename { fileName ->
                    fileName = strippedClassFileName
                }
            }
            libFile.text = packageExcludesAsString
        }
    }
}

project.tasks.findAll {
    it.name.startsWith('prepare') && it.name.endsWith('Dependencies')
}.each { Task task ->
    task.dependsOn stripPlayServices
}
project.tasks.findAll { it.name.contains(prepareTaskName) }.each { Task task ->
    stripPlayServices.mustRunAfter task
}

}

Em seguida, aplique este script em seu build.gradle, como este

apply plugin: 'com.android.application'
apply from: 'strip_play_services.gradle'
Roman Nazarevych
fonte
1

Se estiver usando o Google Play Services, você deve saber que ele adiciona mais de 20k métodos. Como já mencionado, o Android Studio tem a opção de inclusão modular de serviços específicos, mas os usuários presos ao Eclipse precisam assumir a modularização em suas próprias mãos :(

Felizmente, existe um script de shell que torna o trabalho bastante fácil. Basta extrair para o diretório jar do google play services, editar o arquivo .conf fornecido conforme necessário e executar o script de shell.

Um exemplo de seu uso está aqui .

Tom
fonte
1

Se estiver usando o Google Play Services, você deve saber que ele adiciona mais de 20k métodos. Como já mencionado, o Android Studio tem a opção de inclusão modular de serviços específicos, mas os usuários presos ao Eclipse precisam assumir a modularização em suas próprias mãos :(

Felizmente, existe um script de shell que torna o trabalho bastante fácil. Basta extrair para o diretório jar do google play services, editar o arquivo .conf fornecido conforme necessário e executar o script de shell.

Um exemplo de seu uso está aqui.

Como ele disse, substituí compile 'com.google.android.gms:play-services:9.0.0'apenas pelas bibliotecas de que precisava e funcionou.

Tamir Gilany
fonte