Android 方法数 65k 问题

前言

本文参考自

美团 Android DEX 自动拆包及动态加载简介

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

Android 最大方法数和解决方案

1. 什么是方法数 65k 问题?

在 Andorid 中单个 dex 文件所能包含的最大方法数为 65536,这包含 Android FrameWork、依赖的 jar 包以及应用本身的代码中所有的方法,当应用的方法达到 65536 后,编译器就无法完成编译,会抛出异常:

较早版本的编译系统中,错误内容如下:

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

而在新版编译系统中,则是这样:

trouble writing output:
Too many field references: 131000; max is 65536.
You may try using --multi-dex option.
到底是 65k 还是 64k ?

65536 按 1000 算的话,是 65k;
65536 按 1024 算的话,是 64k;

重点是 65536 = 2^16。

2. 为什么会出现这个异常?

这个异常是 Android 应用的方法总数限制造成的。Android 平台的 Java 虚拟机 Dalvik 在执行 DEX 格式的 Java 应用程序时,使用原生类型 short 来索引 DEX 文件中的方法。这意味着单个 DEX 文件可被引用的方法总数被限制为 65536。通常 APK 仅包含一个 classes.dex 文件,因此 Android 应用的方法总数不能超过这个数量。

2.1 LinearAlloc 限制

即使方法数没有超过 65536,能正常编译打包成 apk,在安装的时候,也有可能会提示 INSTALL_FAILED_DEXOPT 而导致安装失败,这个一般就是因为 LinearAlloc 的限制导致的。这个主要是因为 Dexopt 使用 LinearAlloc 来存储应用的方法信息。Dalvik LinearAlloc 是一个固定大小的缓冲区。在 Android 版本的历史上,LinearAlloc 分别经历了 4M/5M/8M/16M 限制。Android 2.2 和 2.3 的缓冲区只有 5MB,Android 4.x 提高到了 8MB 或 16MB。当方法数量过多导致超出缓冲区大小时,也会造成 Dexopt 崩溃。

3. 如何解决方法数 65k 问题?

要解决这个问题,一般有下面几种方案:

  • 一种方案是加大 Proguard 的力度来减小 DEX 的大小和方法数,但这是治标不治本的方案,随着业务代码的添加,方法数终究会到达这个限制;

  • 一种是插件化方案;

  • 谷歌提供了一个 MultiDex 的分包方案,当方法数超过 65536 的时候,生成多个 dex 文件,把应用启动时必须用到的类和该类的直接引用类放到 Main dex 中,把其他类放到 Second dex 中。当应用启动之后,动态加载 Second dex,从而避免 65k 问题(建议);

  • google 在推出 MultiDex 之前 Android Developers 博客介绍的通过自定义类加载过程(不建议);

  • Facebook 推出的为 Android 应用开发的 Dalvik 补丁,但 Facebook 博客里写的不是很详细(不建议);

美团的技术团队在文章中写到:

我们在插件化方案上也做了探索和尝试,发现部署插件化方案,首先需要梳理和修改各个业务线的代码,使之解耦,改动的面和量比较巨大,通过一定的探讨和分析,我们认为对我们目前来说采用 MultiDex 方案更靠谱一些,这样我们可以快速和简洁的对代码进行拆分,同时代码改动也在可以接受的范围内; 这样我们采用了 google 提供的 MultiDex 方式进行了开发。

3.1 MultiDex 的分包方法

3.1.1 首先使用 Android SDK Manager 升级到最新的 Android SDK Build Tools 和 Android Support Library。
3.1.2 修改 Gradle 配置文件,启用 MultiDex 并包含 MultiDex 支持:
android {

    compileSdkVersion 26
    buildToolsVersion '26.0.2'

    defaultConfig {
        ...
        minSdkVersion 22
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

        //启动 MultiDex 支持
        multiDexEnabled true
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

    dexOptions {
        //设置虚拟机堆内存大小,避免编译期间OOM,一般设置4g就够了
        javaMaxHeapSize "4g"
    }

}
    ...

    dependencies { 
    ...
    //使用google提供的分包库
    implementation 'com.android.support:multidex:1.0.2'
    implementation 'com.android.support:multidex-instrumentation:1.0.2'
    ...
}

3.1.3 让应用支持多 DEX 文件,在官方文档中描述了三种可选方法:
  1. AndroidManifest.xmlapplication 中声明 android.support.MultiDex.MultiDexApplication

    
        
        ...
        
    

  1. 如果你已经有自己的 Application 类,让其继承 MultiDexApplication
public class MyApplication extends MultiDexApplication {

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
    }
}
  1. 如果你的 Application 类已经继承自其它类,你不想/能修改它,那么可以重写 attachBaseContext() 方法:
