熱修復與外掛化基礎——Java與Android的類載入器

GitLqr發表於2018-05-17

一、java中的ClassLoader

1、類載入器

熱修復與外掛化基礎——Java與Android的類載入器

2、載入流程

熱修復與外掛化基礎——Java與Android的類載入器

  • Loading:類的資訊從檔案中獲取並載入到JVM的記憶體中。
  • Verifying:檢查讀入的結構是否符合JVM規範的描述。
  • Preparing:分配一個結構用來儲存類資訊。
  • Resolving:把類的常量池中的所有符號引用變成直接引用。
  • Initializing:執行靜態初始化程式,把靜態變數初始化成指定的值。

二、Android中的ClassLoader

1、類載入器

Android中最主要的類載入器有如下4個:

熱修復與外掛化基礎——Java與Android的類載入器

一個app一定會用到BootClassLoader、PathClassLoader這2個類載入器,可通過如下程式碼進行驗證:

@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);
            }
        }
    }
複製程式碼

日誌輸出結果如下:

熱修復與外掛化基礎——Java與Android的類載入器

上面程式碼中可以通過上下文拿到當前類的類載入器(PathClassLoader),然後通過getParent()得到父類載入器(BootClassLoader),這是由於Android中的類載入器使用的是雙親委派模型。

2、特點及作用

雙親委派模型:

在載入一個位元組碼檔案時,會詢問當前的classLoader是否已經載入過此位元組碼檔案。如果載入過,則直接返回,不再重複載入。如果沒有載入過,則會詢問它的Parent是否已經載入過此位元組碼檔案,同樣的,如果已經載入過,就直接返回parent載入過的位元組碼檔案,而如果整個繼承線路上的classLoader都沒有載入過,才由child類載入器(即,當前的子classLoader)執行類的載入工作。

1)特點:

顯然,如果一個類被classLoader繼承線路上的任意一個載入過,那麼在以後整個系統的生命週期中,這個類都不會再被載入,大大提高了類的載入效率。

2)作用:

  1. 類載入的共享功能

一些Framework層級的類一旦被頂層classLoader載入過,會快取到記憶體中,以後在任何地方用到,都不會去重新載入。

  1. 類載入的隔離功能

共同繼承執行緒上的classLoader載入的類,肯定不是同一個類,這樣可以避免某些開發者自己去寫一些程式碼冒充核心類庫,來訪問核心類庫中可見的成員變數。如java.lang.String在應用程式啟動前就已經被系統載入好了,如果在一個應用中能夠簡單的用自定義的String類把系統中的String類替換掉的話,會有嚴重的安全問題。

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

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

3、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實際上看不到系統級原始碼。

熱修復與外掛化基礎——Java與Android的類載入器

這種情況,在本人之前的《熱修復——深入淺出原理與實現》文章中也有所提及,可以藉助第三方原始碼網站上檢視,如:

PathClassLoader和DexClassLoader是BaseDexClassLoader的子類,原始碼很少,就先查閱這2個類,再去研讀BaseDexClassLoader。

4、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。

4、類載入流程

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

熱修復與外掛化基礎——Java與Android的類載入器

歡迎關注微信公眾號:全棧行動

相關文章