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

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 moreE/AndroidRuntime(7655): Suppressed: java.lang.ClassNotFoundException: com.test.utils.AppUtilE/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 moreE/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.cc 和 dex_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 artstatic 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 || dex2oat.IsImage() || dex2oat.IsHost() || !kIsTargetBuild) {    LOG(INFO) << CommandLine();  } else {    LOG(INFO) << StrippedCommandLine();  }  if (!dex2oat.Setup()) {    dex2oat.EraseOatFile();    return EXIT_FAILURE;  }  if (dex2oat.IsImage()) {    return CompileImage(dex2oat);  } else {    return CompileApp(dex2oat);  }}

main函数的逻辑比较简单,直接调用静态的dex2oat函数并传入参数,主要的工作在该函数中。dex2oat函数中主要包含几个流程:ParseArgs, Setup, CompileApp

Dex2Oat.ParseArgs函数
  // Parse the arguments from the command line. In case of an unrecognized option or impossible  // values/combinations, a usage error will be displayed and exit() is called. Thus, if the method  // returns, arguments have been successfully parsed.  void ParseArgs(int argc, char** argv) {    //此处省略代码    for (int i = 0; i < argc; i++) {      //此处省略代码      if (option.starts_with("--dex-file=")) { dex_filenames_.push_back(option.substr(strlen("--dex-file=")).data());      } else if  (option.starts_with("--zip-fd=")) { const char* zip_fd_str = option.substr(strlen("--zip-fd=")).data(); if (!ParseInt(zip_fd_str, &zip_fd_)) {   Usage("Failed to parse --zip-fd argument '%s' as an integer", zip_fd_str); } if (zip_fd_ < 0) {   Usage("--zip-fd passed a negative value %d", zip_fd_); }      } else if (option.starts_with("--zip-location=")) { zip_location_ = option.substr(strlen("--zip-location=")).data();      }//此处省略代码      //由于命令行参数没有指定 "--image="和"--boot-image=", 解析的结果为空      else if (option.starts_with("--image=")) { image_filename_ = option.substr(strlen("--image=")).data();      }      else if (option.starts_with("--boot-image=")) { boot_image_filename = option.substr(strlen("--boot-image=")).data();      }      //此处省略代码     }     //给 "boot_image_filename"和"boot_image_option_"初始化默认值    image_ = (!image_filename_.empty());    if (!image_ && boot_image_filename.empty()) {      boot_image_filename += android_root_;      boot_image_filename += "/framework/boot.art";    }    if (!boot_image_filename.empty()) {      boot_image_option_ += "-Ximage:";      boot_image_option_ += boot_image_filename;    }    //此处省略代码   }

ParseArgs()函数中会解析命令行中的参数--zip-fd文件id 和 --zip-location文件路径,分别保存在zip_fd_zip_location_。由于不是通过--dex-file指定要编译的文件dex_filenames_的值为空,后面还有会给没有指定值的boot_image_filename, boot_image_option_赋默认值。

Dex2Oat.Setup函数
  // Set up the environment for compilation. Includes starting the runtime and loading/opening the  // boot class path.  bool Setup() {     //此处省略代码      //这里boot_image_option_不为空    if (boot_image_option_.empty()) {      dex_files_ = Runtime::Current()->GetClassLinker()->GetBootClassPath();    } else {      //这里dex_filenames_为空      if (dex_filenames_.empty()) { ATRACE_BEGIN("Opening zip archive from file descriptor"); std::string error_msg; std::unique_ptr<ZipArchive> zip_archive(ZipArchive::OpenFromFd(zip_fd_, zip_location_.c_str(), &error_msg)); if (zip_archive.get() == nullptr) {   LOG(ERROR) << "Failed to open zip from file descriptor for '" << zip_location_ << "': "<< error_msg;   return false; } if (!DexFile::OpenFromZip(*zip_archive.get(), zip_location_, &error_msg, &opened_dex_files_)) {   LOG(ERROR) << "Failed to open dex from file descriptor for zip file '" << zip_location_<< "': " << error_msg;   return false; } for (auto& dex_file : opened_dex_files_) {   dex_files_.push_back(dex_file.get()); } ATRACE_END();      } else {//此处省略代码       }    }    //此处省略代码     return true;  }

