熱修復與插件化基礎——Java與Android的類加載器

1、java中的ClassLoader

一、類加載器

二、加載流程

  • Loading:類的信息從文件中獲取並載入到JVM的內存中。
  • Verifying:檢查讀入的結構是否符合JVM規範的描述。
  • Preparing:分配一個結構用來存儲類信息。
  • Resolving:把類的常量池中的全部符號引用變成直接引用。
  • Initializing:執行靜態初始化程序,把靜態變量初始化成指定的值。

2、Android中的ClassLoader

一、類加載器

Android中最主要的類加載器有以下4個:java

一個app必定會用到BootClassLoader、PathClassLoader這2個類加載器,可經過以下代碼進行驗證:android

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...

        ClassLoader classLoader = getClassLoader();
        if (classLoader != null) {
            Log.e("lqr", "classLoader = " + classLoader);
            while (classLoader.getParent() != null) {
                classLoader = classLoader.getParent();
                Log.e("lqr", "classLoader = " + classLoader);
            }
        }
    }
複製代碼

日誌輸出結果以下:c++

上面代碼中能夠經過上下文拿到當前類的類加載器(PathClassLoader),而後經過getParent()獲得父類加載器(BootClassLoader),這是因爲Android中的類加載器使用的是雙親委派模型。數組

二、特色及做用

雙親委派模型:緩存

在加載一個字節碼文件時,會詢問當前的classLoader是否已經加載過此字節碼文件。若是加載過,則直接返回,再也不重複加載。若是沒有加載過,則會詢問它的Parent是否已經加載過此字節碼文件,一樣的,若是已經加載過,就直接返回parent加載過的字節碼文件,而若是整個繼承線路上的classLoader都沒有加載過,才由child類加載器(即,當前的子classLoader)執行類的加載工做。安全

1)特色:

顯然,若是一個類被classLoader繼承線路上的任意一個加載過,那麼在之後整個系統的生命週期中,這個類都不會再被加載,大大提升了類的加載效率。cookie

2)做用:

  1. 類加載的共享功能

一些Framework層級的類一旦被頂層classLoader加載過,會緩存到內存中,之後在任何地方用到,都不會去從新加載。網絡

  1. 類加載的隔離功能

共同繼承線程上的classLoader加載的類,確定不是同一個類,這樣能夠避免某些開發者本身去寫一些代碼冒充核心類庫,來訪問核心類庫中可見的成員變量。如java.lang.String在應用程序啓動前就已經被系統加載好了,若是在一個應用中可以簡單的用自定義的String類把系統中的String類替換掉的話,會有嚴重的安全問題。app

驗證多個類是同一個類的成立條件:ide

  • 相同的className
  • 相同的packageName
  • 被相同的classLoader加載

三、ClassLoader源碼

經過閱讀ClassLoader的源碼來驗證雙親委派模型。

1)loadClass()

找到ClassLoader這個類中的loadClass()方法,它調用的是另外一個2個參數的重載loadClass()方法。

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}
複製代碼

找到最終這個真正的loadClass()方法,下面即是該方法的源碼:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    // First, check if the class has already been loaded
    Class<?> c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
                c = parent.loadClass(name, false);
            } else {
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
            // ClassNotFoundException thrown if class not found
            // from the non-null parent class loader
        }

        if (c == null) {
            // If still not found, then invoke findClass in order
            // to find the class.
            c = findClass(name);
        }
    }
    return c;
}
複製代碼

能夠看到,如前面所說,加載一個類時,會有以下3步:

  1. 檢查當前的classLoader是否已經加載琮這個class,有則直接返回,沒有則進行第2步。
  2. 調用父classLoader的loadClass()方法,檢查父classLoader是否有加載過這個class,有則直接返回,沒有就繼續檢查上上個父classLoader,直到頂層classLoader。
  3. 若是全部的父classLoader都沒有加載過這個class,則最終由當前classLoader調用findClass()方法,去dex文件中找出並加載這個class。

