【问题标题】:How to shrink code - 65k method limit in dex如何收缩代码 - dex 中的 65k 方法限制
【发布时间】:2013-03-06 11:47:25
【问题描述】:

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

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

1) 缩小代码

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

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

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

【问题讨论】:

  • 我碰巧喜欢你的应用。您是否考虑过以伪apk 形式下载所有额外的库?我个人希望看到 Drive 集成
  • Facebook 最近记录了他们的解决方法,以解决他们的 Android 应用程序中似乎几乎相同的问题。可能有用:facebook.com/notes/facebook-engineering/…
  • 开始走多路dex路线。我成功创建了一个辅助 dex 文件来使用 Google Drive。我为任何需要番石榴作为依赖的人感到难过。 :P 这对我来说仍然是一个相当大的问题
  • 方法数怎么算?
  • 这里的一些附加说明:stackoverflow.com/questions/21490382(包括一个实用程序的链接,该实用程序将列出 APK 中的方法引用)。请注意,64K 限制与链接几个 cmets 的 Facebook 问题无关。

标签: android google-drive-api guava dex android-multidex


【解决方案1】:

看起来 Google 终于实施了解决方法/修复超过 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 support library,它变成 应用程序的主要 DEX 文件的一部分,然后管理对 额外的 DEX 文件及其包含的代码。

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

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

见:Building Apps with Over 65K Methods


Multidex 支持库

这个库为构建提供支持 具有多个 Dalvik 可执行 (DEX) 文件的应用程序。引用的应用 使用 multidex 配置需要超过 65536 种方法。 有关使用 multidex 的更多信息,请参阅Building Apps with Over 65K Methods

这个库位于 /extras/android/support/multidex/ 下载 Android 支持库后的目录。这 库不包含用户界面资源。将其包含在 您的应用项目,请按照Adding libraries without resources.的说明进行操作

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

com.android.support:multidex:1.0.+ 这个依赖符号指定 发布版本 1.0.0 或更高版本。


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

【讨论】:

  • +1,为什么当同一个人回答正确答案时,人们不赞成?
  • 最低api等级变为14!
  • 我们编写了一个小的 Gradle 插件,为您提供每个构建的当前方法计数。对我们管理图书馆很有帮助 - github.com/KeepSafe/dexcount-gradle-plugin
【解决方案2】:

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

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

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

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

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

3) 如果您的应用有一个 application 类,则像这样覆盖 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)如果您没有为您的应用程序提供 application 类,请注册 android.support.multidex.MultiDexApplication 作为清单文件中的应用程序。像这样:

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

它应该可以正常工作!

【讨论】:

    【解决方案3】:

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

    “从 Google Play 服务 6.5 版开始,您将能够 从多个单独的 API 中挑选,您可以看到”

    ...

    "这将传递包含使用的“基础”库 跨所有 API。”

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

    “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

    【讨论】:

    • 关于如何在 Eclipse 项目中执行此操作的任何信息?
    • 我还不能升级到那个版本。但是如果你的项目是基于 Maven 的,那么希望你只需要在你的 maven pom 中解决这个问题。
    • @webo80 好吧,这仅在您达到 6.5.87 版本时才有帮助。我想知道 petey 的回答,即 proguard 剥离了未使用的功能。我想知道这是否也涉及第二方库,或者只是你自己的东西。我应该阅读更多关于 proguard 的内容。
    • @BrianWhite 目前唯一的解决方案似乎是用一些外部工具剥离 .jar 文件..
    • @CsabaToth,你救了我!我只添加了上面列表中的一小部分,而不是完整的“com.google.android.gms:play-services”,这很重要!
    【解决方案4】:

    在 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'
    

    更多参考,可以点击here

    【讨论】:

    • 在eclipse中怎么做?
    【解决方案5】:

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

    【讨论】:

    • 这看起来很有希望,但我在运行 ./gradlew :myapp:proguardDevDebug 时得到了 Warning: butterknife.internal.ButterKnifeProcessor: can't find superclass or interface javax.annotation.processing.AbstractProcessor
    • 不过,在开发过程中,proguard 通常不会运行(最后不会在 Eclipse 中运行),因此在进行发布构建之前,您无法从缩减中受益。
    【解决方案6】:

    您可以使用 Jar Jar Links 来缩小像 Google Play 服务这样的巨大外部库(16K 方法!)

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

    【讨论】:

      【解决方案7】:

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

      我使用strip_play_services.sh by dextorer

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

      【讨论】:

        【解决方案8】:

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

        【讨论】:

        • 我正在寻找使用 Gradle 的正确方法:-/ 有什么提示吗?
        【解决方案9】:

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

        【讨论】:

          【解决方案10】:

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

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

          这里是所有可用的包Selectively compiling APIs into your executable

          如果这还不够,您可以使用 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'
          

          【讨论】:

            【解决方案11】:

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

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

            它的一个使用例子是here

            【讨论】:

              【解决方案12】:

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

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

              这里有一个使用示例。

              就像他说的那样,我将 compile 'com.google.android.gms:play-services:9.0.0' 替换为我需要的库并且它有效。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2015-06-18
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2013-07-17
                相关资源
                最近更新 更多