Android之ClassLoader類加載器(MultiDex、動態加載dex)

android中的類加載

先看個簡單的打印日誌,直接將代碼貼到Activity#onCreate()中即可:

protected void onCreate(Bundle savedInstanceState) {
    //...其他省略

    Log.e(TAG, "classLoader -> " + getClassLoader());
    String classPath = System.getProperty("java.class.path", ".");
    String librarySearchPath = System.getProperty("java.library.path", "");
    Log.e(TAG, "classPath -> " + classPath);
    Log.e(TAG, "librarySearchPath -> " + librarySearchPath);
}
//打印日誌  
07-11 17:42:39.299 3874-3874/com.tv189.dl E/MainActivity: classLoader -> dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.tv189.dl-1/base.apk"],nativeLibraryDirectories=[/data/app/com.tv189.dl-1/lib/arm64, /vendor/lib64, /system/lib64]]]
07-11 17:42:39.299 3874-3874/com.tv189.dl E/MainActivity: classPath -> .
07-11 17:42:39.299 3874-3874/com.tv189.dl E/MainActivity: librarySearchPath -> /vendor/lib64:/system/lib64

說明:
dalvik.system.PathClassLoader 爲android虛擬機類加載器
System.getProperty("java.class.path", ".")查看當前虛擬機中加載類的路徑
System.getProperty("java.library.path", "") 查看當前虛擬機中加載的動態鏈接庫的路徑
顯然這裏android-VM中是通過PathClassLoader來加載類的。

android中類加載器的類圖如下:
這裏寫圖片描述

涉及的類

  • PathClassLoader.java
    PathClassLoader作用於本地文件系統中的文件列表和目錄,而不能從網絡加載classes。

  • DexClassLoader.java
    從包含classes.dex的jar/apk文件中加載class,該類可以加載未打包到apk中的代碼。所以可以用來動態加載dex等文件中的classes。
    該加載器需要傳入一個應用私有的可寫入的目錄來緩存優化過的dex,如File dexOutputDir = context.getDir("dex", 0);

  • BaseDexClassLoader.java
    在android中使用該類加載系統classes和app中的classes。可以加載的文件類型爲包含classes的jar/apk文件,也支持多個文件同時加載,使用File.pathSeparator 把文件路徑連接即可並且android中默認爲冒號: ,如path/to/a.jar:path/to/b.apk。dex文件實際被加在到private DexPathList pathList,而findClass(name) 也是從pathList 中來尋找已被加載的class的。

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        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;
    }
  • DexPathList.java
    主要是類加載器把dex/resource保存在private final Element[] dexElements; 數組中的,每個Element 對應一個dex文件; 而BaseDexClassLoader#findClass 加載類時會從dexElements 中遍歷獲得,如果找到了Class就終止遍歷。
注意這一段說明

/**
 * A pair of lists of entries, associated with a {@code ClassLoader}.
 * One of the lists is a dex/resource path &mdash; typically referred
 * to as a "class path" &mdash; list, and the other names directories
 * containing native code libraries. Class path entries may be any of:
 * a {@code .jar} or {@code .zip} file containing an optional
 * top-level {@code classes.dex} file as well as arbitrary resources,
 * or a plain {@code .dex} file (with no possibility of associated
 * resources).
 *
 * <p>This class also contains methods to use these lists to look up
 * classes and resources.</p>
 */