以上就是雙親委派模型的核心。在loadClass()中,調用了一個很重要的方法,那就是findClass(),去查找要加載的類。

2)findClass()

在ClassLoader中,findClass()是空實現,這說明具體的方法會在子類中去重寫實現。

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}
複製代碼

因而,找到其子類BaseDexClassLoader,發現,AS實際上看不到系統級源碼。

這種狀況,在本人以前的《熱修復——深刻淺出原理與實現》文章中也有所說起,能夠藉助第三方源碼網站上查看,如:

PathClassLoader和DexClassLoader是BaseDexClassLoader的子類,源碼不多,就先查閱這2個類,再去研讀BaseDexClassLoader。

四、BaseDexClassLoader源碼

1)DexClassLoader

/** * A class loader that loads classes from {@code .jar} and {@code .apk} files * containing a {@code classes.dex} entry. This can be used to execute code not * installed as part of an application. * ... */
public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
    super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}
複製代碼

DexClassLoader的構造函數:

  • dexPath:dex文件路徑
  • optimizedDirectory:dex文件解壓路徑(通常是app的data目錄)
  • libraryPath:加載dex文件時須要用到的庫的路徑
  • parent:父類加載器

再回過頭來看DexClassLoader類上的註釋,大概翻譯就是說,DexClassLoader能夠加載jar包和apk包內dex文件中的類,能夠被用來執行非安裝過的app中的代碼。

這句註釋實際上是很重要的,它就是騰訊Tinker這一類熱修復解決方案的核心。一句話:能夠加載任意路徑下的dex文件。

2)PathClassLoader

/** * Provides a simple {@link ClassLoader} implementation that operates on a list * of files and directories in the local file system, but does not attempt to * load classes from the network. Android uses this class for its system class * loader and for its application class loader(s). */
public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) {
        super(dexPath, null, libraryPath, parent);
    }
}
複製代碼

PathClassLoader的構造函數:

  • dexPath:dex文件路徑
  • libraryPath:加載dex文件時須要用到的庫的路徑
  • parent:父類加載器

相比DexClassLoader的構造函數,PathClassLoader的構造函數少了一個參數libraryPath,這也就致使了PathClassLoader只能加載已安裝應用內dex中的class,從類上的說明中也能夠了解到,只能加載本地應用中的類,不能加載網絡上的類。

一句話,PathClassLoader只能用於加載已安裝應用的dex文件。

3)BaseDexClassLoader

看完DexClassLoader和PathClassLoader,發現它們根本沒有對findClass()這個方法進行重寫,說明它們的findClass()方法確定在其父類BaseDexClassLoader中進行了統一實現處理。

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;
    
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, 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()方法,而這個DexPathList對象是在BaseDexClassLoader構造函數中進行實例化,並保存了幾個BaseDexClassLoader會用到的屬性,注意,DexPathList保存的optimizedDirectory可能爲空,到時走的是PathClassLoader的邏輯。因此,下面就來看DexPathList:

4)DexPathList

a.構造函數

final class DexPathList {
    private static final String DEX_SUFFIX = ".dex";
    
    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;

    public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {
        this.definingContext = definingContext;
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions);
    }
複製代碼
  • definingContext:就是在前面傳入的BaseDexClassLoader(app運行後,這個definingContext多是PathClassLoader或DexClassLoader)
  • dexElements:這個就是 dex文件 或 資源文件 組成的元素數據了,它是經過makeDexElements()方法建立出來的,

天然下面就得先了解下這個Element和makeDexElements()方法。

b.Element

static class Element {
    private final File file;
    private final boolean isDirectory;
    private final File zip;
    private final DexFile dexFile;
    private ZipFile zipFile;
    private boolean initialized;
    
    public Element(File file, boolean isDirectory, File zip, DexFile dexFile) {
        this.file = file;
        this.isDirectory = isDirectory;
        this.zip = zip;
        this.dexFile = dexFile;
    }
    ...
}
複製代碼

