1. 程式人生 > >Android熱修復入門:Android中的ClassLoader

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;
    }

當類載入器收到載入類或資源的請求時,通常都是先委託給父類載入器載入,也就是說只有當父類載入器找不到指定類或資源的時候,自身才會執行實際類載入過程:

  1. 源ClassLoader先判斷該Class是否已載入,如果已載入,則直接返回Class,如果沒有則委託給父類載入器
  2. 父類載入器判斷是否載入過該Class,如果已載入,則直接返回Class,如果沒有則委託給祖父類載入器。
  3. 依次類推,直到始祖類載入器(引用類載入器)。
  4. 始祖類載入器判斷是否載入過該Class,如果已載入,則直接返回Class,如果沒有則嘗試從其對應的類路徑下尋找class位元組碼檔案並載入。如果加入成功,則直接返回class,如果載入失敗,則委託給始祖類載入器的子類載入器。
  5. 始祖類載入器的子類載入器嘗試從其對應的類路徑下尋找class位元組碼檔案並載入。如果載入成功,則直接返回Class,如果載入失敗,則委託給始祖類載入器的孫類載入器。
  6. 依次類推,知道源ClassLoader。
  7. 源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.ZygoteInitmain() 方法中呼叫了 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