Android APK 中 dex 文件数量限制问题

问题

通过AS直接运行程序,启动就报必现的ClassNotFoundException异常, 仅在5.X的系统版本 API 21和22的出现, 6.0以后的系统版本正常。并且仅在Debug模式下有问题,Release模式正常。

E/AndroidRuntime(7655): Caused by: java.lang.ClassNotFoundException: Didn't find class 
"com.test.utils.AppUtil" on path: DexPathList[[zip file "/data/app/com.test-1/base.apk"],
nativeLibraryDirectories=[/data/app/com.test-1/lib/arm, /vendor/lib, /system/lib]]
E/AndroidRuntime(7655): at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:56)
E/AndroidRuntime(7655): at java.lang.ClassLoader.loadClass(ClassLoader.java:511)
E/AndroidRuntime(7655): at java.lang.ClassLoader.loadClass(ClassLoader.java:469)
E/AndroidRuntime(7655): ... 13 more
E/AndroidRuntime(7655): Suppressed: java.lang.ClassNotFoundException: com.test.utils.AppUtil
E/AndroidRuntime(7655): at java.lang.Class.classForName(Native Method)
E/AndroidRuntime(7655): at java.lang.BootClassLoader.findClass(ClassLoader.java:781)
E/AndroidRuntime(7655): at java.lang.BootClassLoader.loadClass(ClassLoader.java:841)
E/AndroidRuntime(7655): at java.lang.ClassLoader.loadClass(ClassLoader.java:504)
E/AndroidRuntime(7655): ... 14 more
E/AndroidRuntime(7655): Caused by: java.lang.NoClassDefFoundError: 
Class not found using the boot class loader; no stack available

问题背景

随着应用发展App的方法数不断的上涨,为了加快Android的编译速度,我们添加了以下内容:

android {
   
  defaultConfig {
   
      multiDexEnabled = true
      minSdkVersion 21
  }
  dexOptions {
   
      preDexLibraries = true
  }
}
  • multidexDexEnable
    分包设置: 当总方法数超过64k时,允许拆分成多个dex文件 (更多内容)。

  • minSdkVersion
    最低支持设备版本:Android 5.0开始ART虚拟机默认支持加载多dex文件。如果我们把值设置为21或者更大,在编译App时,2.3或者更高版本的AS会检测所要安装的设备是否大于5.0或者更高,是的话会开启pre-dexing(更多内容)。

  • preDexLibaries
    预缓存dex文件:每个依赖对应一个classes.dex文件,保存在app\build\intermediates\transforms目录中。下次编译时,当存在对应的缓存dex文件时,将直接使用缓存文件,加快编译速度(该配置为可选配置,在高版本的AS 和AGP中会自动根据连接的设备进行设置)。
    dex缓存目录

问题分析

关于类找不到的问题一般多发生于4.X版本的系统,系统本身不支持多dex的模式,需要使用MultiDex Library在第一次运行时对多个dex文件进行释放和优化。这里还涉及到一个MainDexList的问题,要求从Application启动到MultiDex.install()间所有相关的类都必须在第一个dex文件中,否则一启动就可能因为找不到类而闪退。

关于dex文件的优化,还会遇到一些奇怪的问题,即使MultiDex.install()执行成功了,可还是会出现类找不到的问题,并且遇到这个问题的用户还不在少数,问题都集中在4.X的版本。详细内容可以查看 Tinker的issue解决方案

由于发生问题的是在Android 5.X版本的设备上,这显然不是上面提到的问题。因为新设备本身就有对多dex文件的支持,系统会在App安装的时候通过dex2oat把多个dex合并为一个oat文件。在6.0以上的设备是正常的,Deubg包也没有开启Proguard,并且在APK包的classes103.dex文件中也找到了AppUtil类和方法定义(由于开启了preDexLibraries, 所以dex文件非常多),说明打包出来的APK文件也是没有问题的。在这里插入图片描述
通过对比新旧版本App的安装和启动日志,在5.X的设备上,并没有发现两个版本的日志有什么不同和异常。不过在6.0设备上发现了一条奇怪的Warn级别日志,并且这条日志在旧版本正常启动的安装日志里面是没有的。

W/dex2oat: base.apk has in excess of 100 dex files. Please consider coalescing and shrinking 
the number to  avoid runtime overhead.

对比新旧版本的APK文件发现,旧版的Debug APK中的dex文件有93个,而新版的Debug APK有103个dex文件(新版升级到LeakCanary 2.0)。93个正常,103个则异常,再根据上面的日志提示,是否有可能是因为dex文件的增多导致的问题?


安装流程
dex2oat编译

我们应用在安装的时候,系统会通过dex2oat工具将APK内的dex文件合并成oat文件。

 I/dex2oat: /system/bin/dex2oat 
 --zip-fd=12 
 --zip-location=/data/app/com.test-1/base.apk 
 --oat-fd=13 
 --oat-location=/data/dalvik-cache/arm/data@app@com.test-1@base.apk@classes.dex 
 --instruction-set=x86 --instruction-set-features=default
 --runtime-arg -Xms64m --runtime-arg -Xmx512m4

dex2oat执行的主要流程如下:
在这里插入图片描述
上图涉及的源码在 dex2oat.ccdex_file.cc两个部分。

dex2oat的main函数
int main(int argc, char** argv) {
   
  int result = art::dex2oat(argc, argv);
  // Everything was done, do an explicit exit here to avoid running Runtime destructors that take
  // time (bug 10645725) unless we're a debug build or running on valgrind. Note: The Dex2Oat class
  // should not destruct the runtime in this case.
  if (!art::kIsDebugBuild && (RUNNING_ON_VALGRIND == 0)) {
   
    exit(result);
  }
  return result;
}

// namespace art
static int dex2oat(int argc, char** argv) {
   
  b13564922();
  TimingLogger timings("compiler", false, false);
  Dex2Oat dex2oat(&timings);

  // Parse arguments. Argument mistakes will lead to exit(EXIT_FAILURE) in UsageError.
  dex2oat.ParseArgs(argc, argv);

  // Check early that the result of compilation can be written
  if (!dex2oat.OpenFile()) {
   
    return EXIT_FAILURE;
  }

  // Print the complete line when any of the following is true:
  //   1) Debug build
  //   2) Compiling an image
  //   3) Compiling with --host
  //   4) Compiling on the host (not a target build)
  // Otherwise, print a stripped command line.
  if (kIsDebugBuild ||
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值