91

我有一个相当大的 Android 应用程序,它依赖于许多库项目。Android 编译器对每个 .dex 文件有 65536 个方法的限制,我已经超过了这个数字。

当你达到方法限制时,基本上有两条路径可以选择(至少我知道)。

1)缩小你的代码

2)构建多个dex文件(见this blog post

我查看了两者并试图找出导致我的方法计数如此之高的原因。Google Drive API 占据了 Guava 依赖项的最大部分,超过 12,000 个。Drive API v2 的库总数超过 23,000 个!

我想我的问题是,你认为我应该怎么做?我应该删除 Google Drive 集成作为我的应用程序的一项功能吗?有没有办法缩小 API(是的,我使用 proguard)?我应该走多 dex 路线(这看起来相当痛苦,尤其是处理第三方 API)?

4

12 回答 12

69

看起来谷歌终于实现了一个解决方法/修复超过 dex 文件的 65K 方法限制。

关于 65K 参考限制

Android 应用程序 (APK) 文件包含 Dalvik Executable (DEX) 文件形式的可执行字节码文件,其中包含用于运行您的应用程序的编译代码。Dalvik Executable 规范将单个 DEX 文件中可以引用的方法总数限制为 65,536 个,包括 Android 框架方法、库方法和您自己代码中的方法。超过此限制要求您将应用程序构建过程配置为生成多个 DEX 文件,称为 multidex 配置。

Android 5.0 之前的 Multidex 支持

Android 5.0 之前的平台版本使用 Dalvik 运行时来执行应用程序代码。默认情况下,Dalvik 将应用程序限制为每个 APK 单个 classes.dex 字节码文件。为了绕过这个限制,您可以使用multidex 支持库,它成为您应用程序的主要 DEX 文件的一部分,然后管理对其他 DEX 文件及其包含的代码的访问。

对 Android 5.0 及更高版本的 Multidex 支持

Android 5.0 及更高版本使用名为 ART 的运行时,它本机支持从应用程序 APK 文件加载多个 dex 文件。ART 在应用程序安装时执行预编译,扫描 classes(..N).dex 文件并将它们编译为单个 .oat 文件以供 Android 设备执行。有关 Android 5.0 运行时的更多信息,请参阅ART 简介

请参阅:使用超过 65K 方法构建应用程序


Multidex 支持库

该库支持使用多个 Dalvik Executable (DEX) 文件构建应用程序。引用超过 65536 个方法的应用程序需要使用 multidex 配置。有关使用 multidex 的更多信息,请参阅使用超过 65K 方法构建应用程序

下载 Android 支持库后,此库位于 /extras/android/support/multidex/ 目录中。该库不包含用户界面资源。要将其包含在您的应用程序项目中,请按照添加没有资源的库的说明进行操作。

该库的 Gradle 构建脚本依赖标识符如下:

com.android.support:multidex:1.0.+ 此依赖表示法指定发布版本 1.0.0 或更高版本。


您仍然应该通过积极使用 proguard 并查看您的依赖项来避免达到 65K 方法限制。

于 2014-11-03T22:45:37.923 回答
53

您可以为此使用 multidex 支持库,以启用multidex

1)将其包含在依赖项中:

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

2)在您的应用中启用它:

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

3)如果您的应用程序有一个应用程序类,则像这样覆盖attachBaseContext方法:

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

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

4)如果您的应用程序没有应用程序类,则在清单文件中将android.support.multidex.MultiDexApplication注册为您的应用程序。像这样:

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

它应该可以正常工作!

于 2015-04-12T18:02:06.437 回答
32

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

“从 Google Play 服务的 6.5 版开始,您将能够从许多单独的 API 中进行选择,并且您可以看到”

...

“这将传递地包括在所有 API 中使用的‘基础’库。”

这是个好消息,例如,对于一个简单的游戏,您可能只需要base,games或许drive.

“API 名称的完整列表如下。更多详细信息可以在 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: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
于 2015-01-19T05:39:23.183 回答
9

在 6.5 之前的 Google Play 服务版本中,您必须将整个 API 包编译到您的应用程序中。在某些情况下,这样做会使您的应用程序中的方法数量(包括框架 API、库方法和您自己的代码)保持在 65,536 个限制以下变得更加困难。

从 6.5 版开始,您可以有选择地将 Google Play 服务 API 编译到您的应用程序中。例如,要仅包含 Google Fit 和 Android Wear API,请在 build.gradle 文件中替换以下行:

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

这些行:

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

更多参考,您可以点击这里

于 2015-02-27T08:31:38.770 回答
7

使用 proguard 来减轻您的 apk,因为未使用的方法不会出现在您的最终构建中。仔细检查您的 proguard 配置文件中是否有以下内容,以便将 proguard 与 guava 一起使用(如果您已经有了这个,我很抱歉,在撰写本文时还不知道):

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

另外,如果你使用的是 ActionbarSherlock,切换到 v7 appcompat 支持库也会大大减少你的方法数(根据个人经验)。说明位于:

于 2013-12-20T17:21:27.793 回答
7

您可以使用Jar Jar Links来缩小 Google Play Services 等庞大的外部库(16K 方法!)

在您的情况下,您将只从 Google Play Services jar 中提取除common internaldrive包之外的所有内容。

于 2014-06-15T13:58:24.217 回答
4

对于不使用 Gradle 的 Eclipse 用户,有一些工具可以分解 Google Play Services jar 并仅使用您想要的部分重建它。

我使用dextorer 的 strip_play_services.sh

可能很难确切知道要包含哪些服务,因为存在一些内部依赖关系,但是如果发现缺少所需的东西,您可以从小处着手并添加到配置中。

于 2015-04-24T13:20:14.353 回答
3

我认为从长远来看,在多个 dex 中破坏您的应用程序将是最好的方法。

于 2013-12-09T22:06:21.823 回答
2

多 dex 支持将成为此问题的官方解决方案。有关详细信息,请参阅我的答案

于 2014-10-04T19:39:13.630 回答
2

如果不使用 multidex,这会使构建过程非常缓慢。您可以执行以下操作。正如yahska提到的使用特定的谷歌播放服务库。在大多数情况下,只需要这样做。

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

这是所有可用的包选择性地将 API 编译到您的可执行文件中

如果这还不够,您可以使用 gradle 脚本。将此代码放入文件'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
}

}

然后在你的 build.gradle 中应用这个脚本,像这样

apply plugin: 'com.android.application'
apply from: 'strip_play_services.gradle'
于 2015-04-23T07:42:10.767 回答
1

如果使用 Google Play Services,您可能知道它添加了 20k+ 方法。如前所述,Android Studio 可以选择模块化包含特定服务,但坚持使用 Eclipse 的用户必须自己动手进行模块化:(

幸运的是,有一个 shell 脚本可以让这项工作变得相当容易。只需解压到 google play services jar 目录,根据需要编辑提供的 .conf 文件并执行 shell 脚本。

它的使用示例在这里

于 2015-05-28T03:19:44.543 回答
1

如果使用 Google Play Services,您可能知道它添加了 20k+ 方法。如前所述,Android Studio 可以选择模块化包含特定服务,但坚持使用 Eclipse 的用户必须自己动手进行模块化:(

幸运的是,有一个 shell 脚本可以让这项工作变得相当容易。只需解压到 google play services jar 目录,根据需要编辑提供的 .conf 文件并执行 shell 脚本。

它的使用示例在这里。

就像他说的那样,我compile 'com.google.android.gms:play-services:9.0.0'只替换了我需要的库并且它起作用了。

于 2016-05-22T13:52:39.220 回答