問題
通過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檔案,儲存在
目錄中。下次編譯時,當存在對應的緩存dex檔案時,将直接使用緩存檔案,加快編譯速度(該配置為可選配置,在高版本的AS 和AGP中會自動根據連接配接的裝置進行設定)。app\build\intermediates\transforms
Android APK 中 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/[email protected]@[email protected]@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 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 || 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優化,這也就造成了開頭類找不到的問題。