Android插件化之【類加載機制】

文章大綱

一. 類加載器

Android中的類加載器中主要包括三類BootClassLoader(繼承ClassLoader),PathClassLoader和DexClassLoader。后兩個繼承于BaseDexClassLoader。
1.BootClassLoader:主要用于加載系統的類,包括java和android系統的類庫。(比如TextView,Context,只要是系統的類都是由BootClassLoader加載完成)。

通過打印TextView.class.getClassLoader()即可驗證

2.PathClassLoader:主要用于加載我們應用程序內的類。路徑是固定的,只能加載
/data/app中的apk,無法指定解壓釋放dex的路徑,無法動態加載。對于我們的應用默認為PathClassLoader

通過打印getClassLoader()以及ClassLoader.getSystemClassLoader()即可驗證

3.DexClassLoader:可以用來加載任意路徑的zip,jar或者apk文件。可以實現動態加載。

簡單看一下這兩個類的源碼:

DexClassLoader類的源碼如下:

package dalvik.system;
public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
//但是api26以上 這個函數源碼如下 也就是第二個參數已經沒有影響
// * @param optimizedDirectory this parameter is deprecated and has no effect since API level 26.
   public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

PathClassLoader類的源碼如下:

package dalvik.system;
public class PathClassLoader extends BaseDexClassLoader {
   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 :需要被加載的jar/apk/dex 文件地址,可以多個,用File.pathSeparator分割。
optimizedDirectory:因為加載apk/jar的時候會被編譯器優化解壓出dex文件,這個路徑就是保存dex文件的。但在api26以上這個參數默認也為null。
libraryPath:庫lib文件的路徑
parent:給DexClassLoader指定父加載器

可以發現PathClassLoader和DexClassLoader源碼很簡單,只包含了一個構造函數,去調用父類BaseDexClassLoader。(所有的工作都應該是在BaseDexClassLoader里完成的了。)而這兩個加載器不同的是PathClassLoader的構造中少了optimizedDirectory這個參數,原因是PathClassLoader是加載/data/app中的apk,也就是系統中的apk,而這部分的apk都會解壓釋放dex到指定的目錄:/data/dalvik-cache中,這個操作由系統完成,不需要單獨傳入路徑,而DexClassLoader傳入,用來緩存需要加載的dex文件,并創建一個DexFile對象,如果為null,會直接使用dex文件原有路徑創建DexFile;這個參數已經棄用,自API26起無效;

二、 DexPathList

接下來具體看一下BaseDexClassLoader

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
    }
   @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;
    }
}

在BaseDexClassLoader里我們可以看到根據傳入的地址參數構造了一個DexPathList對象。從findClass方法可以看出來加載的類都是從pathList中查找。【findclass方法】是BaseDexClassLoader這個類的核心。那接下來看一下DexPathList類

private final Element[] dexElements;

public DexPathList(ClassLoader definingContext, String dexPath,
        String libraryPath, File optimizedDirectory) {
    ...
    this.definingContext = definingContext;
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions);
    ...
}

這里的重點是通過makeDexElements方法得到dexElements集合。而splitDexPath方法是將傳入的文件集合轉化為一個文件File合集,因為我們上面提到了dexPath可以是多個,用文件分隔符連接即可。

private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) {
    // 1.創建Element集合
    ArrayList<Element> elements = new ArrayList<Element>();
    // 2.遍歷所有dex文件(也可能是jar、apk或zip文件)
    for (File file : files) {
        ZipFile zip = null;
        DexFile dex = null;
        String name = file.getName();
        ...
        // 如果是dex文件
        if (name.endsWith(DEX_SUFFIX)) {
            dex = loadDexFile(file, optimizedDirectory);

        // 如果是apk、jar、zip文件(這部分在不同的Android版本中,處理方式有細微差別)
        } else {
            zip = file;
            dex = loadDexFile(file, optimizedDirectory);
        }
        ...
        // 3.將dex文件或壓縮文件包裝成Element對象,并添加到Element集合中
        if ((zip != null) || (dex != null)) {
            elements.add(new Element(file, false, zip, dex));
        }
    }
    // 4.將Element集合轉成Element數組返回
    return elements.toArray(new Element[elements.size()]);
}

總體來說,DexPathList的構造函數是將一個個的程序文件(可能是dex、apk、jar、zip)先通過loadDexFile轉變成dex,然后封裝成一個個Element對象,最后添加到Element集合中。BaseDexClassLoader的findclass方法也就是進一步,我們可以繼續看DexPathList的findClass()方法了:

public Class findClass(String name, List<Throwable> suppressed) {
    for (Element element : dexElements) {
        // 遍歷出一個dex文件
        DexFile dex = element.dexFile;

        if (dex != null) {
            // 在dex文件中查找類名與name相同的類
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

對Element數組進行遍歷(也就是對每一個dex文件遍歷),一個dex文件有很多類,通過調用DexFile的loadClassBinaryName找到與name相同的類返回,否則為null。正是這個特性!!我們可以把布丁dex作為Element數組的首個元素。這個就可以動態修復bug了!!【MultiDex方案以及由此衍生出的QQ空間熱更新方案都是通過改變dexElements數組的元素位置來實現的】

結合圖示
image.png

三、 雙親委派機制

如何理解Android ClassLoader的雙親代理/委派機制呢?ClassLoader的loadClass方法保證了雙親委派機制,那我們先看一下這個方法:

public Class<?> loadClass(String className) throws ClassNotFoundException {
       return loadClass(className, false);
}

protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(className);//1
        if (clazz == null) {
            ClassNotFoundException suppressed = null;
            try {
                clazz = parent.loadClass(className, false);//2
            } catch (ClassNotFoundException e) {
                suppressed = e;
            }

            if (clazz == null) {
                try {
                    clazz = findClass(className);//3
                } catch (ClassNotFoundException e) {
                    e.addSuppressed(suppressed);
                    throw e;
                }
            }
        }
    return clazz;

1. 首先調用findLoadedClass看自身是否加載過該name的類文件。
2. 如果沒有,調用父ClassLoader的loadClass看是否加載過類文件。
3. 如果父classLoader也沒有加載過,表明我們這個類從來沒有沒加載過,則調用自身的findClass方法去dex文件中查找這個類。(聯系我們上一節BaseDexClassLoader的findClass方法)

雙親委派模型的工作過程為:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的加載器都是如此,因此所有的類加載請求都會傳給頂層的啟動類加載器(BootClassLoader根加載器 加載器的頂端),只有當父加載器反饋自己無法完成該加載請求(該加載器的搜索范圍中沒有找到對應的類)時,子加載器才會嘗試自己去加載。

總結說:
1. 什么是雙親委派機制:

ClassLoader在加載一個字節碼時,首先會詢問 當前的
ClassLoader是否已經加載過此類,如果已經加載過就直接返回,不在重復的去
加載,如果沒有的話,會查詢它的parent是否已經加載過此類,如果加載過那
么就直接返回parent加載過的字節碼文件,如果整個繼承線路上都沒有加載過
此類,最后由子ClassLoader執行真正的加載。

2. 這樣做的好處:

如果一個類被位于樹中的任意ClassLoader節點加載過,就會緩存在內存里,那么在以后的整個系統的生命周期中這個類都不會在被重新加載,大大提高了加載類的效率。同樣還能類隔離,防止其他類冒充系統類。

3. 什么樣的類可以說是同一個類?

包名類名相同以及要被同一個類加載加載過。三個條件都滿足,才能說是同一個類。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容