Cómo encoger el código - límite del método 65k en dex


91

Tengo una aplicación de Android bastante grande que se basa en muchos proyectos de biblioteca. El compilador de Android tiene una limitación de 65536 métodos por archivo .dex y estoy superando ese número.

Básicamente, hay dos caminos que puede elegir (al menos que yo sepa) cuando alcanza el límite del método.

1) Reduzca su código

2) Cree varios archivos dex ( consulte esta publicación de blog )

Miré en ambos y traté de averiguar qué estaba causando que el recuento de mi método fuera tan alto. La API de Google Drive toma la mayor parte con la dependencia de Guava en más de 12,000. El total de bibliotecas para Drive API v2 supera las 23.000.

Mi pregunta, supongo, es, ¿qué crees que debería hacer? ¿Debo eliminar la integración de Google Drive como una función de mi aplicación? ¿Hay alguna forma de reducir la API (sí, uso proguard)? ¿Debería seguir la ruta dex múltiple (que parece bastante dolorosa, especialmente al tratar con API de terceros)?


2
Me encanta tu aplicación. ¿Ha pensado en realizar una descarga obligatoria de todas las librerías adicionales en una pseudo apkforma? Personalmente, me gustaría ver la integración de Drive
JBirdVegas

8
Facebook documentó recientemente su solución para lo que parece ser un problema casi idéntico en su aplicación de Android. Puede ser útil: facebook.com/notes/facebook-engineering/…
Reuben Scratton

4
Comenzando a seguir la ruta de múltiples dex. Creé con éxito un archivo dex secundario para trabajar con Google Drive. Me siento mal por cualquiera que necesite la guayaba como dependencia. : P Sin embargo, sigue siendo un gran problema para mí
Jared Rummler

4
¿Cómo se cuentan los métodos?
Bri6ko

1
Algunas notas adicionales aquí: stackoverflow.com/questions/21490382 (incluido un enlace a una utilidad que enumerará las referencias del método en un APK). Tenga en cuenta que el límite de 64K no está relacionado con el problema de Facebook vinculado a algunos comentarios.
fadden

Respuestas:


69

Parece que Google finalmente ha implementado una solución / solución para superar el límite de método de 65K de archivos dex.

Acerca del límite de referencia de 65K

Los archivos de aplicaciones de Android (APK) contienen archivos de código de bytes ejecutables en forma de archivos Dalvik Executable (DEX), que contienen el código compilado que se utiliza para ejecutar su aplicación. La especificación Dalvik Executable limita la cantidad total de métodos a los que se puede hacer referencia dentro de un solo archivo DEX a 65,536, incluidos los métodos del marco de trabajo de Android, los métodos de biblioteca y los métodos en su propio código. Superar este límite requiere que configure el proceso de compilación de su aplicación para generar más de un archivo DEX, conocido como configuración multidex.

Compatibilidad con Multidex antes de Android 5.0

Las versiones de la plataforma anteriores a Android 5.0 utilizan el tiempo de ejecución de Dalvik para ejecutar el código de la aplicación. De forma predeterminada, Dalvik limita las aplicaciones a un solo archivo de código de bytes classes.dex por APK. Para sortear esta limitación, puede utilizar la biblioteca de soporte multidex , que se convierte en parte del archivo DEX principal de su aplicación y luego administra el acceso a los archivos DEX adicionales y el código que contienen.

Soporte Multidex para Android 5.0 y superior

Android 5.0 y superior utiliza un tiempo de ejecución llamado ART que admite de forma nativa la carga de varios archivos dex desde los archivos APK de la aplicación. ART realiza una compilación previa en el momento de la instalación de la aplicación, que busca archivos de clases (.. N) .dex y los compila en un solo archivo .oat para su ejecución en el dispositivo Android. Para obtener más información sobre el tiempo de ejecución de Android 5.0, consulte Introducción a ART .

Consulte: Creación de aplicaciones con más de 65.000 métodos


Biblioteca de soporte de Multidex

Esta biblioteca brinda soporte para crear aplicaciones con múltiples archivos Dalvik Executable (DEX). Las aplicaciones que hacen referencia a más de 65536 métodos deben utilizar configuraciones multidex. Para obtener más información sobre el uso de multidex, consulte Creación de aplicaciones con más de 65.000 métodos .

Esta biblioteca se encuentra en el directorio / extras / android / support / multidex / después de descargar las bibliotecas de soporte de Android. La biblioteca no contiene recursos de interfaz de usuario. Para incluirlo en su proyecto de aplicación, siga las instrucciones para Agregar bibliotecas sin recursos.

El identificador de dependencia del script de compilación de Gradle para esta biblioteca es el siguiente:

com.android.support:multidex:1.0.+ Esta notación de dependencia especifica la versión de lanzamiento 1.0.0 o superior.


Aún debe evitar alcanzar el límite de métodos de 65K usando activamente proguard y revisando sus dependencias.


6
+1, ¿Por qué las personas no votan a favor de las respuestas correctas cuando las responde la misma persona?
Pacerier

¡El nivel mínimo de API se convierte en 14!
Vihaan Verma

5
Escribimos un pequeño complemento de Gradle para darle el recuento de su método actual en cada compilación. Nos ayudó
philipp

53

puede usar la biblioteca de soporte multidex para eso, para habilitar multidex

1) inclúyelo en dependencias:

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

2) Habilítelo en su aplicación:

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

3) si tiene una clase de aplicación para su aplicación, anule el 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) Si no tiene una clase de aplicación para su aplicación, registre android.support.multidex.MultiDexApplication como su aplicación en su archivo de manifiesto. Me gusta esto:

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

