Android動態加載基礎 ClassLoader工做機制

Last Edit: 2016-02-10java

基本信息


類加載器ClassLoader

早期使用過Eclipse等Java編寫的軟件的同窗可能比較熟悉,Eclipse能夠加載許多第三方的插件(或者叫擴展),這就是動態加載。這些插件大可能是一些Jar包,而使用插件其實就是動態加載Jar包裏的Class進行工做。這其實很是好理解,Java代碼都是寫在Class裏面的,程序運行在虛擬機上時,虛擬機須要把須要的Class加載進來才能建立實例對象並工做,而完成這一個加載工做的角色就是ClassLoader。github

對於Java程序來講,編寫程序就是編寫類,運行程序也就是運行類(編譯獲得的class文件),其中起到關鍵做用的就是類加載器ClassLoader。緩存

Android的Dalvik/ART虛擬機如同標準JAVA的JVM虛擬機同樣,在運行程序時首先須要將對應的類加載到內存中。所以,咱們能夠利用這一點,在程序運行時手動加載Class,從而達到代碼動態加載可執行文件的目的。Android的Dalvik/ART虛擬機雖然與標準Java的JVM虛擬機不同,ClassLoader具體的加載細節不同,可是工做機制是相似的,也就是說在Android中一樣能夠採用相似的動態加載插件的功能,只是在Android應用中動態加載一個插件的工做要比Eclipse加載一個插件複雜許多(這點後面在解釋說明)。安全

有幾個ClassLoader實例?

動態加載的基礎是ClassLoader,從名字也能夠看出,ClassLoader就是專門用來處理類加載工做的,因此這貨也叫類加載器,並且一個運行中的APP 不只只有一個類加載器cookie

其實,在Android系統啓動的時候會建立一個Boot類型的ClassLoader實例,用於加載一些系統Framework層級須要的類,咱們的Android應用裏也須要用到一些系統的類,因此APP啓動的時候也會把這個Boot類型的ClassLoader傳進來。app

此外,APP也有本身的類,這些類保存在APK的dex文件裏面,因此APP啓動的時候,也會建立一個本身的ClassLoader實例,用於加載本身dex文件中的類。下面咱們在項目裏驗證看看框架

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ClassLoader classLoader = getClassLoader();
        if (classLoader != null){
            Log.i(TAG, "[onCreate] classLoader " + i + " : " + classLoader.toString());
            while (classLoader.getParent()!=null){
                classLoader = classLoader.getParent();
                Log.i(TAG,"[onCreate] classLoader " + i + " : " + classLoader.toString());
            }
        }
    }

輸出結果爲ide

[onCreate] classLoader 1 : dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/me.kaede.anroidclassloadersample-1/base.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]]

[onCreate] classLoader 2 : java.lang.BootClassLoader@14af4e32

能夠看見有2個Classloader實例,一個是BootClassLoader(系統啓動的時候建立的),另外一個是PathClassLoader(應用啓動時建立的,用於加載「/data/app/me.kaede.anroidclassloadersample-1/base.apk」裏面的類)。由此也能夠看出,一個運行的Android應用至少有2個ClassLoader

建立本身ClassLoader實例

動態加載外部的dex文件的時候,咱們也可使用本身建立的ClassLoader實例來加載dex裏面的Class,不過ClassLoader的建立方式有點特殊,咱們先看看它的構造方法

/*
     * constructor for the BootClassLoader which needs parent to be null.
     */
    ClassLoader(ClassLoader parentLoader, boolean nullAllowed) {
        if (parentLoader == null && !nullAllowed) {
            throw new NullPointerException("parentLoader == null && !nullAllowed");
        }
        parent = parentLoader;
    }

建立一個ClassLoader實例的時候,須要使用一個現有的ClassLoader實例做爲新建立的實例的Parent。這樣一來,一個Android應用,甚至整個Android系統裏全部的ClassLoader實例都會被一棵樹關聯起來,這也是ClassLoader的 雙親代理模型(Parent-Delegation Model)的特色。

ClassLoader雙親代理模型加載類的特色和做用

JVM中ClassLoader經過defineClass方法加載jar裏面的Class,而Android中這個方法被棄用了。

@Deprecated
    protected final Class<?> defineClass(byte[] classRep, int offset, int length)
            throws ClassFormatError {
        throw new UnsupportedOperationException("can't load this type of class file");
    }

取而代之的是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);

        if (clazz == null) {
            ClassNotFoundException suppressed = null;
            try {
                clazz = parent.loadClass(className, false);
            } catch (ClassNotFoundException e) {
                suppressed = e;
            }

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

        return clazz;
    }

特色