/*package*/ final class DexPathList {
    private static final String DEX_SUFFIX = ".dex";

    /** class definition context */
    private final ClassLoader definingContext;

    /**
     * List of dex/resource (class path) elements.
     * Should be called pathElements, but the Facebook app uses reflection
     * to modify 'dexElements' (http://b/7726934).
     */
    private final Element[] dexElements;

    /** List of native library directories. */
    private final File[] nativeLibraryDirectories;

解決android中方法數超出問題

由於android系統的bug,一個dex文件中方法數最大值爲65536;所以隨着應用代碼的膨脹必然會導致程序dex中的方法數超過最大值的,要解決這個問題的方案就是把dex進行切分爲多個dex,但是android5.0之前dalvik虛擬機系統只加載一個dex,從android5.0開始系統使用art虛擬機已支持加載多個dex文件;google官方也出了補丁(使用multidex加載多個dex)來修復這個bug;所以如果app要適配android5.0以下的系統程序中就要使用MultiDex方案,這個補丁支持api4 ~ api20(android5.0以下)。

涉及的類

主要類在 android.support.multidex包中

  • MultiDex.java
    加載多個dex,主要過程:

    1. 刪除舊的secondarydex緩存 clearOldDexDir
    2. 嘗試加載已存在的secondarydexes,失敗則從新解壓apk然後加載secondarydex;
  • MultiDexExtractor .java
    解壓apk,並從中提取dexes

動態加載dex

參考QQ空間熱修復方案,在app啓動時把補丁dex加載到Element[] 數組的前端(參見classloader類圖);

測試時可以先修改某個類,然後把這個修改過的類轉爲jar,再把jar轉爲dex;
再把修改的內容改回去;
把下面代碼放入項目中去加載dex;

import android.content.Context;

import java.lang.reflect.Array;
import java.lang.reflect.Field;

import dalvik.system.BaseDexClassLoader;
import dalvik.system.DexClassLoader;

/**
 * 加載補丁dex文件,並插入系統dex數組中的第一個位置
 * <p>
 * Created by June on 2017/7/14.
 */

public class PatchDexLoader {

    private Context mContext;

    public PatchDexLoader(Context context) {
        this.mContext = context;
    }

    public void load(String path) {
        try {
            // 已加載的dex
            Object dexPathList = getField(BaseDexClassLoader.class, "pathList", mContext.getClassLoader());
            Object dexElements = getField(dexPathList.getClass(), "dexElements", dexPathList);

            // patchdex
            String dexOptDir = mContext.getDir("patchDex_optDir", 0).getAbsolutePath();
            DexClassLoader dcl = new DexClassLoader(path, dexOptDir, null, mContext.getClassLoader());
            Object patchDexPathList = getField(BaseDexClassLoader.class, "pathList", dcl);
            Object patchDexElements = getField(patchDexPathList.getClass(), "dexElements", patchDexPathList);

            // 將patchdex和已加載的dexes數組拼接連接
            Object concatDexElements = concatArray(patchDexElements, dexElements);

            // 重新給dexPathList#dexElements賦值
            setField(dexPathList.getClass(), "dexElements", dexPathList, concatDexElements);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }


    /**
     * @param cls       被訪問對象的class
     * @param fieldName 對象的成員變量名
     * @param object    被訪問對象
     * @return
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     */
    public Object getField(Class<?> cls, String fieldName, Object object) throws NoSuchFieldException, IllegalAccessException {
        Field field = cls.getDeclaredField(fieldName);
        field.setAccessible(true);
        return field.get(object);
    }


    /**
     * @param cls       被訪問對象的class
     * @param fieldName 對象的成員變量名
     * @param object    被訪問對象
     * @param value     賦值給成員變量
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     */
    public void setField(Class<?> cls, String fieldName, Object object, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field field = cls.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(object, value);
    }


    /**
     * 連接兩個數組(指定位置)
     *
     * @param left  連接後在新數組的左側
     * @param right 連接後在新數組的右側
     * @return
     */
    public Object concatArray(Object left, Object right) {
        int len1 = Array.getLength(left);
        int len2 = Array.getLength(right);
        int totalLen = len1 + len2;
        Object concatArray = Array.newInstance(left.getClass().getComponentType(), totalLen);
        for(int i = 0; i < len1; i++) {
            Array.set(concatArray, i, Array.get(left, i));
        }
        for(int j = 0; j < len2; j++) {
            Array.set(concatArray, len1 + j, Array.get(right, j));
        }
        return concatArray;
    }
}

refrence

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章