函数中会进行boot_image_option_dex_filenames_的判断。根据ParseArgs()函数解析得到的值,通过ZipArchive::OpenFromFd()打开APK文件,并进入到DexFile::OpenFromZip()的分支逻辑中。

DexFile::OpenFromZip函数
// Technically we do not have a limitation with respect to the number of dex files that can be in a// multidex APK. However, it's bad practice, as each dex file requires its own tables for symbols// (types, classes, methods, ...) and dex caches. So warn the user that we open a zip with what// seems an excessive number.static constexpr size_t kWarnOnManyDexFilesThreshold = 100;bool DexFile::OpenFromZip(const ZipArchive& zip_archive, const std::string& location,     std::string* error_msg,     std::vector<std::unique_ptr<const DexFile>>* dex_files) {  DCHECK(dex_files != nullptr) << "DexFile::OpenFromZip: out-param is nullptr";  ZipOpenErrorCode error_code;  std::unique_ptr<const DexFile> dex_file(Open(zip_archive, kClassesDex, location, error_msg,     &error_code));  if (dex_file.get() == nullptr) {    return false;  } else {    // Had at least classes.dex.    dex_files->push_back(std::move(dex_file));    for (size_t i = 1; ; ++i) {      std::string name = GetMultiDexClassesDexName(i);      std::string fake_location = GetMultiDexLocation(i, location.c_str());      std::unique_ptr<const DexFile> next_dex_file(Open(zip_archive, name.c_str(), fake_location,error_msg, &error_code));      if (next_dex_file.get() == nullptr) { if (error_code != ZipOpenErrorCode::kEntryNotFound) {   LOG(WARNING) << error_msg; } break;      } else { dex_files->push_back(std::move(next_dex_file));      }      if (i == kWarnOnManyDexFilesThreshold) { LOG(WARNING) << location << " has in excess of " << kWarnOnManyDexFilesThreshold<< " dex files. Please consider coalescing and shrinking the number to "   " avoid runtime overhead.";      }      if (i == std::numeric_limits<size_t>::max()) { LOG(ERROR) << "Overflow in number of dex files!"; break;      }    }    return true;  }}

函数会循环读取APK文件中的classes.dex和classesN.dex文件,并生成对应DexFile对象。在这里我们也看到了

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

日志的出处。当APK文件中的dex文件的数量超过100个的时候,会打印这条警告日志。但这里仅是打印日志,生成的DexFile对象还是会被添加到dex_files,并不影响后续的编译和应用功能。

我们再看下是5.X版本中的DexFile::OpenFromZip()的逻辑:

bool DexFile::OpenFromZip(const ZipArchive& zip_archive, const std::string& location,     std::string* error_msg, std::vector<const DexFile*>* dex_files) {  ZipOpenErrorCode error_code;  std::unique_ptr<const DexFile> dex_file(Open(zip_archive, kClassesDex, location, error_msg,     &error_code));  if (dex_file.get() == nullptr) {    return false;  } else {    // Had at least classes.dex.    dex_files->push_back(dex_file.release());    // Now try some more.    size_t i = 2;    // We could try to avoid std::string allocations by working on a char array directly. As we    // do not expect a lot of iterations, this seems too involved and brittle.    while (i < 100) {      std::string name = StringPrintf("classes%zu.dex", i);      std::string fake_location = location + kMultiDexSeparator + name;      std::unique_ptr<const DexFile> next_dex_file(Open(zip_archive, name.c_str(), fake_location,error_msg, &error_code));      if (next_dex_file.get() == nullptr) { if (error_code != ZipOpenErrorCode::kEntryNotFound) {   LOG(WARNING) << error_msg; } break;      } else { dex_files->push_back(next_dex_file.release());      }      i++;    }    return true;  }}

在5.X的版本中,dex2oat仅会加载前99个classesN.dex文件。当APK中的dex文件的数量超过99的时候,超过的这些dex文件将不会被载入和参与OAT优化,这也就造成了开头类找不到的问题。