¡y debería funcionar bien!


31

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

"A partir de la versión 6.5 de los servicios de Google Play, podrá elegir entre varias API individuales y podrá ver"

...

"Esto incluirá de forma transitiva las bibliotecas 'base', que se utilizan en todas las API".

Esta es una buena noticia, para un juego simple, por ejemplo, probablemente solo necesite el base, gamesy tal vez drive.

"La lista completa de nombres de API se encuentra a continuación. Puede encontrar más detalles en el sitio para desarrolladores de Android:

  • 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: juegos-servicios-juegos: 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

¿Alguna información sobre cómo hacer eso dentro de un proyecto Eclipse?
Brian White

Todavía no estoy en condiciones de actualizar a esa versión. Pero si su proyecto está basado en Maven, entonces con suerte solo tendrá que resolverlo en su maven pom.
Csaba Toth

@ webo80 Bueno, esto solo ayuda si tiene la versión 6.5.87. Me pregunto acerca de la respuesta de Petey, que proguard elimina las funciones no utilizadas. Me pregunto si eso también involucra bibliotecas de segunda parte, o solo tus propias cosas. Debería leer más sobre proguard.
Csaba Toth

@BrianWhite La única solución por ahora parece eliminar el archivo .jar con alguna herramienta externa ..
milosmns

Terminé usando esta herramienta: gist.github.com/dextorer/a32cad7819b7f272239b
Brian White

9

En las versiones de los servicios de Google Play anteriores a la 6.5, tenía que compilar todo el paquete de API en su aplicación. En algunos casos, hacerlo hizo que fuera más difícil mantener la cantidad de métodos en su aplicación (incluidas las API de marco, los métodos de biblioteca y su propio código) por debajo del límite de 65.536.

A partir de la versión 6.5, puede compilar selectivamente las API del servicio Google Play en su aplicación. Por ejemplo, para incluir solo las API de Google Fit y Android Wear, reemplace la siguiente línea en su archivo build.gradle:

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

con estas líneas:

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

para más referencias, puede hacer clic aquí


¿Cómo hacerlo en un eclipse?
Hardik9850

7

Use proguard para aligerar su apk ya que los métodos que no se utilizan no estarán en su compilación final. Verifique que tiene lo siguiente en su archivo de configuración proguard para usar proguard con guava (mis disculpas si ya tiene esto, no se sabía al momento de escribir este artículo):

# 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.**

Además, si está usando ActionbarSherlock, cambiar a la biblioteca de soporte de appcompat v7 también reducirá mucho el recuento de métodos (según la experiencia personal). Las instrucciones se encuentran:


esto parece prometedor pero lo conseguí Warning: butterknife.internal.ButterKnifeProcessor: can't find superclass or interface javax.annotation.processing.AbstractProcessorcuando corría./gradlew :myapp:proguardDevDebug
ericn

1
Sin embargo, durante el desarrollo, proguard generalmente no se ejecuta (al menos no con Eclipse), por lo que no puede beneficiarse de la reducción hasta que realice una versión de lanzamiento.
Brian White

7

Podrías usar Jar Jar Links para reducir enormes bibliotecas externas como Google Play Services (¡métodos de 16K!)

En su caso, simplemente extraerá todo del jar de Google Play Services excepto common internaly drivelos subpaquetes.


4

Para los usuarios de Eclipse que no usan Gradle, existen herramientas que descompondrán el frasco de Google Play Services y lo reconstruirán solo con las partes que desee.

Yo uso strip_play_services.sh de dextorer .

Puede ser difícil saber exactamente qué servicios incluir porque hay algunas dependencias internas, pero puede comenzar poco a poco y agregar a la configuración si resulta que faltan cosas necesarias.


3

Creo que, a la larga, romper tu aplicación en múltiples dex sería la mejor manera.


2
Estoy buscando una forma adecuada de hacer esto con Gradle: - / ¿Alguna pista?
Ivan Morgillo


2

Si no se usa multidex, lo que hace que el proceso de construcción sea muy lento. Puede hacer lo siguiente. Como mencionó yahska, use la biblioteca de servicios de Google Play específica. En la mayoría de los casos, solo se necesita esto.

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

Aquí están todos los paquetes disponibles compilan API selectivamente en su ejecutable

Si esto no es suficiente, puede usar el script gradle. Pon este código en el archivo '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
}

}

Luego aplique este script en su build.gradle, así

apply plugin: 'com.android.application'
apply from: 'strip_play_services.gradle'

1

Si usa Google Play Services, es posible que sepa que agrega más de 20k métodos. Como ya se mencionó, Android Studio tiene la opción de inclusión modular de servicios específicos, pero los usuarios que se quedan con Eclipse tienen que tomar la modularización en sus propias manos :(

Afortunadamente hay un script de shell que facilita bastante el trabajo. Simplemente extraiga al directorio jar de los servicios de Google Play, edite el archivo .conf suministrado según sea necesario y ejecute el script de shell.

Un ejemplo de su uso está aquí .


1

Si usa Google Play Services, es posible que sepa que agrega más de 20k métodos. Como ya se mencionó, Android Studio tiene la opción de inclusión modular de servicios específicos, pero los usuarios que se quedan con Eclipse tienen que tomar la modularización en sus propias manos :(

Afortunadamente, hay un script de shell que facilita bastante el trabajo. Simplemente extraiga al directorio jar de los servicios de Google Play, edite el archivo .conf proporcionado según sea necesario y ejecute el script de shell.

Un ejemplo de su uso está aquí.

Como dijo, reemplacé compile 'com.google.android.gms:play-services:9.0.0'solo con las bibliotecas que necesitaba y funcionó.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.