Android提供機制來在應用運作的過程中動态加載dex檔案中的類,ClassLoader是抽象類,一般使用DexClassLoader或者PathClassLoader加載,他們的差別是
- DexClassLoader可以加載jar/apk/dex,可以從SD卡中加載未安裝的apk
- PathClassLoader隻能加載系統中已經安裝過的apk
首先看一下動态代碼加載如何加載dex檔案中的類,下面給出一個DexClassLoader動态加載dex檔案的例子:
final File optimizedDexOutputPath = new File(Environment
.getExternalStorageDirectory().toString()
+ File.separator
+ "test.dex");
DexClassLoader cl = new DexClassLoader(
optimizedDexOutputPath.getAbsolutePath(), Environment
.getExternalStorageDirectory().toString(), null,
getClassLoader());
Class Clazz = null;
try {
Clazz = cl.loadClass("com.dynamic.DynamicTest");
Object lib = Clazz.newInstance();
Method m = Clazz.getDeclaredMethod("hehe");
m.invoke(lib);
} catch (Exception exception) {
// Handle exception gracefully here.
exception.printStackTrace();
}
接下來以這個例子往下分析DexClassLoader是如何一步一步加載dex檔案中的類,底層是如何将dex檔案轉換成oat檔案的。
DexClassLoader建立流程(java層):
首先看DexClassLoader構造函數(libcore\dalvik\src\main\java\dalvik\system\DexClassLoader.java):
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
DexClassLoader隻是簡單的對BaseDexClassLoader做了一下封裝,具體的實作還是在父類裡(libcore\dalvik\src\main\java\dalvik\system\BaseDexClassLoader.java):
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
這裡建立了一個DexPathList執行個體,後邊類的查找都會用到(libcore\dalvik\src\main\java\dalvik\system\DexPathList.java):
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
......
this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions);
......
}
private static Element[] makePathElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions) {
List<Element> elements = new ArrayList<>();
for (File file : files) {
File zip = null;
File dir = new File("");
DexFile dex = null;
String path = file.getPath();
String name = file.getName();
if (path.contains(zipSeparator)) {
String split[] = path.split(zipSeparator, );
zip = new File(split[]);
dir = new File(split[]);
} else if (file.isDirectory()) {
elements.add(new Element(file, true, null, null));
} else if (file.isFile()) {
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
} else {
zip = file;
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException suppressed) {
suppressedExceptions.add(suppressed);
}
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(dir, false, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
private static DexFile loadDexFile(File file, File optimizedDirectory)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, );
}
}
private static String optimizedPathFor(File path,
File optimizedDirectory) {
String fileName = path.getName();
if (!fileName.endsWith(DEX_SUFFIX)) {
int lastDot = fileName.lastIndexOf(".");
if (lastDot < ) {
fileName += DEX_SUFFIX;
} else {
StringBuilder sb = new StringBuilder(lastDot + );
sb.append(fileName, , lastDot);
sb.append(DEX_SUFFIX);
fileName = sb.toString();
}
}
File result = new File(optimizedDirectory, fileName);
return result.getPath();
}
optimizedDirectory是用來緩存我們需要加載的dex檔案的,并建立一個DexFile對象,如果它為null,那麼會直接使用dex檔案原有的路徑來建立DexFile對象。
optimizedDirectory必須是一個内部存儲路徑,無論哪種動态加載,加載的可執行檔案一定要存放在内部存儲。DexClassLoader可以指定自己的optimizedDirectory,是以它可以加載外部的dex,因為這個dex會被複制到内部路徑的optimizedDirectory;而PathClassLoader沒有optimizedDirectory,是以它隻能加載内部的dex,這些大都是存在系統中已經安裝過的apk裡面的。
DexFile建立流程(native層):
從上一步的分析,DexClassLoader最終會調用DexFile類将加載的dex檔案緩存在DexPathList對象中。
DexFile建立流程如下(libcore\dalvik\src\main\java\dalvik\system\DexFile.java):
public DexFile(File file) throws IOException {
this(file.getPath());
}
public DexFile(String fileName) throws IOException {
mCookie = openDexFile(fileName, null, );
mFileName = fileName;
guard.open("close");
}
private static Object openDexFile(String sourceName, String outputName, int flags) throws IOException {
return openDexFileNative(new File(sourceName).getAbsolutePath(),
(outputName == null) ? null : new File(outputName).getAbsolutePath(),
flags);
}
private static native Object openDexFileNative(String sourceName, String outputName, int flags);
DexFile構造函數調用openDexFile,然後調用本地方法openDexFileNative,進入到native層的dex檔案建立。
openDexFileNative函數在art\runtime\native\dalvik_system_DexFile.cc檔案中下面的代碼導出了openDexFileNative符号。
static JNINativeMethod gMethods[] = {
NATIVE_METHOD(DexFile, closeDexFile, "(Ljava/lang/Object;)V"),
NATIVE_METHOD(DexFile, defineClassNative,
"(Ljava/lang/String;Ljava/lang/ClassLoader;Ljava/lang/Object;)Ljava/lang/Class;"),
NATIVE_METHOD(DexFile, getClassNameList, "(Ljava/lang/Object;)[Ljava/lang/String;"),
NATIVE_METHOD(DexFile, isDexOptNeeded, "(Ljava/lang/String;)Z"),
NATIVE_METHOD(DexFile, getDexOptNeeded,
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)I"),
NATIVE_METHOD(DexFile, openDexFileNative,
"(Ljava/lang/String;Ljava/lang/String;I)Ljava/lang/Object;"),
};
通過觀察上面的全局變量可以發現,openDexFileNative所代表的函數是DexFile_openDexFileNative。
static jobject DexFile_openDexFileNative(
JNIEnv* env, jclass, jstring javaSourceName, jstring javaOutputName, jint) {
ScopedUtfChars sourceName(env, javaSourceName);
if (sourceName.c_str() == nullptr) {
return ;
}
NullableScopedUtfChars outputName(env, javaOutputName);
if (env->ExceptionCheck()) {
return ;
}
ClassLinker* linker = Runtime::Current()->GetClassLinker();
std::vector<std::unique_ptr<const DexFile>> dex_files;
std::vector<std::string> error_msgs;
dex_files = linker->OpenDexFilesFromOat(sourceName.c_str(), outputName.c_str(), &error_msgs);
if (!dex_files.empty()) {
jlongArray array = ConvertNativeToJavaArray(env, dex_files);
if (array == nullptr) {
ScopedObjectAccess soa(env);
for (auto& dex_file : dex_files) {
if (Runtime::Current()->GetClassLinker()->IsDexFileRegistered(*dex_file)) {
dex_file.release();
}
}
}
return array;
} else {
ScopedObjectAccess soa(env);
CHECK(!error_msgs.empty());
// The most important message is at the end. So set up nesting by going forward, which will
// wrap the existing exception as a cause for the following one.
auto it = error_msgs.begin();
auto itEnd = error_msgs.end();
for ( ; it != itEnd; ++it) {
ThrowWrappedIOException("%s", it->c_str());
}
return nullptr;
}
}
這個函數非常大,其主要的幾個動作如下:
- 構造OatFileAssistant對象:這個對象的主要作用是協助dex檔案解析成oat檔案
- 周遊ClassLinker的成員變量:oat_files_,目的是檢視目前的dex檔案是否已經被解析成oat了
- 如果目前dex已經被解析成了oat檔案,那麼就跳過dex的解析,直接進入下一步),反之,如果目前dex檔案并沒有被解析過,那麼就會走:oat_file_assistant.MakeUpToDate
-
填充dex_files對象并傳回,這裡也有兩種情況(區分oat or dex):
1.oat_file_assistant.LoadDexFiles
2.DexFile::Open
其中我們需要關注的點其實隻有兩點共兩個函數:
- oat_file_assistant.MakeUpToDate
- oat_file_assistant.LoadDexFiles
oat_file_assistant.MakeUpToDate的實作如下(art\runtime\Oat_file_assistant.cc):
bool OatFileAssistant::MakeUpToDate(std::string* error_msg) {
switch (GetDexOptNeeded()) {
case kNoDexOptNeeded: return true;
case kDex2OatNeeded: return GenerateOatFile(error_msg);
case kPatchOatNeeded: return RelocateOatFile(OdexFileName(), error_msg);
case kSelfPatchOatNeeded: return RelocateOatFile(OatFileName(), error_msg);
}
UNREACHABLE();
}
由于我們這裡是第一次load,是以就會進入到GenerateOatFile:
bool OatFileAssistant::GenerateOatFile(std::string* error_msg) {
......
std::vector<std::string> args;
args.push_back("--dex-file=" + std::string(dex_location_));
args.push_back("--oat-file=" + oat_file_name);
if (!OS::FileExists(dex_location_)) {
*error_msg = "Dex location " + std::string(dex_location_) + " does not exists.";
return false;
}
if (!Dex2Oat(args, error_msg)) {
TEMP_FAILURE_RETRY(unlink(oat_file_name.c_str()));
return false;
}
ClearOatFileCache();
return true;
}
可以看到這支函數的主要最用是調用Dex2Oat,在最終調用之前會去檢查一下檔案是否存在。Dex2Oat函數實作如下:
bool OatFileAssistant::Dex2Oat(const std::vector<std::string>& args,
std::string* error_msg) {
......
std::vector<std::string> argv;
argv.push_back(runtime->GetCompilerExecutable());
argv.push_back("--runtime-arg");
......
return Exec(argv, error_msg);
}
argv是此處的關鍵,不光會傳入dst,src等基本資訊,還會有諸如debugger,classpath等資訊,接下來跳轉到(art/runtime/utils.cc)執行Exec函數:
bool Exec(std::vector<std::string>& arg_vector, std::string* error_msg) {
......
const char* program = arg_vector[].c_str();
......
pid_t pid = fork();
if (pid == ) {
......
execv(program, &args[]);
......
} else {
......
pid_t got_pid = TEMP_FAILURE_RETRY(waitpid(pid, &status, ));
......
return true;
}
這個函數的重點是在fork,而fork之後會在子程序執行arg帶入的第一個參數指定的那個可執行檔案。
而在fork的父程序,則會繼續監聽子程序的運作狀态并一直wait,是以對于上層來說..這邊就是一個同步調用了。
再回頭看一下argv的第一個參數:argv.push_back(runtime->GetCompilerExecutable());位于(art\runtime\Runtime.cc)
std::string Runtime::GetCompilerExecutable() const {
if (!compiler_executable_.empty()) {
return compiler_executable_;
}
std::string compiler_executable(GetAndroidRoot());
compiler_executable += (kIsDebugBuild ? "/bin/dex2oatd" : "/bin/dex2oat");
return compiler_executable;
}
是以..如果非debug狀态下,我們用到可執行檔案就是:/bin/dex2oat,是以到這邊,我們大緻就了解了dex檔案是如何轉換到oat檔案的。
DexClassLoader查找類過程
當DexClassLoader.loadClass被調用時,同樣繼承自父類BaseDexClassLoader,BaseDexClassLoader又繼承自父類ClassLoader(libcore\libart\src\main\java\java\lang\ClassLoader.java):
public Class<?> loadClass(String className) throws ClassNotFoundException {
return loadClass(className, false);
}
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
clazz = findClass(className);
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}
loadClass方法調用了findClass方法,而BaseDexClassLoader重載了這個方法(libcore\dalvik\src\main\java\dalvik\system\BaseDexClassLoader.java):
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
這裡就調用了之前建立DexClassLoader時建立的pathList對象的findClass方法(libcore\dalvik\src\main\java\dalvik\system\DexPathList.java):
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
這裡周遊了之前pathList所有的DexFile執行個體,其實也就是周遊了所有加載過的dex檔案,再調用loadClassBinaryName方法一個個嘗試能不能加載想要的類(libcore\dalvik\src\main\java\dalvik\system\DexFile.java):
public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
return defineClass(name, loader, mCookie, suppressed);
}
private static Class defineClass(String name, ClassLoader loader, Object cookie,
List<Throwable> suppressed) {
Class result = null;
try {
result = defineClassNative(name, loader, cookie);
} catch (NoClassDefFoundError e) {
if (suppressed != null) {
suppressed.add(e);
}
} catch (ClassNotFoundException e) {
if (suppressed != null) {
suppressed.add(e);
}
}
return result;
}
private static native Class defineClassNative(String name, ClassLoader loader, Object cookie)
throws ClassNotFoundException, NoClassDefFoundError;
loadClassBinaryName實際就是封裝調用了defineClass(從代碼中可以看出,傳入的參數中有一個是mCookie,這個變量儲存的正是之前加載的dex檔案清單),而defineClass實際調用的是native方法defineClassNative,隻是額外做了異常處理.defineClassNative的實作在art/runtime/native/dalvik_system_DexFile.cc
參考資料:
Android動态加載——DexClassloader分析
Art下DexClassLoader将dex轉化為oat檔案格式的過程
Android ClassLoader