@Override 
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    MultiDex.install(this);
}

并在 AndroidManifest 中添加以下声明:


    
        
        ...
        
    

添加这些配置后,编译工具会构建出一个主 DEX 文件(classes.dex)和其他附属 DEX 文件(classes2.dexclasses3.dex 等,如果需要的话),编译系统会将他们打包到 Apk 文件中,可以通过配置文件来控制哪些代码放到主 DEX 包中。

下图是 Android 的打包流程示意图:

Android 方法数 65k 问题_第1张图片

4. 谷歌 MultiDex 存在的问题

虽然谷歌的分包方案很简单,但是效果并不是那么好,谷歌本身也枚举了分包方案的缺点

  1. 如果在主线程中执行 MultiDex.install,加载 second dex,因为加载 dex 是同步的,会阻塞线程,second dex 太大的话,有可能导致 ANR;

  2. API Level 14之前,由于 Dalvik LinearAlloc bug,很可能会出问题;

  3. 应用程序使用了 Multiedex 配置的,会造成使用比较大的内存;

  4. 对于应用程序比较复杂的,存在较多的 library 的项目,Multidex 可能会造成不同依赖项目间的 dex 文件函数相互调用,找不到方法。

5. 如何解决谷歌分包方案的问题

针对上面的问题,参考网上的一些解决方案,如美团、facebook、微信等,初步使用的解决方法如下:

  1. 第一次启动的时候,检测到未曾加载过 second dex,那么启动欢迎页面(启动新的进程,原来进程进入阻塞等待,注意,此时不会发生 ANR,因为已经不是前台进程了),在欢迎页面里面进行 second dex 的加载,加载完成后通知主线程继续;

  2. 设定单个 dex 文件最大方法数为 48000(经验值)而不是 65536,避免内存问题;

  3. 控制程序逻辑,未加载完 second dex 之前,进入阻塞等待,直到加载完程序才往下执行。

下面是流程图:

Android 方法数 65k 问题_第2张图片

6. 使用 APK Analyzer 查看方法总数

Android Studio 自带的 APK Analyzer,功能齐全,使用方便,使用 Android Studio APK Analyzer ,我们至少能够做到:

  • 查看APK压缩文件中各个子文件的大小(如 DEX 和 resource 文件)

  • 理解 DEX 文件的结构

  • 快速查看 APK 文件的版本信息(直接查看 AndroidManifest.xml 内容)

  • 直观地比较两个 APK 文件内容

Android 方法数 65k 问题_第3张图片

开发阶段使用 Android Studio 打开一个项目时,有三种方式使用 APK Analyzer 工具:

  • 直接拖拽 APK 文件到 Android Studio 的编辑窗口

  • 双击打开项目目录 app/build/outputs/apk/ 下的 APK 文件

  • 点击菜单栏 Build->Analyse APK... 并选择 APK 文件

有哪些方式可以减少方法数?

首先 dex 方法数和 dex文件有关,我们把源码编译、转化为 dex 文件时,dex 文件中会有一个区域包含了所有源码中定义或引用的方法列表,这个区域中所有方法项的总数就是方法数。之所以要考虑方法数其实是因为 Android 在设计之初只给这个区域定义了两个字节的范围(方法数量不能超过 65535 个),当超过了这个限制就会导致编译不成功,所以我们要关注方法数问题。

查看方法数可以使用命令行,也可以使用 dexcount-gradle-plugin 插件。

开发中减少方法数的实践经验主要有如下一些:

  • 小需求尽量使用精简第三方库或者不使用。

  • 尽量避免在内部类访问外部类的私有属性和方法。

  • 避免在派生类中调用未被 override 的方法,而是采用 super.func() 形式调用未重写方法,可以减少一个方法数。

你可能感兴趣的:(Android 方法数 65k 问题)