Element是PathList的靜態內部類,其中,DexFile dexFile這個屬性是最關鍵的。接下來是makeDexElements()方法:

private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
                                         ArrayList<IOException> suppressedExceptions) {
    ArrayList<Element> elements = new ArrayList<Element>();
    /* * Open all files and load the (direct or contained) dex files * up front. */
    for (File file : files) {
        File zip = null;
        DexFile dex = null;
        String name = file.getName();
        if (file.isDirectory()) {
            // We support directories for looking up resources.
            // This is only useful for running libcore tests.
            elements.add(new Element(file, true, null, null));
        } else if (file.isFile()){
            if (name.endsWith(DEX_SUFFIX)) {
                // Raw dex file (not inside a zip/jar).
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException ex) {
                    System.logE("Unable to load dex file: " + file, ex);
                }
            } else {
                zip = file;
                dex = loadDexFile(file, optimizedDirectory);
            }
        } else {
            System.logW("ClassLoader referenced unknown path: " + file);
        }
        if ((zip != null) || (dex != null)) {
            elements.add(new Element(file, false, zip, dex));
        }
    }
    return elements.toArray(new Element[elements.size()]);
}
複製代碼

它對files集合進行遍歷(這個files集合就是dexPath下全部的文件及目錄),來看該方法對文件是怎麼處理的:它不論是dex文件,或是壓縮包文件,都會調用到loadDexFile()方法:

private static DexFile loadDexFile(File file, File optimizedDirectory) throws IOException {
    if (optimizedDirectory == null) {
        return new DexFile(file);
    } else {
        String optimizedPath = optimizedPathFor(file, optimizedDirectory);
        return DexFile.loadDex(file.getPath(), optimizedPath, 0);
    }
}
複製代碼

因此,若是optimizedDirectory爲null,說明這是PathClassLoader的處理方式,直接將file封裝成DexFile對象返回;若是optimizedDirectory不爲null,說明這是DexClassLoader的處理方式,若file是dex文件就封裝成DexFile對象返回,若file是壓縮包,會先進行解壓,將其中的dex文件封裝成DexFile對象返回。反正,不論是哪一種方式,就終都是獲得dex文件對象,而且,在makeDexElements()方法的最後,添加進Element數組中。

c.findClass()

public Class findClass(String name, List<Throwable> suppressed) {
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;
        if (dex != null) {
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    return null;
}
複製代碼

終於到最後一個findClass()方法了,其實它就是遍歷dex文件數組(dexElements),獲得一個個的dex文件對象,調用其loadClassBinaryName()方法通用類名找到類,快接近真相了,下面就看看DexFile中究竟是怎麼經過類名找到類的,堅持~

5)DexFile

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

private static Class defineClass(String name, ClassLoader loader, long cookie, List<Throwable> suppressed) {
    Class result = null;
    result = defineClassNative(name, loader, cookie);
    return result;
}

private static native Class defineClassNative(String name, ClassLoader loader, long cookie) throws ClassNotFoundException, NoClassDefFoundError;
複製代碼

在DexFile這個類中,loadClassBinaryName()調用了defineClass(),最終調用的是defineClassNative()這個native方法,也就是說,類的加載最終是用c/c++的方式來進行處理的,由於是native方法,這裏就沒辦法繼續往下跟了,所以,其真實處理邏輯咱們就不得而知了。

可是,聯想到前面的《熱修復與插件化基礎——dex與class》文章中提到的dex頭文件中包含了該dex中全部class的信息,因此,咱們不妨能夠大膽猜測一下,其實defineClassNative()這個native方法應該就是經過讀取dex頭文件的方式找到並定義了class。

四、類加載流程

所謂一圖勝千言,經過上面一系列的方法跟蹤,及流程梳理,最終,獲得以下這張圖:

相關文章
相關標籤/搜索