android classloader异常,Android中ClassLoader类加载机制
Android中apk的構建過程
構建apk
如圖 所示,典型 Android 應用模塊的構建流程通常依循下列步驟:
編譯器將您的源代碼轉換成 DEX(Dalvik Executable) 文件(其中包括 Android 設備上運行的字節碼),將所有其他內容轉換成已編譯資源。
APK 打包器將 DEX 文件和已編譯資源合并成單個 APK。 不過,必須先簽署 APK,才能將應用安裝并部署到 Android 設備上。* APK 打包器使用調試或發布密鑰庫簽署您的 APK:
如果您構建的是調試版本的應用(即專用于測試和分析的應用),打包器會使用調試密鑰庫簽署您的應用。 Android Studio 自動使用調試密鑰庫配置新項目。
如果您構建的是打算向外發布的發布版本應用,打包器會使用發布密鑰庫簽署您的應用。 要創建發布密鑰庫,請閱讀在 Android Studio 中簽署您的應用。
在生成最終 APK 之前,打包器會使用 zipalign 工具對應用進行優化,減少其在設備上運行時占用的內存。
構建流程結束時,將獲得應用的調試 APK 或發布 APK,您可使用它們進行部署、測試,或向外部用戶發布。
Android中的類加載機制
1 Android中的ClassLoader
Java中的ClassLoader是加載class文件,而Android中的虛擬機無論是dvm還是art都只能識別dex文件。因此Java中的ClassLoader在Android中不適用。Android中的java.lang.ClassLoader這個類也不同于Java中的java.lang.ClassLoader。
Android中的ClassLoader類型也可分為系統ClassLoader和自定義ClassLoader。其中系統ClassLoader包括3種分別是
BootClassLoader: Android系統啟動時會使用BootClassLoader來預加載常用類,與Java中的Bootstrap ClassLoader不同的是,它并不是由C/C++代碼實現,而是由Java實現的。BootClassLoader是ClassLoader的一個內部類。
PathClassLoader,全名是dalvik/system.PathClassLoader,可以加載已經安裝的Apk,也就是/data/app/package 下的apk文件,也可以加載/vendor/lib, /system/lib下的nativeLibrary。
DexClassLoader,全名是dalvik/system.DexClassLoader,可以加載一個未安裝的apk文件。
PathClassLoader和DexClasLoader都是繼承自 dalviksystem.BaseDexClassLoader,它們的類加載邏輯全部寫在BaseDexClassLoader中。
Android中的ClassLoader
2 ClassLoader源碼分析
在Android中我們主要關心的是PathClassLoader和DexClassLoader。
PathClassLoader用來操作本地文件系統中的文件和目錄的集合。并不會加載來源于網絡中的類。Android采用這個類加載器一般是用于加載系統類和它自己的應用類。這個應用類放置在data/data/包名下。
看一下PathClassLoader的源碼,只有2個構造方法:
package dalvik.system;
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);
}
}
DexClassLoader可以加載一個未安裝的APK,也可以加載其它包含dex文件的JAR/ZIP類型的文件。DexClassLoader需要一個對應用私有且可讀寫的文件夾來緩存優化后的class文件。而且一定要注意不要把優化后的文件存放到外部存儲上,避免使自己的應用遭受代碼注入攻擊。看一下它的源碼,只有1個構造方法:
package dalvik.system;
import java.io.File;
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
可以看到,PathClassLoader和DexClassLoader除了構造方法傳參不同,其它的邏輯都是一樣的。要注意的是DexClassLoader構造方法第2個參數指的是dex優化緩存路徑,這個值是不能為空的。而PathClassLoader對應的dex優化緩存路徑為null是因為Android系統自己決定了緩存路徑。
接下來我們看一下BaseDexClassLoader這個類:
dexPath,指的是在Androdi包含類和資源的jar/apk類型的文件集合,指的是包含dex文件。多個文件用“:”分隔開,用代碼就是File.pathSeparator。
optimizedDirectory,指的是odex優化文件存放的路徑,可以為null,那么就采用默認的系統路徑。
libraryPath,指的是native庫文件存放目錄,也是以“:”分隔。
parent,parent類加載器
可以看到,在BaseDexClassLoader類中初始化了DexPathList這個類的對象。這個類的作用是存放指明包含dex文件、native庫和優化目錄。
# dalvik.system.BaseDexClassLoader
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
dalvik.system.DexPathList封裝了dex路徑,是一個final類,而且訪問權限是包權限,也就是說外界不可繼承,也不可訪問這個類。
BaseDexClassLoader在其構造方法中初始化了DexPathList對象,我們來看一下DexPathList的源碼,我們需要重點關注一下它的成員變量dexElements,它是一個Element[]數組,是包含dex的文件集合。Element是DexPathList的一個靜態內部類。DexPathList的構造方法有4個參數。從其構造方法中也可以看到傳遞過來的classLoade對象和dexPath不能為null,否則就拋出空指針異常。
# dalvik.system.DexPathList
private final Element[] dexElements;
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;
ArrayList suppressedExceptions = new ArrayList();
// 通過makeDexElements方法來獲取Element數組
// splitDexPath(dexPath)方法是用來把我們之前按照“:”分隔的路徑轉為File集合。
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions);
if (suppressedExceptions.size() > 0) {
this.dexElementsSuppressedExceptions =
suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
} else {
dexElementsSuppressedExceptions = null;
}
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}
加載一個dex文件調用的是loadDexFile()方法。
# dalvik.system.DexPathList
private static DexFile loadDexFile(File file, File optimizedDirectory)
throws IOException {
// 如果緩存存放目錄為null就直接創建一個DexFile對象返回
if (optimizedDirectory == null) {
return new DexFile(file);
} else {
// 根據緩存存放目錄和文件名得到一個優化后的緩存文件路徑
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
// 調用DexFile的loadDex()方法來獲取DexFile對象。
return DexFile.loadDex(file.getPath(), optimizedPath, 0);
}
}
DexFile的loadDex()方法如下,內部也做了一些調用。拋開這些細節來講,它的作用就是加載DexFile文件,而且會把優化后的dex文件緩存到對應目錄。
分析到這,我們可以小結一下:在BaseDexClassLoader對象構造方法內,創建了PathDexList對象。而在PathDexList構造方法內部,通過調用一系列方法,把直接包含或者間接包含dex的文件解壓縮并緩存優化后的dex文件,通過PathDexList的成員變量Element[] dexElements來指向這個文件。
到此我們就分析完了BaseDexClassLoader的構造方法。
類加載是按需加載,也就是說當明確需要使用class文件的時候才會加載。我們來看一下在Android中ClassLoader的loadeClass()方法。
與在Java中的loadClass()方法主要流程是類似的,不過因為Android中BootClassLoader是用Java代碼寫的,所以可以直接當作系統類加載器的parent類加載器。在Android中如果parent類加載器找不到類,最終還是會調用ClassLoader對象自己的findClass()方法。這個與在Java中邏輯是一樣的。
我們可以去看一下BaseDexClassLoader類的findClass()方法。
# dalvik.system.BaseDexClassLoader
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
List suppressedExceptions = new ArrayList();
// 調用DexPathList對象的findClass()方法
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;
}
可以看到,實際上BaseDexClassLoader調用的是其成員變量DexPathList pathList的findClass()方法。
# dalvik.system.DexPathList
public Class findClass(String name, List suppressed) {
// 遍歷Element
for (Element element : dexElements) {
// 獲取DexFile,然后調用DexFile對象的loadClassBinaryName()方法來加載Class文件。
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;
}
從上面的代碼中我們也可以看到,實際上DexPathList最終還是遍歷其自身的Element[]數組,獲取DexFile對象來加載Class文件。我們之前講DexPathList構造方法內是調用其makeDexElements()方法來創建Element[]數組的,而且也提到了如果zip文件或者dex文件二者之一不為null,就把元素添加進來,而添加進來的zip存在不為null也不包含dex文件的可能。從上面的代碼中也可以看到,獲取Class的時候跟這個zip文件沒什么關系,調用的是dex文件對應的DexFile的方法來獲取Class。
數組的遍歷是有序的,假設有兩個dex文件存放了二進制名稱相同的Class,類加載器肯定就會加載在放在數組前面的dex文件中的Class。現在很多熱修復技術就是把修復的dex文件放在DexPathList中Element[]數組的前面,這樣就實現了修復后的Class搶先加載了,達到了修改bug的目的。
Android加載一個Class是調用DexFile的defineClass()方法。而不是調用ClassLoader的defineClass()方法。這一點與Java不同,畢竟Android虛擬機加載的dex文件,而不是class文件。
總結
Android中的類加載器是BootClassLoader、PathClassLoader、DexClassLoader,其中BootClassLoader是虛擬機加載系統類需要用到的,PathClassLoader是App加載自身dex文件中的類用到的,DexClassLoader可以加載直接或間接包含dex文件的文件,如APK等。
PathClassLoader和DexClassLoader都繼承自BaseDexClassLoader,它的一個DexPathList類型的成員變量pathList很重要。DexPathList中有一個Element類型的數組dexElements,這個數組中存放了包含dex文件(對應的是DexFile)的元素。BaseDexClassLoader加載一個類,最后調用的是DexFile的方法進行加載的。
無論是熱修復還是插件化技術中都利用了類加載機制,所以深入理解Android中的類加載機制對于理解這些技術的原理很有幫助
總結
以上是生活随笔為你收集整理的android classloader异常,Android中ClassLoader类加载机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: android推送如何推送不在线设备,推
- 下一篇: html正则表达式search,正则表达