從源碼中咱們也能夠看出,loadClass方法在加載一個類的實例的時候,

  1. 會先查詢當前ClassLoader實例是否加載過此類,有就返回;

  2. 若是沒有。查詢Parent是否已經加載過此類,若是已經加載過,就直接返回Parent加載的類;

  3. 若是繼承路線上的ClassLoader都沒有加載,才由Child執行類的加載工做;

這樣作有個明顯的特色,若是一個類被位於樹根的ClassLoader加載過,那麼在之後整個系統的生命週期內,這個類永遠不會被從新加載。

做用

首先是共享功能,一些Framework層級的類一旦被頂層的ClassLoader加載過就緩存在內存裏面,之後任何地方用到都不須要從新加載。

除此以外還有隔離功能,不一樣繼承路線上的ClassLoader加載的類確定不是同一個類,這樣的限制避免了用戶本身的代碼冒充核心類庫的類訪問核心類庫包可見成員的狀況。這也好理解,一些系統層級的類會在系統初始化的時候被加載,好比java.lang.String,若是在一個應用裏面可以簡單地用自定義的String類把這個系統的String類給替換掉,那將會有嚴重的安全問題。

使用ClassLoader一些須要注意的問題

咱們都知道,咱們能夠經過動態加載得到新的類,從而升級一些代碼邏輯,這裏有幾個問題要注意一下。

若是你但願經過動態加載的方式,加載一個新版本的dex文件,使用裏面的新類替換原有的舊類,從而修復原有類的BUG,那麼你必須保證在加載新類的時候,舊類尚未被加載,由於若是已經加載過舊類,那麼ClassLoader會一直優先使用舊類。

若是舊類老是優先於新類被加載,咱們也可使用一個與加載舊類的ClassLoader沒有樹的繼承關係的另外一個ClassLoader來加載新類,由於ClassLoader只會檢查其Parent有沒有加載過當前要加載的類,若是兩個ClassLoader沒有繼承關係,那麼舊類和新類都能被加載。

不過這樣一來又有另外一個問題了,在Java中,只有當兩個實例的類名、包名以及加載其的ClassLoader都相同,纔會被認爲是同一種類型。上面分別加載的新類和舊類,雖然包名和類名都徹底同樣,可是因爲加載的ClassLoader不一樣,因此並非同一種類型,在實際使用中可能會出現類型不符異常。

同一個Class = 相同的 ClassName + PackageName + ClassLoader

以上問題在採用動態加載功能的開發中容易出現,請注意。

DexClassLoader 和 PathClassLoader

在Android中,ClassLoader是一個抽象類,實際開發過程當中,咱們通常是使用其具體的子類DexClassLoader、PathClassLoader這些類加載器來加載類的,它們的不一樣之處是:

  • DexClassLoader能夠加載jar/apk/dex,能夠從SD卡中加載未安裝的apk;

  • PathClassLoader只能加載系統中已經安裝過的apk;

類加載器的初始化

平時開發的時候,使用DexClassLoader就夠用了,可是咱們不妨挖一下這二者具體細節上的區別。

// DexClassLoader.java
public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

// PathClassLoader.java
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);
    }
}

這二者只是簡單的對BaseDexClassLoader作了一下封裝,具體的實現仍是在父類裏。不過這裏也能夠看出,PathClassLoader的optimizedDirectory只能是null,進去BaseDexClassLoader看看這個參數是幹什麼的

public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.originalPath = dexPath;
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

這裏建立了一個DexPathList實例,進去看看

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

    private static Element[] makeDexElements(ArrayList<File> files,
            File optimizedDirectory) {
        ArrayList<Element> elements = new ArrayList<Element>();
        for (File file : files) {
            ZipFile zip = null;
            DexFile dex = null;
            String name = file.getName();
            if (name.endsWith(DEX_SUFFIX)) {
                dex = loadDexFile(file, optimizedDirectory);
            } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
                    || name.endsWith(ZIP_SUFFIX)) {
                zip = new ZipFile(file);
            }
            ……
            if ((zip != null) || (dex != null)) {
                elements.add(new Element(file, zip, dex));
            }
        }
        return elements.toArray(new Element[elements.size()]);
    }

    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);
        }
    }

    /**
     * Converts a dex/jar file path and an output directory to an
     * output file path for an associated optimized dex file.
     */
    private static String optimizedPathFor(File path,
            File optimizedDirectory) {
        String fileName = path.getName();
        if (!fileName.endsWith(DEX_SUFFIX)) {
            int lastDot = fileName.lastIndexOf(".");
            if (lastDot < 0) {
                fileName += DEX_SUFFIX;
            } else {
                StringBuilder sb = new StringBuilder(lastDot + 4);
                sb.append(fileName, 0, lastDot);
                sb.append(DEX_SUFFIX);
                fileName = sb.toString();
            }
        }
        File result = new File(optimizedDirectory, fileName);
        return result.getPath();
    }

