如何缩小代码 – 65k的方法限制在dex

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

基本上有两条path可以select(至less我知道),当你达到方法限制时。

1)缩小你的代码

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

我仔细观察了两者,试图找出导致我的方法数量如此之高的原因。 Google Drive API占用了12,000多个Guava依赖项的最大块。 Drive API v2的总库数达到23,000!

我想我的问题是,你认为我应该怎么做? 我应该删除Google云端硬盘集成作为我的应用程序的function? 有没有办法缩小API(是的,我使用proguard)? 我应该去多个dex路线(看起来相当痛苦,尤其是与第三方API打交道)?

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

关于65K参考限制

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

Android 5.0之前的Multidex支持

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

适用于Android 5.0及更高版本的Multidex支持

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

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


Multidex支持库

这个库为使用多个Dalvik Executable(DEX)文件构build应用程序提供了支持。 引用超过65536个方法的应用程序需要使用multidexconfiguration。 有关使用multidex的更多信息,请参见使用超过65K方法构build应用程序 。

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

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

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


您仍然应该避免通过主动使用proguard并检查您的依赖性来达到65K的方法限制。

您可以使用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> 

它应该工作正常!

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

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

“这将会过渡性地包含在所有API中使用的”基础“库。”

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

“API名称的完整列表如下,更多详细信息可以在Android开发人员网站上find。

  • com.google.android.gms:播放服务基地:87年6月5日
  • com.google.android.gms:播放服务的广告:87年6月5日
  • com.google.android.gms:播放服务,appindexing:87年6月5日
  • com.google.android.gms:播放服务,地图:87年6月5日
  • com.google.android.gms:播放服务地点:87年6月5日
  • com.google.android.gms:播放服务的健身:87年6月5日
  • com.google.android.gms:播放服务全景:87年6月5日
  • com.google.android.gms:播放服务驱动的:87年6月5日
  • com.google.android.gms:游戏服务游戏:87年6月5日
  • com.google.android.gms:播放服务钱包:87年6月5日
  • com.google.android.gms:播放服务身份:87年6月5日
  • com.google.android.gms:播放服务铸:87年6月5日
  • com.google.android.gms:播放服务加:87年6月5日
  • com.google.android.gms:播放服务,APPSTATE:87年6月5日
  • com.google.android.gms:播放服务耐磨:87年6月5日
  • com.google.android.gms:播放服务,所有磨损:87年6月5日

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

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

 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' 

如需更多参考,请点击这里

使用proguard减轻你的apk作为未使用的方法将不会在您的最终构build。 请仔细检查你的proguardconfiguration文件中是否使用了带有番石榴的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支持库也将大大减less你的方法(基于个人经验)。 说明位于:

你可以使用Jar Jar Links来缩减像Google Play Services这样庞大的外部库(16K方法!)

在你的情况下,你只会从谷歌播放服务jar,除了common internaldrive子软件包撕裂一切。

对于不使用Gradle的Eclipse用户,有一些工具可以分解Google Play服务jar并仅使用所需的部分重新构build它。

我使用dextorer使用strip_play_services.sh 。

由于存在一些内部依赖关系,因此可能很难确切知道要包含哪些服务,但是如果事实certificate缺less所需的内容,则可以从小规模启动并添加到configuration中。

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

Multi-dex支持将成为这个问题的官方解决scheme。 在这里看到我的答案的细节。

如果不使用使构build过程非常缓慢的multidex。 您可以执行以下操作。 正如yahska提到使用特定的谷歌播放服务库。 对于大多数情况下只有这是必要的。

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

这里是所有可用的包有select地编译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' 

如果使用Google Play服务,您可能会知道它会添加20k +方法。 正如已经提到的,Android Studio可以select模块化包含特定的服务,但用户坚持使用Eclipse必须把模块化到自己手中:(

幸运的是有一个shell脚本使得这个工作非常简单。 只需解压到google play services jar目录,根据需要编辑提供的.conf文件并执行shell脚本。

这是一个使用的例子。

如果使用Google Play服务,您可能会知道它会添加20k +方法。 正如已经提到的,Android Studio可以select模块化包含特定的服务,但用户坚持使用Eclipse必须把模块化到自己手中:(

幸运的是有一个shell脚本使得这个工作非常简单。 只需解压到google play services jar目录,根据需要编辑提供的.conf文件并执行shell脚本。

这是一个使用的例子。

就像他所说的,我只是用我需要的库来替代compile 'com.google.android.gms:play-services:9.0.0'