Android熱修復入門:Android中的ClassLoader
ClassLoader簡介
對於Java程式來說,編寫程式就是編寫類,執行程式也就是執行類(編譯得到的class檔案),其中起到關鍵作用的就是類載入器ClassLoader。
任何一個Java程式都是若干個class檔案組成的一個完整的Java程式,在程式執行的時候,需要將class檔案載入到JVM中才可以使用後,負責載入這些class檔案的就是Java的類載入(ClassLoader)機制。
因此ClassLoader的作用簡單來說就是載入class檔案,提供給程式執行時使用。
ClassLoader的雙親委託模型
先看jdk中的ClassLoader類的構造方法,其需要傳入一個父類載入器,並持有該引用:
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
private static Void checkCreateClassLoader() {
return null;
}
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
}
當類載入器收到載入類或資源的請求時,通常都是先委託給父類載入器載入,也就是說只有當父類載入器找不到指定類或資源的時候,自身才會執行實際類載入過程:
- 源ClassLoader先判斷該Class是否已載入,如果已載入,則直接返回Class,如果沒有則委託給父類載入器
- 父類載入器判斷是否載入過該Class,如果已載入,則直接返回Class,如果沒有則委託給祖父類載入器。
- 依次類推,直到始祖類載入器(引用類載入器)。
- 始祖類載入器判斷是否載入過該Class,如果已載入,則直接返回Class,如果沒有則嘗試從其對應的類路徑下尋找class位元組碼檔案並載入。如果加入成功,則直接返回class,如果載入失敗,則委託給始祖類載入器的子類載入器。
- 始祖類載入器的子類載入器嘗試從其對應的類路徑下尋找class位元組碼檔案並載入。如果載入成功,則直接返回Class,如果載入失敗,則委託給始祖類載入器的孫類載入器。
- 依次類推,知道源ClassLoader。
- 源ClassLoader嘗試從其對應的類路徑下尋找class位元組碼檔案並載入。如果載入成功,則直接返回Class,如果載入失敗,源ClassLoader不會再委託其子類載入器,而是丟擲異常。
Android中的ClassLoader
Android的Dalvik/ART虛擬機器圖通標準Java的虛擬機器一樣,也是同樣需要載入class檔案到記憶體中來使用,但是在ClassLoader的載入細節上會有略微的差別。
Android應用打包成apk檔案時,class檔案會被打包成一個或者多個dex檔案,將一個apk檔案字尾改成.zip格式解壓後(也可以直接解壓,apk檔案本質是個zip檔案),裡面就有class.dex檔案,由於Android的65K問題,使用MultiDex就會生成多個dex檔案。
當Android系統安裝一個應用的時候,會針對不同平臺對Dex進行優化,這個過程由一個專門的工具來處理,叫DexOpt。DexOpt是在第一次載入Dex檔案的時候執行的,該過程會生成一個ODEX檔案,即Optimised Dex。執行ODEX的效率會比直接執行Dex檔案的效率高很多,加快App的啟動和響應。
更多可以參考:
http://www.mywiki.cn/hovercool/index.php/ART和Dalvik
https://www.jianshu.com/p/242abfb7eb7f
總之,Android的Dalvik/ART無法向JVM那樣直接載入class檔案的jar檔案中的class,需要通過dx工具來優化轉換成Dalvik byte code才行,只能那個通過dex或者包含dex的jar,apk檔案來載入(注意odex檔案字尾肯呢個會是.dex或.odex,也屬於dex檔案),因此Android中的ClassLoader工作就交給了BaseDexClassLoader來處理。
注:如果 jar 檔案包含有 dex 檔案,此時 jar 檔案也是可以用來載入的,不過實際載入的還是其中的 dex 檔案,不要弄混淆了。
BaseDexClassLoader及其子類
ClassLoader是一個抽象類,其具體實現的子類有BaseDexClassLoader和SecureClassLoader。
SercureClassLoader的子類是URLClassLoader,其只能用來載入jar檔案,在Android的Dalvik/ART上沒發使用。
BaseDexClassLoader的子類是PathClassLoader和DexClassLoader。
PathDexClassLoader
PathClassLoader在應用啟動時建立,從data/app/…安裝目錄下載入apk檔案。
其有兩個建構函式:遵循雙親委託模型
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
- dexPath:包含dex檔案的jar檔案或者apk檔案的路徑集,多個以檔案分隔符分隔,預設是“:”
- libraryPath:包含C/C++庫的路徑集,多個同樣以檔案分隔符分隔,可以為空。
PathClassLoader裡面除了這兩個構造方法以外就沒有其他的程式碼了,具體的實現都是在BaseClassLoader裡面,其dexParh比較受限制,一般是已經安裝應用的apk檔案路徑。
在Android中,App安裝到手機後,apk裡面的class.dex中的class均是PathClassLoader來載入的。
我們可以來看一下:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ClassLoader loader = MainActivity.class.getClassLoader();
while (loader != null) {
System.out.println(loader.toString());
loader = loader.getParent();
}
}
}
輸出:
I/System.out: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.asus1.rexiufu-aF8D1nfuAzND1R3FxXGMDA==/base.apk"],nativeLibraryDirectories=[/data/app/com.example.asus1.rexiufu-aF8D1nfuAzND1R3FxXGMDA==/lib/arm64, /system/lib64, /vendor/lib64, /product/lib64]]]
I/System.out: java.lang.BootClassLoader@cfe4423
/data/app/com.example.asus1.rexiufu-aF8D1nfuAzND1R3FxXGMDA==/base.apk就是示例應用安裝在上機上的位置。
BootClassLoader是PathClassLoader的父載入器,其在系統啟動時建立,在App啟動時會將該物件傳進來,具體的呼叫在com.android.internal.os.ZygoteInit
的 main()
方法中呼叫了 preload()
, 然後呼叫 preloadClasses()
方法,在該方法內部呼叫了 Class 的 forName() 方法:
Class.forName(line, true, null);
在forName方法內部獲取到BootClassLoader例項:
public static Class<?> forName(String className, boolean shouldInitialize,
ClassLoader classLoader) throws ClassNotFoundException {
if (classLoader == null) {
classLoader = BootClassLoader.getInstance();
}
// Catch an Exception thrown by the underlying native code. It wraps
// up everything inside a ClassNotFoundException, even if e.g. an
// Error occurred during initialization. This as a workaround for
// an ExceptionInInitializerError that's also wrapped. It is actually
// expected to be thrown. Maybe the same goes for other errors.
// Not wrapping up all the errors will break android though.
Class<?> result;
try {
result = classForName(className, shouldInitialize, classLoader);
} catch (ClassNotFoundException e) {
Throwable cause = e.getCause();
if (cause instanceof LinkageError) {
throw (LinkageError) cause;
}
throw e;
}
return result;
}
而PathClassLoader的例項化又是在哪裡進行呢?
其中:
- 在Zygotelnit中呼叫的是用來啟動相關的系統服務
- 在ApplicationLoaders中用來載入系統安裝過的apk,用來載入apk內的class,其呼叫時在LoadApk類中的
getClassLoader()
方法中呼叫的,得到的就是PathClassLoader:
mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip, lib,
mBaseClassLoader);
DexClassLoader
A class loader that loads classes from .jar and .apk files containing a classes.dex entry. This can be used to execute code not installed as part of an application.
對比PahtClassLoader只能載入已經安裝應用的dex或apl檔案,DexClassLoader則沒有此限制,可以從SD卡上載入包含class.dex的jar和apk檔案,這也就是外掛化和熱修復的基礎,在不需要安裝應用的情況下,完成需要使用的dex的載入。
DexClassLoader的原始碼裡面只有一個構造方法,這裡也是遵循雙親委託模型:
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
引數說明:
-
String dexPath : 包含 class.dex 的 apk、jar 檔案路徑 ,多個用檔案分隔符(預設是 :)分隔
-
String optimizedDirectory : 用來快取優化的 dex 檔案的路徑,即從 apk 或 jar 檔案中提取出來的 dex 檔案。該路徑不可以為空,且應該是應用私有的,有讀寫許可權的路徑(實際上也可以使用外部儲存空間,但是這樣的話就存在程式碼注入的風險),可以通過以下方式來建立一個這樣的路徑:
File dexOutputDir = context.getCodeCacheDir();
-
String libraryPath : 儲存 C/C++ 庫檔案的路徑集
-
ClassLoader parent : 父類載入器,遵從雙親委託模型
其實PathClassLoader和DexClassLoader都是隻是對BaseClassLoader的一層簡單的封裝,真正的實現都在BaseClassLoader。
BaseClassLoader原始碼分析
先看一下它的結構:
其中有個重要的欄位,pathList,其繼承ClassLoader實現的findClass(),findResource()均是基於pathList來實現的:
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
...
return c;
}
@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);
}
具體實現就在DexPathList裡面了,DexPathList的構造方法也比較簡單:
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
...
}
接手之前傳進來的包含dex的apk/jar/dex的路徑集、native庫的路徑集和快取優化的dex檔案的路徑,然後呼叫makePathElements()方法生成一個Element[ ] dexElements陣列,Element是DexPathList的一個巢狀類:
static class Element {
private final File dir;
private final boolean isDirectory;
private final File zip;
private final DexFile dexFile;
private ZipFile zipFile;
private boolean initialized;
}
那麼makePathElements()是如何生成Element陣列呢?
private static Element[] makePathElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions) {
List<Element> elements = new ArrayList<>();
// 遍歷所有的包含 dex 的檔案
for (File file : files) {
File zip = null;
File dir = new File("");
DexFile dex = null;
String path = file.getPath();
String name = file.getName();
// 判斷是不是 zip 型別
if (path.contains(zipSeparator)) {
String split[] = path.split(zipSeparator, 2);
zip = new File(split[0]);
dir = new File(split[1]);
} else if (file.isDirectory()) {
// 如果是資料夾,則直接新增 Element,這個一般是用來處理 native 庫和資原始檔
elements.add(new Element(file, true, null, null));
} else if (file.isFile()) {
// 直接是 .dex 檔案,而不是 zip/jar 檔案(apk 歸為 zip),則直接載入 dex 檔案
if (name.endsWith(DEX_SUFFIX)) {
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
} else {
// 如果是 zip/jar 檔案(apk 歸為 zip),則將 file 值賦給 zip 欄位,再載入 dex 檔案
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));
}
}
// list 轉為陣列
return elements.toArray(new Element[elements.size()]);
}
loadDexFile() 方法最終會呼叫 JNI 層的方法來讀取 dex 檔案 ,有興趣的可以閱讀 https://blog.csdn.net/nanzhiwen666/article/details/50515895 這篇文章深入瞭解。
接下來看findClass方法:其根據傳入的完整的類名來載入對應的class:
public Class findClass(String name, List<Throwable> suppressed) {
// 遍歷 dexElements 陣列,依次尋找對應的 class,一旦找到就終止遍歷
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;
}
這裡有關於修復實現的一個點,就是將補丁dex檔案放到dexElements陣列前面,這樣在載入class的時候,優先找到補丁包中的dex檔案,載入到class之後就不再尋找了,從而原來的apk檔案中同名的類就不會再使用,從而達到修復的目的。雖然說起來比較簡單,但是實現起來還是有很多細節需要注意。
至此,BaseDexClassLader 尋找 class 的路線就清晰了:
- 當傳入一個完整的類名,呼叫 BaseDexClassLader 的 findClass(String name) 方法
- BaseDexClassLader 的 findClass 方法會交給 DexPathList 的 findClass(String name, List suppressed 方法處理
- 在 DexPathList 方法的內部,會遍歷 dexFile ,通過 DexFile 的 dex.loadClassBinaryName(name, definingContext, suppressed) 來完成類的載入
需要注意的是,在專案中使用BaseDexLoader或者DexClassLoader去載入某個dex或者apk中的class的時候,是無法呼叫findClass()方法的,因為該方法是包訪問許可權,我們需要呼叫loadClass(),該方法其實是BaseDexClassLoader的父類ClassLoader內實現的:
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;
}
上面這段程式碼結合之前提到的雙親委託模型就很好理解了,先查詢當前的 ClassLoader 是否已經載入過,如果沒有就交給父 ClassLoader 去載入,如果父 ClassLoader 沒有找到,才呼叫當前 ClassLoader 來載入,此時就是呼叫上面分析的 findClass() 方法了。
ClassLoader使用
使用dx命令建立一個dex檔案,然後放到你的手機裡面,然後執行下面的程式碼:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
File dexFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
.getPath()+File.separator+"TestClass.dex");
if(!dexFile.exists()){
return;
}
DexClassLoader dexClassLoader = new DexClassLoader(dexFile.getAbsolutePath()
,getExternalCacheDir().getAbsolutePath(),null,getClassLoader());
try {
Class clazz = dexClassLoader.
loadClass("com.example.asus1.rexiufu.TestClass");
TestClass testClass = (TestClass)clazz.newInstance();
System.out.println(testClass.showToast());
}catch (ClassNotFoundException e){
e.printStackTrace();
}catch (IllegalAccessException e){
e.printStackTrace();
}catch (InstantiationException e){
e.printStackTrace();
}
}
});
輸出
I/System.out: Hello,Android!
轉載自:
https://jaeger.itscoder.com/android/2016/08/27/android-classloader.html