Android 突破 DEX 文件的 64K 方法数限制

Trista98E 7年前
   <p>随着安卓平台的不断发展与壮大,市场上大而全的应用比比皆是,产品需求的变更累积和UI交互的极致追求,除了 resources 文件的俱增,在 Android Project 中依赖的 Library 和 自己写的 Java 代码也会越来越多。这些变化,除了会导致打包出的 APK 文件越来越大之外,当项目中java代码包含的方法数(method count)超出一个峰值时,编译过程中就会出现如下错误:</p>    <p>较早版本的编译系统中,错误内容如下:</p>    <pre>  <code class="language-java">Conversion to Dalvik format failed:  Unable to execute dex: method ID not in [0, 0xffff]: 65536</code></pre>    <p>而在新版编译系统中,是这样:</p>    <pre>  <code class="language-java">trouble writing output:  Too many field references: 131000; max is 65536.  You may try using --multi-dex option.</code></pre>    <p>尽管在不同版本的编译系统中显示的错误内容不尽相同,但内容中都提到了一个具体的数字:65536,这个数字也是本文要讲到的核心内容:Android 64K Method Counts Limit 的峰值。</p>    <h2>Android 64K Method Counts Limit</h2>    <p>Android Project 经过编译打包,其中的Java代码(包括Library)转化为DEX格式的字节码文件,这是Android 5.0之前的 Dalvik 虚拟机决定的(5.0之后改为 ART 虚拟机),并且采用 short 类型引用 DEX 文件中的 method,这也为method数量的峰值大小埋下了隐患。short 类型能够表示的最大值是 65536,也就说单个 DEX 文件中最多只有 65536 个 method 能够得到引用,如果代码执行了超出部分的 method 引用,自然会报错,如 methodNotFound 等。1K 等于 1024,65536 刚好是 64K,为了便于称呼和使用,就将这个限制规则统称为 64K 方法数的引用限制。</p>    <p>为了解决 64K 方法数限制的问题,我们可以在项目中使用 multidex 配置,当项目中的方法数(包括:Android framework,library 和我们自己写的代码)超过 64K 时,编译系统会自动编译出多个 DEX 文件。</p>    <h2>Multidex Support</h2>    <p>Android 5.0 之前,安卓系统采用的是 Dalvik 虚拟机,采用的是JIT技术(Just-in-time compilation,即时编译,运行时编译DEX字节码文件,这也是以前为什么安卓手机用户总是诟病Android系统比iOS系统运行卡顿的原因),限制每个APK文件只能包含一个 DEX 文件(即 classes.dex )。为了绕开这个限制,Google给我们提供了 multidex support library 兼容包,帮助我们实现应用程序加载多个DEX文件,并且这个兼容包作为程序的主DEX文件,管理者其他DEX文件的访问。</p>    <p>注意:由于Instant Run 机制利用的就是 multidex 原理,当项目中 minSdkVersion 参数设置为20或者更小,并且运行在 Android 4.4 (API 20) 或更低版本的设备中时, Instant Run 将失效。</p>    <p>Android 5.0之后,安卓系统改用了ART虚拟机(Android RunTime),采用的是OAT技术(Ahead-of-time,预编译,在应用安装的时候扫描应用中的所有DEX文件,并编译成一个 .oat 格式的文件供安卓设备执行,所以相比Dalvik虚拟机下的应用,安装时间较长)。因此可以理解为,使用ART虚拟机下的安卓系统自动支持APK文件中多个DEX的加载。</p>    <p>注意:使用 Instant Run 时,如果项目中的 minSdkVersion 参数设为21或更高版本,Android Studio编译运行时会自动使应用支持multidex。但 Instant Run 仅仅作用于debug版本,我们依然需要给release版本配置multidex来避开64K方法数的限制。</p>    <h2>Config for Multidex With Gradle</h2>    <p>Android Gradle 插件在 Android SDK Build Tools 21.1 及更高版本的编译工具上支持multidex作为编译配置的一部分,所以确保我们的Android SDK Build Tools tools已经更新至21.1或更高版本,然后再来配置应用的multidex部分。</p>    <p>第一步,修改 app/build.grale 文件,使项目能够使用multidex:</p>    <pre>  <code class="language-java">android {        compileSdkVersion 21      buildToolsVersion "21.1.0"        defaultConfig {          ...            // Enabling multidex support.          multiDexEnabled true      }      ...  }    dependencies {    compile 'com.android.support:multidex:1.0.0'  }</code></pre>    <p>第二步,修改 AndroidManifest.xml 文件,引用 MultiDexApplication 类:</p>    <pre>  <code class="language-java"><?xml version="1.0" encoding="utf-8"?>  <manifest xmlns:android="http://schemas.android.com/apk/res/android"      package="com.yifeng.mdstudysamples">      <application          ...          android:name="android.support.multidex.MultiDexApplication">          ...      </application>  </manifest></code></pre>    <p>有时候,你可能还需要改变一下 javaMaxHeapSize 的大小:</p>    <pre>  <code class="language-java">android {      dexOptions {          javaMaxHeapSize "4g"      }  }</code></pre>    <p>添加这些配置后,编译工具会构建出一个主 DEX 文件(classes.dex)和其他附属 DEX 文件(classes2.dex,classes3.dex 等,如果需要的话),编译系统会将他们打包到 Apk 文件中。</p>    <p>注意:一般我们会在项目中自定义一个继承自 Application 的类,此时就需要重写 attachBaseContext() 方法,并在该方法里面调用 MultiDex.install(this) 来支持multidex,可参考:MultiDexApplication</p>    <h2>Optimizing Multidex Development Builds</h2>    <p>multidex 配置下的应用,编译系统需要经过复杂的 DEX 分割运算,导致增加项目的编译时间,从而影响开发人员的开发效率。我们可以使用productFlavors 构建开发环境和正式环境的不同 flavors 来优化 multidex 的长时间编译问题。</p>    <p>对于development flavor,设置 minSdkVersion 值为21,运行在Android 5.0以上版本的设备中,使用 ART-supported 格式生成 multidex 的速度要快得多。对于 release flavor, minSdkVersion 值则设为应用实际支持的版本,编译系统耗费较长的时间来生成适配多设备的multidex APK文件。如:</p>    <pre>  <code class="language-java">android {      productFlavors {          // Define separate dev and prod product flavors.          dev {              // dev utilizes minSDKVersion = 21 to allow the Android gradle plugin              // to pre-dex each module and produce an APK that can be tested on              // Android Lollipop without time consuming dex merging processes.              minSdkVersion 21          }          prod {              // The actual minSdkVersion for the application.              minSdkVersion 14          }      }            ...      buildTypes {          release {              runProguard true              proguardFiles getDefaultProguardFile('proguard-android.txt'),                                                   'proguard-rules.pro'          }      }  }  dependencies {    compile 'com.android.support:multidex:1.0.0'  }</code></pre>    <p>这样,在开发阶段,使用 devDebug 类型的变种 app,取消混淆,支持 multidex,并且运行在 5.0 及以上版本的设备中,能够加快编译过程。</p>    <h2>strings count limit</h2>    <p>前面我们说完单一 dex 文件的方法数限制,事实上,还有一个字符串数量限制。如果项目没有使用 multidex 支持的话,当 strings 超出一定限制,编译过程也会出错:</p>    <pre>  <code class="language-java">Dex: Error converting bytecode to dex:  Cause: com.android.dex.DexIndexOverflowException: Cannot merge new index 65868 into a non-jumbo instruction!</code></pre>    <p>不同项目编译过程中报错信息里的具体数字可能不同。Dex 文件中出现的 string 默认是 4 个字节即 16 位大小的 int 类型的数字引用使用的,即单个 Dex 文件最多只能引用 2^16 个 strings,当你的项目中出现超过这个最大数字的字符串引用,而又没有使用 multidex 支持,编译过程便会出错。</p>    <p>对于这种情况,除了使用 multidex,还有另外一种解决方案:jumboMode。这个模式允许单个 Dex 文件支持到 32 为大小的 strings 引用,即 2^32 的引用峰值。使用方法是,在工程 app 模块下 build.gradle 文件的 android 配置下添加:</p>    <pre>  <code class="language-java">dexOptions {          jumboMode true  }</code></pre>    <p>注意:虽然单个 Dex 文件中 strings 数量限制与 method 数量限制非常相似,但是如果项目方法数超过 64K, 我们还是需要使用 multidex 来解决,注意区分。有关这方面的更多详细介绍,请参考Dalvik bytecode。</p>    <h2>Methods Count Statistics</h2>    <p>尽管安卓系统支持multidex,我们还是要学会分析我们的应用,查看各个部分的方法数,减少冗余方法。这里推荐几个工具,帮助我们分析。</p>    <h2><a href="/misc/goto?guid=4959748506816820739" rel="nofollow,noindex">Library Methods count</a></h2>    <p>一个在线统计 Android Library 方法数的网站,能够统计出 Android 领域常见 libraries 的方法数、JAR 文件和 DEX 文件大小,并且能够选择不同版本,以图表的形式展示出来。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/638fd5f18f85f2e54e7a16a1b9bf5244.png"></p>    <p>http://www.methodscount.com/</p>    <p>该网站也提供了Android Studio的插件,帮助我们分析项目中所依赖的libraries的方法数,如图所示:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/91ad4e7441efe4d8de7df44c6eacdece.png"></p>    <p>methodscount-samples-02.png</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/7b0dba7439527f4a422f7555d53db5e9.png"></p>    <p>methodscount-samples-03.png</p>    <h2><a href="/misc/goto?guid=4959748506961751162" rel="nofollow,noindex">Apk Method Count</a></h2>    <p>一个在线统计 APK 文件方法数的开源项目,只需要将需要分析的APK文件拖拽上传至此,即可得到分析结果,如图:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/eeb122cd31c3171f179bbeaba9422c2d.png"></p>    <p>apk-method-count-samples.png</p>    <h2><a href="/misc/goto?guid=4959748507078909447" rel="nofollow,noindex">Android Studio APK Analyzer</a></h2>    <p>最后,要重磅推荐Android Studio自带的APK Analyzer,功能齐全,使用方便,绝对是安卓开发人员分析应用的不二选择。使用 Android Studio APK Analyzer ,我们至少能够做到:</p>    <ul>     <li> <p>查看APK压缩文件中各个子文件的大小(如DEX和resource文件)</p> </li>     <li> <p>理解DEX文件的结构</p> </li>     <li> <p>快速查看APK文件的版本信息(直接查看 AndroidManifest.xml 内容)</p> </li>     <li> <p>直观地比较两个APK文件内容</p> </li>    </ul>    <p style="text-align:center"><img src="https://simg.open-open.com/show/2c08040ebe926b214c2cf25d9e061e5d.png"></p>    <p>Android-Studio-APK-Analyzer-Samples.png</p>    <p>开发阶段使用Android Studio打开一个项目时,有三种方式使用APK Analyzer工具:</p>    <ul>     <li> <p>直接拖拽APK文件到Android Studio的编辑窗口</p> </li>     <li> <p>双击打开项目目录 app/build/outputs/apk/ 下的APK文件</p> </li>     <li> <p>点击菜单栏 Build->Analyse APK... 并选择APK文件</p> </li>    </ul>    <p> </p>    <p> </p>    <p>来自:https://juejin.im/post/591106e0a22b9d00580c9e10</p>    <p> </p>