看到這裏咱們明白了,optimizedDirectory是用來緩存咱們須要加載的dex文件的,並建立一個DexFile對象,若是它爲null,那麼會直接使用dex文件原有的路徑來建立DexFile
對象。

optimizedDirectory必須是一個內部存儲路徑,還記得咱們以前說過的,不管哪一種動態加載,加載的可執行文件必定要存放在內部存儲。DexClassLoader能夠指定本身的optimizedDirectory,因此它能夠加載外部的dex,由於這個dex會被複制到內部路徑的optimizedDirectory;而PathClassLoader沒有optimizedDirectory,因此它只能加載內部的dex,這些大都是存在系統中已經安裝過的apk裏面的。

加載類的過程

上面還只是建立了類加載器的實例,其中建立了一個DexFile實例,用來保存dex文件,咱們猜測這個實例就是用來加載類的。

Android中,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);
        if (clazz == null) {
            ClassNotFoundException suppressed = null;
            try {
                clazz = parent.loadClass(className, false);
            } catch (ClassNotFoundException e) {
                suppressed = e;
            }

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

loadClass方法調用了findClass方法,而BaseDexClassLoader重載了這個方法,獲得BaseDexClassLoader看看

@Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class clazz = pathList.findClass(name);
        if (clazz == null) {
            throw new ClassNotFoundException(name);
        }
        return clazz;
    }

結果仍是調用了DexPathList的findClass

public Class findClass(String name) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;
            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        return null;
    }

這裏遍歷了以前全部的DexFile實例,其實也就是遍歷了全部加載過的dex文件,再調用loadClassBinaryName方法一個個嘗試能不能加載想要的類,真是簡單粗暴

public Class loadClassBinaryName(String name, ClassLoader loader) {
        return defineClass(name, loader, mCookie);
    }
    private native static Class defineClass(String name, ClassLoader loader, int cookie);

看到這裏想必你們都明白了,loadClassBinaryName中調用了Native方法defineClass加載類。

至此,ClassLoader的建立和加載類的過程的完成了。有趣的是,標準JVM中,ClassLoader是用defineClass加載類的,而Android中defineClass被棄用了,改用了loadClass方法,並且加載類的過程也挪到了DexFile中,在DexFile中加載類的具體方法也叫defineClass,不知道是Google故意寫成這樣的仍是巧合。

自定義ClassLoader

平時進行動態加載開發的時候,使用DexClassLoader就夠了。但咱們也能夠建立本身的類去繼承ClassLoader,須要注意的是loadClass方法並非final類型的,因此咱們能夠重載loadClass方法並改寫類的加載邏輯。

經過前面咱們分析知道,ClassLoader雙親代理的實現很大一部分就是在loadClass方法裏,咱們能夠經過重寫loadClass方法避開雙親代理的框架,這樣一來就能夠在從新加載已經加載過的類,也能夠在加載類的時候注入一些代碼。這是一種Hack的開發方式,採用這種開發方式的程序穩定性可能比較差,可是卻能夠實現一些「黑科技」的功能。

Android程序比起通常Java程序在使用動態加載時麻煩在哪裏

經過上面的分析,咱們知道使用ClassLoader動態加載一個外部的類是很是容易的事情,因此很容易就能實現動態加載新的可執行代碼的功能,可是比起通常的Java程序,在Android程序中使用動態加載主要有兩個麻煩的問題:

  1. Android中許多組件類(如Activity、Service等)是須要在Manifest文件裏面註冊後才能工做的(系統會檢查該組件有沒有註冊),因此即便動態加載了一個新的組件類進來,沒有註冊的話仍是沒法工做;

  2. Res資源是Android開發中常常用到的,而Android是把這些資源用對應的R.id註冊好,運行時經過這些ID從Resource實例中獲取對應的資源。若是是運行時動態加載進來的新類,那類裏面用到R.id的地方將會拋出找不到資源或者用錯資源的異常,由於新類的資源ID根本和現有的Resource實例中保存的資源ID對不上;

說到底,拋開虛擬機的差異不說,一個Android程序和標準的Java程序最大的區別就在於他們的上下文環境(Context)不一樣。Android中,這個環境能夠給程序提供組件須要用到的功能,也能夠提供一些主題、Res等資源,其實上面說到的兩個問題均可以統一說是這個環境的問題,而如今的各類Android動態加載框架中,核心要解決的東西也正是「如何給外部的新類提供上下文環境」的問題。

相關文章
相關標籤/搜索