插件化(二)-dex文件合併 一、加載插件類

Android知識總結

一、加載插件類

那我們如何使用類加載器去加載一個類呢?
非常的簡單,例如:我們有一個apk文件,路徑是 apkPath,然後裏面有個類 com.enjoy.plugin.Test,那麼我們可以通過如下方式去加載 Test 類:

  • 插件裏的類
public class Test {
    public static void print() {
        Log.e("--->", "print: 啓動插件方法");
    }
}
DexClassLoader dexClassLoader = newDexClassLoader(dexPath,context.getCacheDir().getAbsolutePath(), null,context.getClassLoader());
try {
    Class<?> clazz = dexClassLoader.loadClass("com.enjoy.plugin.Test");
    Method print = clazz.getMethod("print");
    print.invoke(null);
} catch (Exception e) {
    e.printStackTrace();
}

PathClassLoader dexClassLoader = new PathClassLoader("/sdcard/test.dex",null);
try {
    Class<?> clazz = dexClassLoader.loadClass("com.enjoy.plugin.Test");
    Method print = clazz.getMethod("print");
    print.invoke(null);
} catch (Exception e) {
    e.printStackTrace();
}

因爲我們需要將插件的 dex 文件加載到宿主裏面,所以我們接下來分析源碼,看 DexClassLoader 類加載器到底是怎麼加載一個 apk 的 dex 文件的。

通過查找發現,DexClassLoader 類中沒有 loadClass 方法,一路向上查找,最後在 ClassLoader 類中找到了改方法,源碼如下:(後續源碼如無標明,都是 API 26 Android 8.0

// /libcore/ojluni/src/main/java/java/lang/ClassLoader.java
//parent 是父類 BootClassLoader
private final ClassLoader parent;

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
    // 檢測這個類是否已經被加載 --> 1
    Class c = findLoadedClass(name);
    if (c == null) {
        long t0 = System.nanoTime();
        try {
            if (parent != null) {
                //如果parent不爲null,則調用parent的loadClass進行加載
                c = parent.loadClass(name, false);
            } else {
                //正常情況下不會走這兒,因爲 BootClassLoader 重寫了 loadClass 方法,結束了遞歸
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
        }
        if (c == null) {
            // 如果仍然找不到,就調用 findClass 去查找 --> 2
            long t1 = System.nanoTime();
            c = findClass(name);
        }
    }
    return c;
}
// -->1 檢測這個類是否已經被加載
protected final Class<?> findLoadedClass(String name) {
    ClassLoader loader;
    if (this == BootClassLoader.getInstance())
        loader = null;
    else
        loader = this;
    // 最後通過 native 方法實現查找
    return VMClassLoader.findLoadedClass(loader, name);
}



// -->2 加載器一般都會重寫這個方法,定義自己的加載規則
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}
// /libcore/libart/src/main/java/java/lang/VMClassLoader.java
native static Class findLoadedClass(ClassLoader cl, String name);

首先檢測這個類是否已經被加載了,如果已經加載了,直接獲取並返回。如果沒有被加載,parent 不爲 null,則調用parent的loadClass進行加載,依次遞歸,如果找到了或者加載了就返回,如果即沒找到也加載不了,才自己去加載。這個過程就是我們常說的 雙親委託機制。

根據前面的打印結果可以知道,BootClassLoader 是最後一個加載器,所以我們來看下它是如何結束向上遞歸查找
的。

class BootClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        return Class.classForName(name, false, null);
    }
    
    @Override
    protected Class<?> loadClass(String className, boolean resolve)throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(className);
        if (clazz == null) {
            clazz = findClass(className);
        }
        return clazz;
    }
}

我們發現 BootClassLoader 重寫了 findClass 和 loadClass 方法,並且在 loadClass 方法中,不再獲取 parent,從而結束了遞歸。

接着我們再來看下,在所有 parent 都沒加載成功的情況下,DexClassLoader 是如何加載的。通過查找我們發現在它的父類 BaseDexClassLoader 中,重寫了 findClass 方法。

// /libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) {
    super(parent);
    // 初始化 pathList
    this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    // 在 pathList 中查找指定的 Class
    Class c = pathList.findClass(name, suppressedExceptions);
    return c;
}

接着再來看 DexPathList 類中的 findClass 方法。

