如何缩小代码-dex中的65k方法限制


91

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

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

1)缩小代码

2)构建多个dex文件(请参阅此博客文章

我对两者进行了调查,试图找出导致我的方法计数过高的原因。Google Drive API的Guava依赖项占最大部分,超过12,000。Drive API v2的库总数超过23,000!

我想我的问题是,您认为我应该怎么做?我是否应该删除Google云端硬盘集成作为我的应用程序的功能?有没有一种缩小API的方法(是的,我使用proguard)?我是否应该走多dex路线(看起来很痛苦,尤其是与第三方API的交流)?


2
我碰巧喜欢您的应用。您是否考虑过要以伪apk格式下载所有额外的库?我个人希望看到云端硬盘集成
-JBirdVegas

8
Facebook最近在其Android应用中记录了他们解决几乎相同问题的解决方法。可能是有用的:facebook.com/notes/facebook-engineering/...
鲁本Scratton

4
开始降低多dex路线。我成功创建了一个辅助dex文件,以用于Google云端硬盘。我对需要番石榴为依赖的人感到难过。:P不过,对我来说,这仍然是一个很大的问题
贾里德·拉姆勒

4
您如何计算方法?
Bri6ko 2014年

1
此处提供一些其他说明:stackoverflow.com/questions/21490382(包括指向实用程序的链接,该实用程序将在APK中列出方法引用)。请注意,64K限制与Facebook问题无关,并链接了一些评论。
fadden

Answers:


69

看来Google终于实现了解决方法/修复程序,以超越dex文件的65K方法限制。

关于65K参考限值

Android应用程序(APK)文件包含Dalvik可执行(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可执行文件(DEX)构建应用程序。引用超过65536种方法的应用程序需要使用multidex配置。有关使用multidex的更多信息,请参阅使用65K以上方法构建应用程序

下载Android支持库后,该库位于/ extras / android / support / multidex /目录中。该库不包含用户界面资源。要将其包括在您的应用程序项目中,请遵循有关在没有资源的情况下添加库的说明

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

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


您仍应通过积极使用Proguard并检查依赖项来避免达到65K方法限制。


6
+1,为什么当一个人回答了正确的答案后,人们却不支持正确的答案?
Pacerier 2014年

最小api级别变为14!
Vihaan Verma

5
我们编写了一个Gradle小插件,为您提供每个构建中当前方法的数量。对我们管理图书馆很有帮助-github.com/KeepSafe/dexcount-gradle-plugin
philipp

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>

它应该可以正常工作!


31

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

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

...

“这将可传递地包括在所有API中使用的'基本'库。”

这是个好消息,对于一个简单的游戏,例如,你可能只需要的basegames也许drive

“ API名称的完整列表如下。更多详细信息可以在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

关于如何在Eclipse项目中执行此操作的任何信息?
Brian White

我尚无法升级到该版本。但是,如果您的项目是基于Maven的,那么希望您只需要在maven pom中解决该问题即可。
卡萨托·托斯

@ webo80好吧,这仅在您使用6.5.87之前的版本时才有用。我不知道petey的回答,即proguard会删除未使用的功能。我想知道这是否也涉及第二方库,或者仅涉及您自己的东西。我应该阅读有关proguard的更多信息。
卡萨巴·托斯

@BrianWhite唯一的解决方案,现在似乎会.jar文件与一些外部工具..
milosmns


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'

有关更多参考,请单击此处


如何在日食中做到?
Hardik9850

7

使用proguard减轻apk的负担,因为未使用的方法将不在最终版本中。仔细检查您的proguard配置文件中是否包含以下内容,以将proguard与番石榴配合使用(如果您已经拥有,请道歉,撰写本文时尚不了解):

# 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支持库也会大大减少方法数量(基于个人经验)。说明位于:


这看起来令人发指,但我Warning: butterknife.internal.ButterKnifeProcessor: can't find superclass or interface javax.annotation.processing.AbstractProcessor在跑步时得到了./gradlew :myapp:proguardDevDebug
ericn 2015年

1
但是,在开发过程中,通常不会运行proguard(最后不在Eclipse中运行),因此您只有在进行发行版本构建后才能从收缩中受益。
布赖恩·怀特

7

您可以使用Jar Jar链接来缩小庞大的外部库,例如Google Play服务(16K方法!)

就您而言,您将只从Google Play服务jar中删除所有内容,common internal以及drive子包。


4

对于不使用Gradle的Eclipse用户,有一些工具可以分解Google Play服务jar,并仅用所需的部件对其进行重建。

我使用dextorer的strip_play_services.sh

可能很难确切知道要包括哪些服务,因为存在一些内部依赖关系,但是如果事实证明缺少所需内容,则可以从小处着手并添加到配置中。


3

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


2
我正在寻找使用Gradle的正确方法:-/有任何提示吗?
伊万·莫尔吉洛


2

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

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'

1

如果使用Google Play服务,您可能会知道它增加了20k多种方法。如前所述,Android Studio可以选择模块化包含特定服务,但是对Eclipse感兴趣的用户必须自己掌握模块化:

幸运的是,有一个shell脚本可以使这项工作相当容易。只需解压缩到google play服务jar目录,根据需要编辑提供的.conf文件,然后执行shell脚本即可。

这里是使用示例。


1

如果使用Google Play服务,您可能会知道它增加了20k多种方法。如前所述,Android Studio可以选择模块化包含特定服务,但是对Eclipse感兴趣的用户必须自己掌握模块化:

幸运的是,有一个shell脚本可以使这项工作相当容易。只需提取到Google Play服务jar目录,根据需要编辑提供的.conf文件,然后执行Shell脚本即可。

这里是使用示例。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.