前言
上兩篇文章分析了資源的載入和程式,Activity啟動相關的內容,這篇是Dex載入相關的內容了,本篇結束,我們也就可以開始對於一些熱修復,外掛化框架的實現剖析了。
Android中ClassLoader
上圖為Android中ClassLoader的類圖,與JVM不同,Dalvik的虛擬機器不能用ClassCload直接載入.dex,Android從ClassLoader派生出了兩個類:DexClassLoader
和PathClassLoader
。而這兩個類就是我們載入dex檔案的關鍵,這兩者的區別是:
-
DexClassLoader:可以載入jar/apk/dex,可以從SD卡中載入未安裝的apk。
-
PathClassLoader:要傳入系統中apk的存放Path,所以只能載入已經安裝的apk檔案。
Dalvik虛擬機器如同其他Java虛擬機器一樣,在執行程式時首先需要將對應的類載入到記憶體中。而在Java標準的虛擬機器中,類載入可以從class檔案中讀取,也可以是其他形式的二進位制流。因此,我們常常利用這一點,在程式執行時手動載入Class,從而達到程式碼動態載入執行的目的。
只不過Android平臺上虛擬機器執行的是Dex位元組碼,一種對class檔案優化的產物,傳統Class檔案是一個Java原始碼檔案會生成一個.class檔案,而Android是把所有Class檔案進行合併,優化,然後生成一個最終的class.dex,目的是把不同class檔案重複的東西只需保留一份,如果我們的Android應用不進行分dex處理,最後一個應用的apk只會有一個dex檔案。
PathClassLoader和DexClassLoader都繼承自BaseDexClassLoader,其中的主要邏輯都是在BaseDexClassLoader完成的。
可以看出在載入類時首先判斷這個類是否之前被載入過,如果有則直接返回,如果沒有則首先嚐試讓parent ClassLoader進行載入,載入不成功才在自己的findClass中進行載入。這和java虛擬機器中常見的雙親委派模型一致的,這種模型並不是一個強制性的約束模型,比如你可以繼承ClassLoader複寫loadCalss方法來破壞這種模型,只不過雙親委派模是一種被推薦的實現類載入器的方式,而且jdk1.2以後已經不提倡使用者在覆蓋loadClass方法,而應該把自己的類載入邏輯寫到findClass中。
ClassLoader原始碼分析
- BaseDexClassLoader
public class BaseDexClassLoader extends ClassLoader {
private final String originalPath;
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.originalPath = dexPath;
//構造DexPahtList物件
this.pathList =
new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//Class 查詢在PathList中根據類名進行查詢
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
@Override
protected URL findResource(String name) {
return pathList.findResource(name);
}
@Override
protected Enumeration<URL> findResources(String name) {
return pathList.findResources(name);
}
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
@Override
protected synchronized Package getPackage(String name) {
if (name != null && !name.isEmpty()) {
Package pack = super.getPackage(name);
if (pack == null) {
pack = definePackage(name, "Unknown", "0.0", "Unknown",
"Unknown", "0.0", "Unknown", null);
}
return pack;
}
return null;
}
@Override
public String toString() {
return getClass().getName() + "[" + originalPath + "]";
}
}
複製程式碼
通過原始碼可以看出,在BaseDexClassLoader的建構函式中建立了DexPathList例項,在BaseDexClassLoader中,對於類的查詢和資源的查詢,都是通過其中的DexPathList例項來進行的。對於類的等相關資訊的查詢是在DexPathList中實現的,接下來,我們看一下DexPathList的原始碼實現。
final class DexPathList {
private static final String DEX_SUFFIX = ".dex";
private static final String JAR_SUFFIX = ".jar";
private static final String ZIP_SUFFIX = ".zip";
private static final String APK_SUFFIX = ".apk";
/** class definition context */
private final ClassLoader definingContext;
//Dex,Resource元素
private final Element[] dexElements;
//Native庫的地址路徑
private final File[] nativeLibraryDirectories;
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
if (definingContext == null) {
throw new NullPointerException("definingContext == null");
}
if (dexPath == null) {
throw new NullPointerException("dexPath == null");
}
if (optimizedDirectory != null) {
if (!optimizedDirectory.exists()) {
throw new IllegalArgumentException(
"optimizedDirectory doesn't exist: "
+ optimizedDirectory);
}
if (!(optimizedDirectory.canRead()
&& optimizedDirectory.canWrite())) {
throw new IllegalArgumentException(
"optimizedDirectory not readable/writable: "
+ optimizedDirectory);
}
}
this.definingContext = definingContext;
this.dexElements =
makeDexElements(splitDexPath(dexPath), optimizedDirectory);
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}
}
複製程式碼
在建構函式中,根據dexPath,構建一個DexElement陣列,在後面對於類的查詢就會在該陣列中進行查詢。
- makeDexElements
/**
* 根據給予的地址構建一個Element陣列
*/
private static Element[] makeDexElements(ArrayList<File> files,
File optimizedDirectory) {
ArrayList<Element> elements = new ArrayList<Element>();
/*
* Open all files and load the (direct or contained) dex files
* up front.
*/
for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
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 if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
|| name.endsWith(ZIP_SUFFIX)) {
try {
zip = new ZipFile(file);
} catch (IOException ex) {
}
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ignored) {
}
} else {
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
複製程式碼
根據傳遞的路徑,然後解析路徑下的內容,根據dex,zip等構建Element例項,然後將這個例項新增到Element陣列之中。
- loadDexFile
/**
* 構造一個DexFile物件
*/
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, 0);
}
}
複製程式碼
Element資料結構
static class Element {
public final File file;
public final ZipFile zipFile;
public final DexFile dexFile;
public Element(File file, ZipFile zipFile, DexFile dexFile) {
this.file = file;
this.zipFile = zipFile;
this.dexFile = dexFile;
}
}
複製程式碼
- 類的查詢
/**
* 類的查詢,從DexElements中,逐個DexFile中查詢,如果找到,
* 就載入這個類,然後返回
*/
public Class findClass(String name) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext);
if (clazz != null) {
return clazz;
}
}
}
return null;
}
複製程式碼
根據傳遞的目錄,載入相應的dex,apk來構建層Element例項。
- ART和Dalvik
Android Runtime(縮寫為ART),在Android 5.0及後續Android版本中作為正式的執行時庫取代了以往的Dalvik虛擬機器。ART能夠把應用程式的位元組碼轉換為機器碼,是Android所使用的一種新的虛擬機器。它與Dalvik的主要不同在於:Dalvik採用的是JIT技術,位元組碼都需要通過即時編譯器轉換為機器碼,這會拖慢應用的執行效率,而ART採用Ahead-of-time(AOT)技術,應用在第一次安裝的時候,位元組碼就會預先編譯成機器碼,這個過程叫做預編譯。ART同時也改善了效能、垃圾回收(Garbage Collection)、應用程式出錯以及效能分析。但是請注意,執行時記憶體佔用空間較少同樣意味著編譯二進位制需要更高的儲存。
ART模式相比原來的Dalvik,會在安裝APK的時候,使用Android系統自帶的dex2oat工具把APK裡面的.dex檔案轉化成OAT檔案,OAT檔案是一種Android私有ELF檔案格式,它不僅包含有從DEX檔案翻譯而來的本地機器指令,還包含有原來的DEX檔案內容。這使得我們無需重新編譯原有的APK就可以讓它正常地在ART裡面執行,也就是我們不需要改變原來的APK程式設計介面。ART模式的系統裡,同樣存在DexClassLoader類,包名路徑也沒變,只不過它的具體實現與原來的有所不同,但是介面是一致的。實際上,ART執行時就是和Dalvik虛擬機器一樣,實現了一套完全相容Java虛擬機器的介面。
- dexopt還是dexoat
在安裝的優化過程,dexopt函式會啟動一個子程式來執行優化,同時會根據目前系統使用的是Dalvik虛擬機器還是ART來決定將apk轉換層何種格式,如果轉換層odex格式,則呼叫/system/bin/dexopt檔案來執行轉化,如果轉換層ART的oat格式,則呼叫/system/bin/dex2oat來執行轉換。
則dexopt和dexoat中會執行一些優化操作,這個優化操作也會影響到我們後續類的動態載入。但由於上層介面一致,因此,作為開發者無需關心安裝時具體的優化方式。
參考資料
Dexopt優化和驗證Dalvik (Dalvik Optimization and Verification With dexopt)