如何配置方法数超过64K的应用

如何配置方法数超过64K的应用

随着Android平台的继续成长,Android应用的大小也在变大。当一个应用及其引用的库到达一定的规模,在编译应用时就会遇到构建错误,这表示此App已经达到了Android构建系统的某个限制。在早期的构建系统版本中,此错误表现如下:

Conversion to Dalvik format failed:
Unable to execute dex: method ID not in [0, 0xffff]: 65536

现在的Android构建系统可能会报另一个不同的错误,但它们表示的都是同一个问题:

trouble writing output:
Too many field references: 131000; max is 65536.
You may try using --multi-dex option.

这些错误的发生条件都指向了一个共同的数字:65536,这个数字非常重要,它代表了一个单独的DEX字节码文件可执行的引用总数。本文将说明如何通过将一个应用配置为multidex的方式来解决该问题,配置之后,该应用将构建和读取多个DEX文件。

关于64K的引用限制

Android 应用(APK)中以Dalvik虚拟机可执行文件(即DEX)的形式包含了可执行的字节码文件,DEX文件中编译后的代码则用于运行App功能。DEX规范做了如下限制:一个单独的DEX文件可引用的最多方法数为65636——包括Android框架中的方法、第三方库的方法以及自己代码中的方法。在计算机科学的表述中,K表示1024(或2^10)。因为65536即64x1024,所以65536的方法数限制也叫做64K引用限制。

Android 5.0之前的版本使用Mutidex

Android 5.0(API level 21)之前的版本使用Dalvik虚拟机来执行应用代码,默认情况下,Dalvik限制每个APK中只有一个classes.dex字节码文件。为了绕过这个限制,可以使用支持库multidex support library,它将作为主DEX的一部分,用来管理对额外的DEX文件及其代码的访问。

说明:如果为minSdkVersion=20或更低的应用配置multidex,并且运行目标设备为Android 4.4(API level 20)或更低,此时Android Studio将禁止使用Instant Run

Android 5.0及之后的版本使用Multidex

Android 5.0 (API level 21)及更高版本使用ART 运行时,ART默认支持从APK文件中加载多个DEX文件。当应用安装时,ART将执行预编译,会扫描多个.dex文件,并将它们编译为一个单独的.oat文件用来被Android设备执行。因此如果minSdkVersion=21或者更高,则不再需要multidex支持库。

关于更多的Anroid 5.0运行时知识,请参考ART and Dalvik

说明:当使用Instant Run时,如果应用的minSdkVersion=21或者更高,则Android Studio会自动为该应用配置multidex。由于Instant Run仅用于App编译的debug模式,因此仍然需要为release模式配置multidex来避免64K的限制。

避免64K 方法数的限制

在为App配置允许使用64K或更多的方法引用之前,我们应该采取措施来减少App中所需的方法总数,包括我们自定义的方法及引用库的方法。以下策略可以帮助我们避免触发DEX的64K限制:

  • Review应用的直接依赖或传递依赖

    Ensure any large library dependency you include in your app is used in a manner that outweighs the amount of code being added to the app. A common anti-pattern is to include a very large library because a few utility methods were useful. 减少应用的代码依赖可以有效地避免DEX引用限制。

  • 使用ProGuard移除无用代码

    为应用的release版本开启Proguard的Enable code shrinking,代码压缩将不会把无用代码打包到APK中。

使用上述方法可以帮助我们避免在应用中使用multidex,同时能够有效减少APK包的大小。

为应用配置multidex

为应用配置multidex需要对应用工程作一些修改,这取决于该App所支持的最低Android版本。

如果minSdkVersion设置为21或更高,那么只需要为module中的build.gradle文件添加multiDexEnabledtrue,具体如下:

android {
    defaultConfig {
        ...
        minSdkVersion 21 
        targetSdkVersion 25
        multiDexEnabled true
    }
    ...
}

如果minSdkVersion为20或更低,则必须使用multidex support library,具体操作如下:

  1. 为Module中的build.gradle文件添加multiDexEnabledtrue,并且添加multidex library依赖,如下所示:

    android {
        defaultConfig {
            ...
            minSdkVersion 15 
            targetSdkVersion 25
            multiDexEnabled true
        }
        ...
    }
    
    dependencies {
      compile 'com.android.support:multidex:1.0.1'
    }
  2. 根据是否重写了Application类,选择下面的对应方案:

    • 如果没有重写Application类,将AndroidManifest.xml中的标签的android:name做如下设置:

      <?xml version="1.0" encoding="utf-8"?>
      <manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example.myapp">
          <application
                  android:name="android.support.multidex.MultiDexApplication" >
              ...
          </application>
      </manifest>
    • 如果重写了Application类,让它继承MultiDexApplication(如果可能):

      public class MyApplication extends MultiDexApplication { ... }
    • 如果重写了Application类,但是它的基类无法修改,那么可以重写attachBaseContext()方法并且调用MultiDex.install(this)来允许使用multidex。

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

现在当编译应用,Android构建工具将会构造一个主DEX文件(classes.dex),如果有需要的话还会生成其他DEX文件(如classes2.dex,classes3.dex等),构建系统然后会将所有的DEX文件打包到APK中。

在运行的时候,multidex API会使用特定的类加载器在所有可用的DEX文件中来搜索所需方法。

multidex support library的局限性

