Android ABI介绍
不同的 Android 设备使用不同的 CPU,而不同的 CPU 支持不同的指令集。CPU 与指令集的每种组合都有专属的应用二进制接口 (Application Binary Interface,ABI)
常见的cpu架构
- armeabi
第5代、第6代的ARM处理器,早期的手机用的比较多(性能差) - armeabi-v7a
第7代及以上的 ARM 处理器。2011年15月以后的生产的大部分Android设备都使用它 - arm64-v8a
第8代、64位ARM处理器,越来越多的设备开始使用。
现在在应用市场发布app必须要支持64位架构,并且必须有64位so文件
在 Android 系统中,当我们安装apk文件的时候,lib 目录下的 so 文件会被解压到 app 的原生库目录,一般来说是放到 /data/data/<package-name>/lib 目录下,而根据系统和CPU架构的不同,其拷贝策略也是不一样的,在我们测试过程中发现不正确地配置了 so 文件,比如某些 app 使用第三方的 so 时,只配置了其中某一种 CPU 架构的 so,可能会造成 app 在某些机型上的适配问题。所以这篇文章主要介绍一下在不同版本的 Android 系统中,安装 apk 时,PackageManagerService 选择解压 so 库的策略,并给出一些 so 文件配置的建议。
apk 安装
目录
- system/app:系统自带的应用程序,获得adb root权限才能删除
- data/app:用户程序安装的目录。安装时把apk文件复制到此目录
- data/data:存放应用程序的数据
- data/dalvik-cache:将apk中的dex文件安装到dalvik-cache目录下
Android 4.0.4以后
以4.1.2系统为例,遍历选择 so 逻辑如下:
static install_status_t iterateOverNativeFiles(JNIEnv *env, jstring javaFilePath, jstring javaCpuAbi, jstring javaCpuAbi2, iterFunc callFunc, void* callArg) {
ScopedUtfChars filePath(env, javaFilePath);
ScopedUtfChars cpuAbi(env, javaCpuAbi);
ScopedUtfChars cpuAbi2(env, javaCpuAbi2);
ZipFileRO zipFile;
if (zipFile.open(filePath.c_str()) != NO_ERROR) {
ALOGI("Couldn't open APK %s\n", filePath.c_str());
return INSTALL_FAILED_INVALID_APK;
}
const int N = zipFile.getNumEntries();
char fileName[PATH_MAX];
bool hasPrimaryAbi = false;
for (int i = 0; i < N; i++) {
const ZipEntryRO entry = zipFile.findEntryByIndex(i);
if (entry == NULL) {
continue;
}
// Make sure this entry has a filename.
if (zipFile.getEntryFileName(entry, fileName, sizeof(fileName))) {
continue;
}
// Make sure we're in the lib directory of the ZIP.
if (strncmp(fileName, APK_LIB, APK_LIB_LEN)) {
continue;
}
// Make sure the filename is at least to the minimum library name size.
const size_t fileNameLen = strlen(fileName);
static const size_t minLength = APK_LIB_LEN + 2 + LIB_PREFIX_LEN + 1 + LIB_SUFFIX_LEN;
if (fileNameLen < minLength) {
continue;
}
const char* lastSlash = strrchr(fileName, '/');
ALOG_ASSERT(lastSlash != NULL, "last slash was null somehow for %s\n", fileName);
// Check to make sure the CPU ABI of this file is one we support.
const char* cpuAbiOffset = fileName + APK_LIB_LEN;
const size_t cpuAbiRegionSize = lastSlash - cpuAbiOffset;
ALOGV("Comparing ABIs %s and %s versus %s\n", cpuAbi.c_str(), cpuAbi2.c_str(), cpuAbiOffset);
if (cpuAbi.size() == cpuAbiRegionSize
&& *(cpuAbiOffset + cpuAbi.size()) == '/'
&& !strncmp(cpuAbiOffset, cpuAbi.c_str(), cpuAbiRegionSize)) {
ALOGV("Using primary ABI %s\n", cpuAbi.c_str());
hasPrimaryAbi = true;
} else if (cpuAbi2.size() == cpuAbiRegionSize
&& *(cpuAbiOffset + cpuAbi2.size()) == '/'
&& !strncmp(cpuAbiOffset, cpuAbi2.c_str(), cpuAbiRegionSize)) {
/*
* If this library matches both the primary and secondary ABIs,
* only use the primary ABI.
*/
if (hasPrimaryAbi) {
ALOGV("Already saw primary ABI, skipping secondary ABI %s\n", cpuAbi2.c_str());
continue;
} else {
ALOGV("Using secondary ABI %s\n", cpuAbi2.c_str());
}
} else {
ALOGV("abi didn't match anything: %s (end at %zd)\n", cpuAbiOffset, cpuAbiRegionSize);
continue;
}
// If this is a .so file, check to see if we need to copy it.
if ((!strncmp(fileName + fileNameLen - LIB_SUFFIX_LEN, LIB_SUFFIX, LIB_SUFFIX_LEN)
&& !strncmp(lastSlash, LIB_PREFIX, LIB_PREFIX_LEN)
&& isFilenameSafe(lastSlash + 1))
|| !strncmp(lastSlash + 1, GDBSERVER, GDBSERVER_LEN)) {
install_status_t ret = callFunc(env, callArg, &zipFile, entry, lastSlash + 1);
if (ret != INSTALL_SUCCEEDED) {
ALOGV("Failure for entry %s", lastSlash + 1);
return ret;
}
}
}
return INSTALL_SUCCEEDED;
}
拷贝 so 策略:
遍历 apk 中文件,当遍历到有主 Abi 目录的 so 时,拷贝并设置标记 hasPrimaryAbi 为真,以后遍历则只拷贝主 Abi 目录下的 so,当标记为假的时候,如果遍历的 so 的 entry 名包含次 abi 字符串,则拷贝该 so。
策略问题:
经过实际测试, so 放置不当时,安装 apk 时存在 so 拷贝不全的情况。这个策略想解决的问题是在 4.0 ~ 4.0.3 系统中的 so 随意覆盖的问题,即如果有主 abi 目录的 so 则拷贝,如果主 abi 目录不存在这个 so 则拷贝次 abi 目录的 so,但代码逻辑是根据 ZipFileR0 的遍历顺序来决定是否拷贝 so,假设存在这样的 apk, lib 目录下存在 armeabi/libx.so , armeabi/liby.so , armeabi-v7a/libx.so 这三个 so 文件,且 hash 的顺序为 armeabi-v7a/libx.so 在 armeabi/liby.so 之前,则 apk 安装的时候 liby.so 根本不会被拷贝,因为按照拷贝策略, armeabi-v7a/libx.so 会优先遍历到,由于它是主 abi 目录的 so 文件,所以标记被设置了,当遍历到 armeabi/liby.so 时,由于标记被设置为真, liby.so 的拷贝就被忽略了,从而在加载 liby.so 的时候会报异常。
64位系统支持
关于so的拷贝我们还是照旧不细说App的安装流程了,主要还是和之前一样不论是替换还是新安装,都会调用PackageManagerService的scanPackageLI()函数,然后跑去scanPackageDirtyLI函数,而在这个函数中对于非系统的APP他调用了derivePackageABI这个函数,通过这个函数他将会觉得系统的abi是多少,并且也会进行我们最关心的so拷贝操作。
PackageManagerService.java
public void derivePackageAbi(PackageParser.Package pkg, File scanFile,
String cpuAbiOverride, boolean extractLibs)
throws PackageManagerException {
......
......
if (isMultiArch(pkg.applicationInfo)) {
// Warn if we've set an abiOverride for multi-lib packages..
// By definition, we need to copy both 32 and 64 bit libraries for
// such packages.
if (pkg.cpuAbiOverride != null
&& !NativeLibraryHelper.CLEAR_ABI_OVERRIDE.equals(pkg.cpuAbiOverride)) {
Slog.w(TAG, "Ignoring abiOverride for multi arch application.");
}
int abi32 = PackageManager.NO_NATIVE_LIBRARIES;
int abi64 = PackageManager.NO_NATIVE_LIBRARIES;
if (Build.SUPPORTED_32_BIT_ABIS.length > 0) {
if (extractLibs) {
abi32 = NativeLibraryHelper.copyNativeBinariesForSupportedAbi(handle,
nativeLibraryRoot, Build.SUPPORTED_32_BIT_ABIS,
useIsaSpecificSubdirs);
} else {
abi32 = NativeLibraryHelper.findSupportedAbi(handle, Build.SUPPORTED_32_BIT_ABIS);
}
}
maybeThrowExceptionForMultiArchCopy(
"Error unpackaging 32 bit native libs for multiarch app.", abi32);
if (Build.SUPPORTED_64_BIT_ABIS.length > 0) {
if (extractLibs) {
abi64 = NativeLibraryHelper.copyNativeBinariesForSupportedAbi(handle,
nativeLibraryRoot, Build.SUPPORTED_64_BIT_ABIS,
useIsaSpecificSubdirs);
} else {
abi64 = NativeLibraryHelper.findSupportedAbi(handle, Build.SUPPORTED_64_BIT_ABIS);
}
}
maybeThrowExceptionForMultiArchCopy(
"Error unpackaging 64 bit native libs for multiarch app.", abi64);
if (abi64 >= 0) {
pkg.applicationInfo.primaryCpuAbi = Build.SUPPORTED_64_BIT_ABIS[abi64];
}
if (abi32 >= 0) {
final String abi = Build.SUPPORTED_32_BIT_ABIS[abi32];
if (abi64 >= 0) {
pkg.applicationInfo.secondaryCpuAbi = abi;
} else {
pkg.applicationInfo.primaryCpuAbi = abi;
}
}
} else {
String[] abiList = (cpuAbiOverride != null) ?
new String[] { cpuAbiOverride } : Build.SUPPORTED_ABIS;
// Enable gross and lame hacks for apps that are built with old
// SDK tools. We must scan their APKs for renderscript bitcode and
// not launch them if it's present. Don't bother checking on devices
// that don't have 64 bit support.
boolean needsRenderScriptOverride = false;
if (Build.SUPPORTED_64_BIT_ABIS.length > 0 && cpuAbiOverride == null &&
NativeLibraryHelper.hasRenderscriptBitcode(handle)) {
abiList = Build.SUPPORTED_32_BIT_ABIS;
needsRenderScriptOverride = true;
}
final int copyRet;
if (extractLibs) {
copyRet = NativeLibraryHelper.copyNativeBinariesForSupportedAbi(handle,
nativeLibraryRoot, abiList, useIsaSpecificSubdirs);
} else {
copyRet = NativeLibraryHelper.findSupportedAbi(handle, abiList);
}
if (copyRet < 0 && copyRet != PackageManager.NO_NATIVE_LIBRARIES) {
throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR,
"Error unpackaging native libs for app, errorCode=" + copyRet);
}
if (copyRet >= 0) {
pkg.applicationInfo.primaryCpuAbi = abiList[copyRet];
} else if (copyRet == PackageManager.NO_NATIVE_LIBRARIES && cpuAbiOverride != null) {
pkg.applicationInfo.primaryCpuAbi = cpuAbiOverride;
} else if (needsRenderScriptOverride) {
pkg.applicationInfo.primaryCpuAbi = abiList[0];
}
}
} catch (IOException ioe) {
Slog.e(TAG, "Unable to get canonical file " + ioe.toString());
} finally {
IoUtils.closeQuietly(handle);
}
// Now that we've calculated the ABIs and determined if it's an internal app,
// we will go ahead and populate the nativeLibraryPath.
setNativeLibraryPaths(pkg);
}
流程大致如下,这里的nativeLibraryRoot其实就是我们上文提到过的mLibDir,这样就完成了我们的对应关系,我们要从apk中解压出so,然后拷贝到mLibDir下,这样在load的时候才能去这里找的到这个文件,这个值我们举个简单的例子方便理解,比如com.test的app,这个nativeLibraryRoot的值基本可以理解成:/data/app-lib/com.test-1。
接下来的重点就是查看这个拷贝逻辑是如何实现的,代码在NativeLibraryHelper中copyNativeBinariesForSupportedAbi的实现
Android 在5.0之后支持64位 ABI,以5.1.0系统为例:
NativeLibraryHelper.java - Android Code Search
public static int copyNativeBinariesWithOverride(Handle handle, File libraryRoot, String abiOverride) {
try {
if (handle.multiArch) {
// 如果我们为多库包设置了 abiOverride 则发出警告..
// 根据定义,我们需要为此类包复制 32 位和 64 位库。
if (abiOverride != null && !CLEAR_ABI_OVERRIDE.equals(abiOverride)) {
Slog.w(TAG, "Ignoring abiOverride for multi arch application.");
}
int copyRet = PackageManager.NO_NATIVE_LIBRARIES;
//处理32位
if (Build.SUPPORTED_32_BIT_ABIS.length > 0) {
copyRet = copyNativeBinariesForSupportedAbi(handle, libraryRoot,
Build.SUPPORTED_32_BIT_ABIS, true /* use isa specific subdirs */);
if (copyRet < 0 && copyRet != PackageManager.NO_NATIVE_LIBRARIES &&
copyRet != PackageManager.INSTALL_FAILED_NO_MATCHING_ABIS) {
Slog.w(TAG, "Failure copying 32 bit native libraries; copyRet=" +copyRet);
return copyRet;
}
}
//处理64位
if (Build.SUPPORTED_64_BIT_ABIS.length > 0) {
copyRet = copyNativeBinariesForSupportedAbi(handle, libraryRoot,
Build.SUPPORTED_64_BIT_ABIS, true /* use isa specific subdirs */);
if (copyRet < 0 && copyRet != PackageManager.NO_NATIVE_LIBRARIES &&
copyRet != PackageManager.INSTALL_FAILED_NO_MATCHING_ABIS) {
Slog.w(TAG, "Failure copying 64 bit native libraries; copyRet=" +copyRet);
return copyRet;
}
}
} else {
String cpuAbiOverride = null;
if (CLEAR_ABI_OVERRIDE.equals(abiOverride)) {
cpuAbiOverride = null;
} else if (abiOverride != null) {
cpuAbiOverride = abiOverride;
}
String[] abiList = (cpuAbiOverride != null) ?
new String[] { cpuAbiOverride } : Build.SUPPORTED_ABIS;
if (Build.SUPPORTED_64_BIT_ABIS.length > 0 && cpuAbiOverride == null &&
hasRenderscriptBitcode(handle)) {
abiList = Build.SUPPORTED_32_BIT_ABIS;
}
int copyRet = copyNativeBinariesForSupportedAbi(handle, libraryRoot, abiList,
true /* use isa specific subdirs */);
if (copyRet < 0 && copyRet != PackageManager.NO_NATIVE_LIBRARIES) {
Slog.w(TAG, "Failure copying native libraries [errorCode=" + copyRet + "]");
return copyRet;
}
}
return PackageManager.INSTALL_SUCCEEDED;
} catch (IOException e) {
Slog.e(TAG, "Copying native libraries failed", e);
return PackageManager.INSTALL_FAILED_INTERNAL_ERROR;
}
}
copyNativeBinariesWithOverride 分别处理32位和64位 so 的拷贝,内部函数 copyNativeBinariesForSupportedAbi 首先会根据 abilist 去找对应的 abi。
NativeLibraryHelper.java - Android Code Search
public static int copyNativeBinariesForSupportedAbi(Handle handle, File libraryRoot,
String[] abiList, boolean useIsaSubdir, boolean isIncremental) throws IOException {
/*
* 如果这是一个内部应用程序或者我们的 nativeLibraryPath 指向 app-lib 目录,则在必要时解压缩这些库。
*/
int abi = findSupportedAbi(handle, abiList);
if (abi < 0) {
return abi;
}
/*
*如果我们有匹配的指令集,则在本机库根目录下构造一个与该指令集对应的子目录。
*/
final String supportedAbi = abiList[abi];
final String instructionSet = VMRuntime.getInstructionSet(supportedAbi);
final File subDir;
if (useIsaSubdir) {
subDir = new File(libraryRoot, instructionSet);
} else {
subDir = libraryRoot;
}
if (isIncremental) {
int res =
incrementalConfigureNativeBinariesForSupportedAbi(handle, subDir, supportedAbi);
if (res != PackageManager.INSTALL_SUCCEEDED) {
// TODO(b/133435829):此函数的调用者期望我们将索引返回到支持的 ABI。
//但是,任何非负整数都可以是有效索引。 我们应该修复这个函数并确保它不会意外返回一个也可以是有效索引的错误代码。
return res;
}
return abi;
}
// 对于非增量,使用常规提取和复制
createNativeLibrarySubdir(libraryRoot);
if (subDir != libraryRoot) {
createNativeLibrarySubdir(subDir);
}
// 即使 extractNativeLibs 为 false,我们仍然需要检查 APK 中的原生库是否有效。 这是在本机代码中完成的。
int copyRet = copyNativeBinaries(handle, subDir, supportedAbi);
if (copyRet != PackageManager.INSTALL_SUCCEEDED) {
return copyRet;
}
return abi;
}
findSupportedAbi 内部实现是 native 函数,首先遍历 apk,如果 so 的全路径中包含 abilist 中的 abi 字符串,则记录该 abi 字符串的索引,最终返回所有记录索引中最靠前的,即排在 abilist 中最前面的索引。
NativeLibraryHelper.java - Android Code Search
/**
* 检查给定的 APK 是否包含任何提供的本机代码
{@code supportAbis}。 如果找到匹配的 ABI,则返回 {@code supportedAbis} 的索引,如果 APK 不包含任何本机代码,则返回 {@link PackageManager#NO_NATIVE_LIBRARIES},如果 ABI 都不匹配,则返回 {@link PackageManager#INSTALL_FAILED_NO_MATCHING_ABIS}。match.
*/
public static int findSupportedAbi(Handle handle, String[] supportedAbis) {
int finalRes = NO_NATIVE_LIBRARIES;
for (long apkHandle : handle.apkHandles) {
final int res = nativeFindSupportedAbi(apkHandle, supportedAbis, handle.debuggable);
if (res == NO_NATIVE_LIBRARIES) {
// 没有本机代码,请继续查看所有 APK。
} else if (res == INSTALL_FAILED_NO_MATCHING_ABIS) {
// 找到一些本机代码,但没有 ABI 匹配; 如果我们没有找到其他有效代码,请更新我们的最终结果。
if (finalRes < 0) {
finalRes = INSTALL_FAILED_NO_MATCHING_ABIS;
}
} else if (res >= 0) {
// 找到有效的本机代码,跟踪最佳 ABI 匹配
if (finalRes < 0 || res < finalRes) {
finalRes = res;
}
} else {
// Unexpected error; bail
return res;
}
}
return finalRes;
}
举例说明,64位测试手机上的abi属性显示如下,它有3个 abilist,分别对应该手机支持的32位和64位 abi 的字符串组。
当处理32位 so 拷贝时, findSupportedAbi 索引返回之后,若返回为0,则拷贝 armeabi-v7a 目录下的 so,如果为1,则拷贝 armeabi 目录下 so。
拷贝 so 策略:
分别处理32位和64位 abi 目录的 so 拷贝, abi 由遍历 apk 结果的所有 so 中符合 abilist 列表的最靠前的序号决定,然后拷贝该 abi 目录下的 so 文件。
策略问题:
策略假定每个 abi 目录下的 so 都放置完全的,这是和2.3.6一样的处理逻辑,存在遗漏拷贝 so 的可能。
函数copyNativeBinariesForSupportedAbi,他的核心业务代码都在native层,它主要做了如下的工作
这个nativeLibraryRootDir上文在说到去哪找so的时候提到过了,其实是在这里创建的,然后我们重点看看findSupportedAbi和copyNativeBinaries的逻辑。
4.1 findSupportedAbi
findSupportedAbi 函数其实就是遍历apk(其实就是一个压缩文件)中的所有文件,如果文件全路径中包含 abilist中的某个abi 字符串,则记录该abi 字符串的索引,最终返回所有记录索引中最靠前的,即排在 abilist 中最前面的索引。
4.1.1 32位还是64位
这里的abi用来决定我们是32位还是64位,对于既有32位也有64位的情况,我们会采用64位,而对于仅有32位或者64位的话就认为他是对应的位数下,仅有32位就是32位,仅有64位就认为是64位的。
4.1.2 primaryCpuAbi是多少
当前文确定好是用32位还是64位后,我们就会取出来对应的上文查找到的这个abi值,作为primaryCpuAbi。
4.1.3 如果primaryCpuAbi出错
这个primaryCpuAbi的值是安装的时候持久化在pkg.applicationInfo中的,所以一旦abi导致进程位数出错或者primaryCpuAbi出错,就可能会导致一直出错,重启也没有办法修复,需要我们用一些hack手段来进行修复。
NativeLibraryHelper中的findSupportedAbi核心代码主要如下,基本就是我们前文说的主要逻辑,遍历apk(其实就是一个压缩文件)中的所有文件,如果文件全路径中包含 abilist中的某个abi 字符串,则记录该abi 字符串的索引,最终返回所有记录索引中最靠前的,即排在 abilist 中最前面的索引
建议
针对 Android 系统的这些拷贝策略的问题,我们给出了一些配置 so 的建议:
- 1)针对 armeabi 和 armeabi-v7a 两种 ABI
方法1:由于 armeabi-v7a 指令集兼容 armeabi 指令集,所以如果损失一些应用的性能是可以接受的,同时不希望保留库的两份拷贝,可以移除 armeabi-v7a 目录和其下的库文件,只保留 armeabi 目录;比如 apk 使用第三方的 so 只有 armeabi 这一种 abi 时,可以考虑去掉 apk 中 lib 目录下 armeabi-v7a 目录。
方法2:在 armeabi 和 armeabi-v7a 目录下各放入一份 so;
- 2)针对x86
目前市面上的x86机型,为了兼容 arm 指令,基本都内置了 libhoudini 模块,即二进制转码支持,该模块负责把 ARM 指令转换为 X86 指令,所以如果是出于 apk 包大小的考虑,并且可以接受一些性能损失,可以选择删掉 x86 库目录, x86 下配置的 armeabi 目录的 so 库一样可以正常加载使用;
- 3)针对64位 ABI
如果 app 开发者打算支持64位,那么64位的 so 要放全,否则可以选择不单独编译64位的 so,全部使用32位的 so,64位机型默认支持32位 so 的加载。比如 apk 使用第三方的 so 只有32位 abi 的 so,可以考虑去掉 apk 中 lib 目录下的64位 abi 子目录,保证 apk 安装后正常使用。
0x6 备注
其实本文是因为在 Android 的 so 加载上遇到很多坑,相信很多朋友都遇到过 UnsatisfiedLinkError 这个错误,反应在用户的机型上也是千差万别,但是有没有想过,可能不是 apk 逻辑的问题,而是 Android 系统在安装 APK 的时候,由于 PackageManager 的问题,并没有拷贝相应的 SO 呢?可以参考下面第4个链接,作者给出了解决方案,就是当出现 UnsatisfiedLinkError 错误时,手动拷贝 so 来解决的。
so加载流程
调用方式:System.loadLibrary();
//类加载器 和 so名字
Runtime.getRuntime().loadLibrary0(classLoader,libName);
//同步方法。加载肯定是有先后顺序,类似队列形式
synchronized void loadLibrary0(ClassLoader loader, String libname) {
if (libname.indexOf((int)File.separatorChar) != -1) {
throw new UnsatisfiedLinkError(
"Directory separator should not appear in library name: " + libname);
}
String libraryName = libname;
if (loader != null) {
//寻找这个文件是不是存在。 不存在的话返回null,直接抛异常
String filename = loader.findLibrary(libraryName);
if (filename == null) {
// It's not necessarily true that the ClassLoader used
// System.mapLibraryName, but the default setup does, and it's
// misleading to say we didn't find "libMyLibrary.so" when we
// actually searched for "liblibMyLibrary.so.so".
throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
System.mapLibraryName(libraryName) + "\"");
}
//文件存在,就交给下一层处理,这里如果返回的内容不是null就说明加载失败了,且含有错误信息。
//所以这里是重点!!!! 下一步我们就看这里
String error = nativeLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
String filename = System.mapLibraryName(libraryName);
List<String> candidates = new ArrayList<String>();
String lastError = null;
for (String directory : getLibPaths()) {
String candidate = directory + filename;
candidates.add(candidate);
if (IoUtils.canOpenReadOnly(candidate)) {
String error = nativeLoad(candidate, loader);
if (error == null) {
return; // We successfully loaded the library. Job done.
}
lastError = error;
}
}
if (lastError != null) {
throw new UnsatisfiedLinkError(lastError);
}
throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
}
如果这里没有找到就要抛出来so没有找到的错误了,这个也是我们非常常见的错误。所以这里我们很需要知道这个ClassLoader是哪里来的。