dalvik加載、運行過程
我們編寫java代碼都是.java格式的,但是jvm並不能識別.java文件,它只能加載、執行.class文件,所以我們要通過javac命令將.java文件編譯成.class文件,然後通過java命令運行.class文件。其實,如果用C或者Python編寫的程序正確轉換成.class文件後,java虛擬機也是可以識別運行的。
dalvik與jvm差不多,區別就是dalvik只能加載、運行.dex文件(至於如何識別、運行,後面會講到)。我們的Android程序也是用java編寫的,生成的也是.java文件,所以需要把.java文件轉換成.dex文件,dalvik才能執行。IDE編譯、打包的過程,就是將.java文件轉換成.dex文件的過程,我們可以簡單看一下編譯過程,加深理解。
下面是官方介紹的打包流程圖:
總結一下,主要就是這幾步:
1、根據res目錄下的資源文件、AndroidManifest.xml生成R.java文件;
2、處理aidl,生成對應的java文件,如果沒有aidl,則跳過;
3、將前兩步生成的java文件和src目錄下源碼一起編譯成class文件;
4、通過class文件生成成dex文件;
5、將資源文件和dex文件一起打包,生成初始apk;
6、對初始apk簽名 ;
由此可見,項目編譯後,主要結晶就是dex文件。apk的安裝過程,就是把apk解壓成第4步中的dex文件和原始資源文件(比如圖片),運行過程就是dalvik加載、運行dex文件的過程。這裏有兩個過程,一個是加載,一個是運行,它們又是怎樣運作的呢?
dex文件的加載是通過DexClassLoader、PathClassLoader等類來完成的,下面將會從源碼角度對這個過程詳細分析,這也是熱修復技術、插件化技術的核心。
dex的運行就涉及到比較底層的東西了,本文只做一定介紹,瞭解一下dex文件的大概。
Dex文件
通過命令“javac HelloWorld.java”可以生成HelloWorld.class文件。
再通過命令“dx –dex –output=HelloWorld.dex HelloWorld.class”就會生成HelloWorld.dex文件了。
我們通過十六進制文本編輯器打開HelloWorld.dex文件,如下圖:
注意看下面的“Name、Value”,這就是dex文件的標準格式。就像通信協議一樣,dalvik虛擬機讀到什麼內容,就按照預定好的協議執行,這就是dalvik運行dex文件的過程。
ClassLoader
先把我們需要分析的類列出來,捋一捋繼承關係、類的主要作用。
ClassLoader
所有XXXClassLoader的基類,負責加載apk/dex/jar文件;
BootClassLoader
繼承自ClassLoader,負責加載Android系統類庫,我們不會用到;
BaseDexClassLoader
繼承自ClassLoader,看名字就知道是對類加載的抽象;
PathClassLoader
繼承自BaseDexClassLoader,負責加載宿主apk/dex/jar文件;
DexClassLoader
繼承自BaseDexClassLoader,這個比較靈活,每個參數都可以自定義,我們一般用這個來加載自定義的apk/dex/jar文件;
DexPathList
這個類有兩個作用:
① 把dex文件解壓到宿主程序的私有目錄中,因爲jvm只能運行宿主程序的dex文件;
② 通過apk/dex/jar文件生成Element[]數組,方便ClassLoader使用;
由於BootClassLoader是加載系統類庫的,我們就不分析了。我們主要分析PathClassLoader加載apk中的dex文件,分析完這個過程,自定義一個ClassLoader來加載自定義dex文件就不成問題了。
1、PathClassLoader
先看構造方法:
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
// 調用父類的構造方法
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
// 調用父類的構造方法
super(dexPath, null, libraryPath, parent);
}
}
// 參數dexPath:待加載的apk/dex/jar文件路徑;
// 參數optimizedDirectory:dex的輸出路徑,將apk/dex/jar解壓出dex文件,複製到指定路徑,用於dalvik運行
// 參數librarySearchPath:加載時候需要用到的lib庫,這個一般不用,可以傳入Null
// 參數parent:指定父加載器
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.originalPath = dexPath;
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
這裏有兩個“疑點”,一是PathClassLoader是無法指定optimizedDirectory參數的,也就是說,無法保證解壓出來的dex文件在宿主程序中,dalvik就無法運行。另一個就是new一個對象還必須傳一個父類對象作爲參數,爲什麼呢?下面分析loadClass()方法時再說明。
我們再看一下加載的方法,加載方法在基類ClassLoader中:
// 通過類名加載
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
// 在已加載的類中查找
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
// 如果沒有,就讓parent去加載
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
// 如果parent也沒有加載到,就自己加載
clazz = findClass(className);
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}
邏輯是這樣的:
1、先去已加載的列表中查找,如果有(已經加載過),就直接返回;如果沒有,就讓parent去加載;
2、父類仍舊是調用基類ClassLoader的loadClass()方法(如果parent沒有重寫該方法,一般parent都會傳系統自帶的類,甚至是基類,所以基本不存在被重寫的情況),等於是把第一步重複一次;
3、一直找到最頂層的parent,如果頂層parent在已加載列表中還是沒有找到,就會調用findClass()進行加載,並返回;
4、通過parent一層一層地返回,如果最終還是沒有(所有parent都沒有加載到),就自己進行加載;
這樣設計的邏輯就是防止多次加載,一個類只永遠只會被加載一次。
另外要注意的是,只有“PackageName + ClassName + 加載它的ClassLoader”這三個元素一致,才認爲它是同一個類。所以,指定系統的parent能最大限度地保證類的一致性。
上面的邏輯中可以看到,如果沒有加載過,就會調用findClass()方法進行加載,BaseDexClassLoader重載了這個方法:
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
最終調用了DexPathList的findClass()方法,那我們分析一下DexPathList的主要邏輯:
// 構造方法,BaseDexClassLoader的構造方法中會new出DexPathList實例
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
……
// 通過dexPath路徑下的apk/jar/dex文件解壓到optimizedDirectory目錄中,並生成Elements[]數組
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory);
}
private static Element[] makeDexElements(ArrayList<File> files,
File optimizedDirectory) {
ArrayList<Element> elements = new ArrayList<Element>();
// 遍歷該路徑下的文件
for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
if (name.endsWith(DEX_SUFFIX)) {
// 如果是dex文件,就加載該文件
dex = loadDexFile(file, optimizedDirectory);
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
|| name.endsWith(ZIP_SUFFIX)) {
// 如果是壓縮文件,就生成ZipFile
zip = new ZipFile(file);
}
……
if ((zip != null) || (dex != null)) {
// 通過上面生成的DexFile或ZipFile,生成Element對象,添加到List中
elements.add(new Element(file, zip, dex));
}
}
// List轉換成數組返回
return elements.toArray(new Element[elements.size()]);
}
// 通過File生成DexFile
private static DexFile loadDexFile(File file, File optimizedDirectory)
throws IOException {
if (optimizedDirectory == null) {
// 如果輸出路徑爲空,就會使用默認路徑(當前File所在路徑)
return new DexFile(file);
} else {
// 生成解壓路徑
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0);
}
}
/**
* Converts a dex/jar file path and an output directory to an
* output file path for an associated optimized dex file.
*/
private static String optimizedPathFor(File path,
File optimizedDirectory) {
String fileName = path.getName();
if (!fileName.endsWith(DEX_SUFFIX)) {
int lastDot = fileName.lastIndexOf(".");
if (lastDot < 0) {
fileName += DEX_SUFFIX;
} else {
StringBuilder sb = new StringBuilder(lastDot + 4);
sb.append(fileName, 0, lastDot);
sb.append(DEX_SUFFIX);
fileName = sb.toString();
}
}
File result = new File(optimizedDirectory, fileName);
return result.getPath();
}
// 根據name查找dex,將其轉換成Class,返回給ClassLoader
public Class findClass(String name) {
// 遍歷Element[]數組
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
// 通過name嘗試加載Class
Class clazz = dex.loadClassBinaryName(name, definingContext);
if (clazz != null) {
// 如果加載成功,就返回
return clazz;
}
}
}
return null;
}
分析到這裏ClassLoader的加載機制就完結了。