multidex support library有一些已知的局限性,在为应用配置multidex之前我们需要知道并测试:

  • 在启动时将DEX安装到设备的数据分区很复杂,如果第二个DEX文件很大的话,有可能导致ANR错误。这种情况下,我们需要开启code shrinking with ProGuard来减小DEX的大小并移除无用代码。
  • 一些使用了multidex的应用可能无法在低于Android 4.0(API level 14)的设备上运行,这可能是由于Dalvik linarAlloc bugIssue 22586)引起。如果目标API的版本低于14,必须确保对这些平台版本展开充分测试,因为它们可能在启动或某些特殊类加载时产生异常。Code shrinking可以减少或消除这些潜在的问题。
  • 使用了multidex的应用需要更多的内存分配,也可能导致应用崩溃,这是由于Dalvik linearAlloc limitIssue 78053)的原因。虽然内存分配限制在Android 4.0(API level 14)增加了,但App仍有可能在Android 5.0(API level 21)之前的版本中运行时到达上限。

为主DEX 文件声明必须类

为配置了multidex的应用构建每一个DEX文件时,构建工具需要做一个复杂的决策,来决定哪一个类是主DEX文件必须的,来保证应用能成功启动。如果某个在启动时必须的类没有被包含到主DEX文件,那么应用启动时就会崩溃,并报错java.lang.NoClassDefFoundError

如果我们应用的代码可以直接访问某些代码,上述错误一般来说不会发生,因为构建工具可以识别出这些代码路径,但是当代码路径不可见时就可能会发生上述异常,如应用中的某个库包含了复杂的依赖。一个更具体的例子,如代码使用了反射或从Native代码调用Java方法,这些方法则可能不会被识别并放到主DEX文件中。

所以当遇到java.lang.NoClassDefFoundError时,需要手动指定哪些类为主DEX必须的,具体方法为通过在build type中使用multiDexKeepFile属性来声明它们。在这里指定的文件必须每个类声明为一行,书写格式为com/example/MyClass.class。例如,可以创建如下一个叫做dex.keep的文件:

com/example/MyClass.class
com/example/MyOtherClass.class

然后就可以为某个build type指定此文件:

android {
    buildTypes {
        release {
            multiDexKeepFile file('dex.keep')
            ...
        }
    }
}

因为Gradle是相对于build.gradle文件读取路径的,所以上述例子中的dex.keep需要跟build.gradle文件处于相同的目录才可生效。

在开发中优化multidex的构建过程

配置了Multidex的应用所需要的构建时间明显增加,因为构建系统需要做复杂的分析来决定哪些类需要包含在主DEX中,哪些类需要包含则其他DEX文件中。这意味着使用multidex的构建会花费更多时间并减慢我们的开发过程。

为了缩短multidex的构建时间,我们可以使用productFlavors创建两个构建变种:一个development flavor和一个release flavor,它们使用不同的minSdkVersion。

对于development flavor,将minSdkVersion设为21,此设置将会使用一种叫做per-dexing的构建功能,在使用ART模式时(Android 5.0或更高),它会更快地为multidex生成APK。对于release flavor,将minSdkVersion设置为应用真正要支持的最低版本,这种设置方式将会生成兼容更多设备的APK,但是需要花费更长的构建时间。

下面的构建配置说明了在Gradle中如何设置上述flavor:

android {
    defaultConfig {
        ...
        multiDexEnabled true
    }
    productFlavors {
        dev {
            // Enable pre-dexing to produce an APK that can be tested on
            // Android 5.0+ without the time-consuming DEX build processes.
            minSdkVersion 21
        }
        prod {
            // The actual minSdkVersion for the production version.
            minSdkVersion 14
        }
    }
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                                                 'proguard-rules.pro'
        }
    }
}
dependencies {
    compile 'com.android.support:multidex:1.0.1'
}

完成了上述配置后,就可以使用devDebug变种来进行增量构建,此种方式组合了dev product flavor以及debug构建类型。这将生成一个debug版的使用了multidex并且禁用proguard(因为minifyEnabled默认为false)的应用。上述配置会让Gradle插件做以下事情:

  1. 执行pre-dexing:将每一个Module及每一个依赖编译成一个独立的DEX文件。
  2. 将每一个DEX文件不做任何修改地包含进APK文件中(没有执行代码压缩)。
  3. 最重要的是,每个Module生成的DEX没有组合起来,因此不需要花费很长时间来计算主DEX应该包含哪些内容。

上述配置会使得构建加快,因为仅仅是发生修改的module会被重计算及打包构建出新的DEX文件,但是,上述配置生成的APK只能在Android 5.0的设备上测试。然而通过使用flavor配置,我们也可以使用通用的构建方式来生成APK,这些APK可以适配最低的SDK版本并且使用了ProGuard代码压缩。

还可以构建其他变种,如prodDebug,它会花费较长时间来构建,但是可以用来做开发之外的测试。prodRelease可作为最终的测试及发布版本。如需了解更多的构建变种知识,请参考 Configure Build Variants

测试multidex应用

为multidex应用写测试用例时,无需其他配置。AndroidJUnitRunner支持multidex,只要你使用MultiDexApplication或者为自定义的Application重写attachBaseContext()方法并调用MultiDex.install(this)来开启muxtidex支持。

另外,可以在AndroidJUnitRunner中重写onCreate()方法:

public void onCreate(Bundle arguments) {
    MultiDex.install(getTargetContext());
    super.onCreate(arguments);
    ...
}

说明:使用multidex来创建一个测试APK目前是不支持的。

原文地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值