BOOTCLASSPATH簡介
1.BOOTCLASSPATH是Android Linux的一個環境變量,可以在adb shell下用$BOOTCLASSPATH看到。
2.BOOTCLASSPATH於/init.rc文件中export,如果沒有找到的話,可以在init.rc中import的文件里找到(如import /init.environ.rc)。
3.init.rc文件存在於boot.img的ramdisk映像中。如果僅僅是修改/init.rc文件,重啟后會被ramdisk恢復,所以直接修改是沒有效果的。
4.boot.img是一種特殊的Android定制格式,由boot header,kernel,ramdisk以及second stage loader(可選)組成,詳見android/system/core/mkbootimg/bootimg.h。
boot.img空間結構:
** +-----------------+
** | boot header | 1 page
** +-----------------+
** | kernel | n pages
** +-----------------+
** | ramdisk | m pages
** +-----------------+
** | second stage | o pages
** +-----------------+
典型的ramdisk文件結構:
./init.trout.rc
./default.prop
./proc
./dev
./init.rc
./init
./sys
./init.goldfish.rc
./sbin
./sbin/adbd
./system
./data
BOOTCLASSPATH的作用
以Android4.4手機的BOOTCLASSPATH為例:
export BOOTCLASSPATH /system/framework/core.jar:/system/framework/conscrypt.jar:/system/framework/okhttp.jar...
當kernel啟動時1號進程init解析init.rc,將/system/framework下的jar包路徑export出來。
Dalvik虛擬機在初始化過程中,會讀取環境變量BOOTCLASSPATH,用於之后的類加載和優化。
Dalvik虛擬機的啟動和dexopt流程
從Dalvik虛擬機的啟動過程分析 一文可以知道,Zygote會在啟動后創建Dalvik虛擬機實例,並進行初始化。
那我們就接着Dalvik虛擬機初始化后開始探究它是如何通過BOOTCLASSPATH來進行dex優化的:

1.1. VM initialization
android/dalvik/vm/Init.cpp
std::string dvmStartup(int argc, const char* const argv[],
bool ignoreUnrecognized, JNIEnv* pEnv)
{
...
ALOGV("VM init args (%d):", argc);
...
setCommandLineDefaults(); // ---> 讀取BOOTCLASSPATH
...
if (!dvmClassStartup()) { // ---> 初始化bootstrap class loader
return "dvmClassStartup failed";
}
}
1.2. 讀取BOOTCLASSPATH
static void setCommandLineDefaults()
{
const char* envStr = getenv("CLASSPATH");
if (envStr != NULL) {
gDvm.classPathStr = strdup(envStr);
} else {
gDvm.classPathStr = strdup(".");
}
envStr = getenv("BOOTCLASSPATH"); // 讀取到BOOTCLASSPATH環境變量
if (envStr != NULL) {
gDvm.bootClassPathStr = strdup(envStr);
} else {
gDvm.bootClassPathStr = strdup(".");
}
...
}
就這樣,BOOTCLASSPATH的值被保存到gDvm.bootClassPathStr中。
2.1. 初始化bootstrap class loader
android/dalvik/vm/oo/Class.cpp
bool dvmClassStartup()
{
...
/*
* Process the bootstrap class path. This means opening the specified
* DEX or Jar files and possibly running them through the optimizer.
*/
assert(gDvm.bootClassPath == NULL);
processClassPath(gDvm.bootClassPathStr, true); // 下一步
if (gDvm.bootClassPath == NULL)
return false;
}
2.2. 將路徑、Zip文件和Dex文件的list轉換到ClassPathEntry結構體當中
static ClassPathEntry* processClassPath(const char* pathStr, bool isBootstrap)
{
ClassPathEntry* cpe = NULL;
...
if (!prepareCpe(&tmp, isBootstrap)) {}
}
2.3. 根據cpe打開文件
static bool prepareCpe(ClassPathEntry* cpe, bool isBootstrap)
{
...
if ((strcmp(suffix, "jar") == 0) || (strcmp(suffix, "zip") == 0) ||
(strcmp(suffix, "apk") == 0)) {
JarFile* pJarFile = NULL;
/* 打開jar包,找到class.dex或jar包旁邊的.odex文件 */
if (dvmJarFileOpen(cpe->fileName, NULL, &pJarFile, isBootstrap) == 0) {
cpe->kind = kCpeJar;
cpe->ptr = pJarFile;
return true;
}
} else if (strcmp(suffix, "dex") == 0) {
RawDexFile* pRawDexFile = NULL;
/* 與dvmJarFileOpen函數作用類似,是由它復制過來重構的 */
if (dvmRawDexFileOpen(cpe->fileName, NULL, &pRawDexFile, isBootstrap) == 0) {
cpe->kind = kCpeDex;
cpe->ptr = pRawDexFile;
return true;
}
} else {
ALOGE("Unknown type suffix '%s'", suffix);
}
...
}
3. 打開jar包,找到class.dex或jar包旁邊的.odex文件
android/dalvik/vm/JarFile.cpp
int dvmJarFileOpen(const char* fileName, const char* odexOutputName,
JarFile** ppJarFile, bool isBootstrap)
{
...
/* Even if we're not going to look at the archive, we need to
* open it so we can stuff it into ppJarFile.
*/
if (dexZipOpenArchive(fileName, &archive) != 0)
goto bail;
archiveOpen = true;
/* If we fork/exec into dexopt, don't let it inherit the archive's fd.
*/
dvmSetCloseOnExec(dexZipGetArchiveFd(&archive));
/* First, look for a ".odex" alongside the jar file. It will
* have the same name/path except for the extension.
*/
fd = openAlternateSuffix(fileName, "odex", O_RDONLY, &cachedName);
if (fd >= 0) {
ALOGV("Using alternate file (odex) for %s ...", fileName);
/* 讀、驗證header和dependencies */
if (!dvmCheckOptHeaderAndDependencies(fd, false, 0, 0, true, true)) {
ALOGE("%s odex has stale dependencies", fileName);
free(cachedName);
cachedName = NULL;
close(fd);
fd = -1;
goto tryArchive;
} else {
ALOGV("%s odex has good dependencies", fileName);
//TODO: make sure that the .odex actually corresponds
// to the classes.dex inside the archive (if present).
// For typical use there will be no classes.dex.
}
} else {
ZipEntry entry;
tryArchive:
/*
* Pre-created .odex absent or stale. Look inside the jar for a
* "classes.dex".
*/
...
}
4.讀、驗證opt的header,讀、驗證dependencies
android/dalvik/vm/analysis/DexPrepare.cpp
bool dvmCheckOptHeaderAndDependencies(int fd, bool sourceAvail, u4 modWhen,
u4 crc, bool expectVerify, bool expectOpt)
{
...
/*
* Verify dependencies on other cached DEX files. It must match
* exactly with what is currently defined in the bootclasspath.
*/
ClassPathEntry* cpe;
u4 numDeps;
numDeps = read4LE(&ptr);
ALOGV("+++ DexOpt: numDeps = %d", numDeps);
for (cpe = gDvm.bootClassPath; cpe->ptr != NULL; cpe++) {
const char* cacheFileName =
dvmPathToAbsolutePortion(getCacheFileName(cpe));
assert(cacheFileName != NULL); /* guaranteed by Class.c */
const u1* signature = getSignature(cpe);
size_t len = strlen(cacheFileName) +1;
u4 storedStrLen;
if (numDeps == 0) {
/* more entries in bootclasspath than in deps list */
ALOGI("DexOpt: not all deps represented");
goto bail;
}
storedStrLen = read4LE(&ptr);
if (len != storedStrLen ||
strcmp(cacheFileName, (const char*) ptr) != 0)
{
ALOGI("DexOpt: mismatch dep name: '%s' vs. '%s'",
cacheFileName, ptr);
goto bail;
}
ptr += storedStrLen;
if (memcmp(signature, ptr, kSHA1DigestLen) != 0) {
ALOGI("DexOpt: mismatch dep signature for '%s'", cacheFileName);
goto bail;
}
ptr += kSHA1DigestLen;
ALOGV("DexOpt: dep match on '%s'", cacheFileName);
numDeps--;
}
if (numDeps != 0) {
/* more entries in deps list than in classpath */
ALOGI("DexOpt: Some deps went away");
goto bail;
}
...
}
實際應用
打通了Dalvik dexopt的這個流程,那這到底又有什么用呢?
讓我們看看實際開發過程中的手機升級binary后無法boot到Home界面的log:
1 AndroidRuntime >>>>>> AndroidRuntime START com.android.internal.os.ZygoteInit <<<<<< 2 AndroidRuntime CheckJNI is ON 3 dalvikvm DexOpt: Some deps went away 4 dalvikvm /system/framework/core-junit.jar odex has stale dependencies 5 dalvikvm DexOpt: --- BEGIN 'core-junit.jar' (bootstrap=1) --- 6 dalvikvm DexOpt: load 42ms, verify+opt 25ms, 143956 bytes 7 dalvikvm DexOpt: --- END 'core-junit.jar' (success) --- 8 dalvikvm DEX prep '/system/framework/core-junit.jar': unzip in 1ms, rewrite 126ms 9 dalvikvm DexOpt: mismatch dep name: '/data/dalvik-cache/system@framework@core-junit.jar@classes.dex' vs. '/system/framework/conscrypt.odex' 10 dalvikvm /system/framework/bouncycastle.jar odex has stale dependencies 11 dalvikvm DexOpt: --- BEGIN 'bouncycastle.jar' (bootstrap=1) --- 12 dalvikvm DexOpt: Some deps went away 13 dalvikvm /system/framework/core-junit.jar odex has stale dependencies 14 dalvikvm DexOpt: load 33ms, verify+opt 350ms, 681812 bytes 15 dalvikvm DexOpt: --- END 'bouncycastle.jar' (success) --- 16 dalvikvm DEX prep '/system/framework/bouncycastle.jar': unzip in 57ms, rewrite 548ms 17 dalvikvm DexOpt: mismatch dep name: '/data/dalvik-cache/system@framework@core-junit.jar@classes.dex' vs. '/system/framework/conscrypt.odex' 18 dalvikvm /system/framework/ext.jar odex has stale dependencies 19 dalvikvm DexOpt: --- BEGIN 'ext.jar' (bootstrap=1) --- 20 dalvikvm DexOpt: Some deps went away 21 dalvikvm /system/framework/core-junit.jar odex has stale dependencies 22 dalvikvm DexOpt: mismatch dep name: '/data/dalvik-cache/system@framework@core-junit.jar@classes.dex' vs. '/system/framework/conscrypt.odex' 23 dalvikvm /system/framework/bouncycastle.jar odex has stale dependencies
根據前面的流程,結合log我們就可以分析出,DexOpt: mismatch dep name: '/data/dalvik-cache/system@framework@core-junit.jar@classes.dex' vs. '/system/framework/conscrypt.odex'是錯誤所在,是由於data/dalvik-cache/下的dex cache文件和system/framework/下的jar文件驗證依賴關系時候對應不上。
從函數dvmCheckOptHeaderAndDependencies()可以得知,BOOTCLASSPATH和cache必須是完全一致的
嘗試刪除所有cache文件,重啟還是不行。那么應該想到BOOTCLASSPATH和實際的system/framework/的jar包不一致,才會導致和其生成的cache不一致。
對比一下果然不一致,issue trouble-shooted.
解決方法:把對應boot.img也燒進去,這樣BOOTCLASSPATH就能更新一致,dex優化就能正確進行下去。