private Element[] dexElements;
public Class<?> findClass(String name, List<Throwable> suppressed) {
    //通過 Element 獲取 Class 對象
    for (Element element : dexElements) {
        Class<?> clazz = element.findClass(name, definingContext, suppressed);
        if (clazz != null) {
            return clazz;
        }
    }
    return null;
}

進入Element #findClass方法

private final DexFile dexFile;

public Class<?> findClass(String name, ClassLoader definingContext,
        List<Throwable> suppressed) {
    return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
            : null;
}

接下來看DexFile文件

//native 方法
private static native Class defineClassNative(String name, ClassLoader loader, Object cookie,  DexFile dexFile)

@UnsupportedAppUsage
public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
    return defineClass(name, loader, mCookie, this, suppressed);
}

private static Class defineClass(String name, ClassLoader loader, Object cookie,
                                 DexFile dexFile, List<Throwable> suppressed) {
    Class result = null;
    try {
                //進入 native 進行加載
        result = defineClassNative(name, loader, cookie, dexFile);
    } catch (NoClassDefFoundError e) {
        if (suppressed != null) {
            suppressed.add(e);
        }
    } catch (ClassNotFoundException e) {
        if (suppressed != null) {
            suppressed.add(e);
        }
    }
    return result;
}
  • 類加載時序圖


我們發現 Class 對象就是從 Element 中獲得的,而每一個 Element 就對應一個 DexFile 文件,這個 DexFile 文件就是我們的 dex 文件,因爲我們的 dex 文件可能有多個,所以這兒使用數組 Element[]。到這兒我們的思路就出來了,分爲以下幾步:

  • 1、創建插件的 DexClassLoader 類加載器,然後通過反射獲取插件的 dexElements 值。
  • 2、獲取宿主的 PathClassLoader 類加載器,然後通過反射獲取宿主的 dexElements 值。
  • 3、合併宿主的 dexElements 與 插件的 dexElements,生成新的 Element[]。
  • 4、最後通過反射將新的 Element[] 賦值給宿主的 dexElements 。

具體代碼如下:

public class LoadUtil {
    private final static String apkPath = "/sdcard/plugin-debug.apk";

    public static void loadClass(Context context) {

        /**
         * 宿主dexElements = 宿主dexElements + 插件dexElements
         *
         * 1.獲取宿主dexElements
         * 2.獲取插件dexElements
         * 3.合併兩個dexElements
         * 4.將新的dexElements 賦值到 宿主dexElements
         *
         * 目標:dexElements  -- DexPathList類的對象 -- BaseDexClassLoader的對象,類加載器
         *
         * 獲取的是宿主的類加載器  --- 反射 dexElements  宿主
         *
         * 獲取的是插件的類加載器  --- 反射 dexElements  插件
         */
        try {
            Class<?> clazz = Class.forName("dalvik.system.BaseDexClassLoader");
            Field pathListField = clazz.getDeclaredField("pathList");
            pathListField.setAccessible(true);

            Class<?> dexPathListClass = Class.forName("dalvik.system.DexPathList");
            Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
            dexElementsField.setAccessible(true);

            // 宿主的 類加載器
            ClassLoader pathClassLoader = context.getClassLoader();
            // DexPathList類的對象
            Object hostPathList = pathListField.get(pathClassLoader);
            // 宿主的 dexElements
            Object[] hostDexElements = (Object[]) dexElementsField.get(hostPathList);

            // 插件的 類加載器
            ClassLoader dexClassLoader = new DexClassLoader(apkPath, context.getCacheDir().getAbsolutePath(), //優化存放的路徑
                    null, pathClassLoader);
            // DexPathList類的對象
            Object pluginPathList = pathListField.get(dexClassLoader);
            // 插件的 dexElements
            Object[] pluginDexElements = (Object[]) dexElementsField.get(pluginPathList);

            // 宿主dexElements = 宿主dexElements + 插件dexElements
            // 創建一個新數組
            Object[] newDexElements = (Object[]) Array.newInstance(hostDexElements.getClass().getComponentType(),
                    hostDexElements.length + pluginDexElements.length);

            System.arraycopy(hostDexElements, 0, newDexElements,
                    0, hostDexElements.length);
            System.arraycopy(pluginDexElements, 0, newDexElements,
                    hostDexElements.length, pluginDexElements.length);

            // 賦值
            // hostDexElements = newDexElements
            dexElementsField.set(hostPathList, newDexElements);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • 注意:Android10 apk 放到 /Sdcard 中會報錯,放/data/data/.../cache 私有目錄中就可以了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章