1. 程式人生 > >android動態載入ClassLoader機制

android動態載入ClassLoader機制

1, 概述

平時做開發有時會匯入第三方jar包, 在執行程式時首先需要將apk對應的類載入到記憶體中, 動態載入是指載入動態庫以及jar包等。

Android的Dalvik/ART虛擬機器如同標準JAVA的JVM虛擬機器一樣,在執行程式時首先需要將對應的類載入到記憶體中。

因此,可以利用這一點,在程式執行時手動載入Class,從而達到程式碼動態載入可執行檔案的目的。

Android的Dalvik/ART虛擬機器雖然與標準Java的JVM虛擬機器不一樣,ClassLoader具體的載入細節不一樣,

但是工作機制是類似的,也就是說在Android中同樣可以採用類似的動態載入外掛的功能。

ClassLoader例項

動態載入的基礎是ClassLoader,從名字也可以看出,ClassLoader就是專門用來處理類載入工作的,

所以也叫類載入器,而且一個執行中的APP 不僅只有一個類載入器。

其實,在Android系統啟動的時候會建立一個BootClassLoader型別的ClassLoader例項,

用於載入一些系統Framework層級需要的類,的Android應用裡也需要用到一些系統的類,

所以APP啟動的時候也會把這個Boot型別的ClassLoader傳進來。

此外,APP也有自己的類,這些類儲存在APK的dex檔案裡面,所以APP啟動的時候,

也會建立一個自己的ClassLoader例項,用於載入自己dex檔案中的類。

public class MainActivity extends Activity {
    private String TAG = “ClassLoader”;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ClassLoader classLoader = getClassLoader();
        if (classLoader != null){
            Log.d(TAG, " classLoader: " + classLoader.toString());
            while (classLoader.getParent()!=null){
                classLoader = classLoader.getParent();
                Log.d(TAG, " classLoader: " + classLoader.toString());
            }
        }
    }
}

會輸出2個ClassLoader,

dalvik.system.PathClassLoader。

java.lang.BootClassLoader。

由此也可以看出,一個執行的Android應用至少有2個ClassLoader。

2, 原理分析

2.1建立ClassLoader例項

動態載入外部的dex檔案的時候,也可以使用自己建立的ClassLoader例項來載入dex裡面的Class,

不過ClassLoader的建立方式有點特殊,先看看它的構造方法,

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-DelegationModel)的特點。

2.2雙親代理模型

JVM中ClassLoader通過defineClass方法載入jar裡面的Class,而Android中使用loadClass方法。ClassLoader的loadClass方法如下,

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一些需要注意的問題

可以通過動態載入獲得新的類,從而升級一些程式碼邏輯,這裡有幾個問題要注意一下。

1,如果希望通過動態載入的方式,載入一個新版本的dex檔案,使用裡面的新類替換原有的舊類,

從而修復原有類的BUG,那麼必須保證在載入新類的時候,舊類還沒有被載入,因為如果已經載入過舊類,那麼ClassLoader會一直優先使用舊類。

2,如果舊類總是優先於新類被載入,也可以使用一個與載入舊類的ClassLoader沒有樹的繼承關係的另一個ClassLoader來載入新類,

因為ClassLoader只會檢查其Parent有沒有載入過當前要載入的類,如果兩個ClassLoader沒有繼承關係,

那麼舊類和新類都能被載入。不過這樣一來又有另一個問題了,在Java中,只有當兩個例項的類名、包名以及載入其的ClassLoader都相同

,才會被認為是同一種類型。上面分別載入的新類和舊類,雖然包名和類名都完全一樣,但是由於載入的ClassLoader不同,

所以並不是同一種類型,在實際使用中可能會出現型別不符異常。

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

2.3 DexClassLoader 和 PathClassLoader

在Android中,ClassLoader是一個抽象類,實際開發過程中,一般是使用其具體的子類DexClassLoader、PathClassLoader這些類載入器來載入類的,不同之處是:

DexClassLoader可以載入jar/apk/dex,可以從SD卡中載入未安裝的apk;

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

這2個類都繼承於BaseDexClassLoader, BaseDexClassLoader繼承於ClassLoader。

DexClassLoader的構造方法如下,

public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }

PathClassLoader有2個構造方法,

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.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

在此,主要關注的是optimizedDirectory引數到底如何處理,主要的流程圖如下,


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

optimizedPathFor方法如下,

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裡面的。

2.4類載入過程

上面還只是建立了類載入器的例項,其中建立了一個DexFile例項,用來儲存dex檔案,這個例項應該就是用來載入類的。

 Android中,ClassLoader用loadClass方法來載入需要的類,流程圖如下,


DexPathList的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;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

遍歷了之前所有的DexFile例項,其實也就是遍歷了所有載入過的dex檔案,

再呼叫loadClassBinaryName方法逐個載入類,最後呼叫Native方法defineClassNative載入類。

2.5自定義ClassLoader

平時進行動態載入開發的時候,使用DexClassLoader就夠了。但也可以建立自己的類去繼承ClassLoader,

需要注意的是loadClass方法並不是final型別的,所以可以過載loadClass方法並改寫類的載入邏輯。

通過前面分析,ClassLoader雙親代理的實現很大一部分就是在loadClass方法裡,可以通過重寫loadClass方法避開雙親代理的框架,

這樣一來就可以在重新載入已經載入過的類,也可以在載入類的時候注入一些程式碼。這是一種Hack的開發方式,

採用這種開發方式的程式穩定性可能比較差,但是卻可以實現一些“黑科技”的功能。

Android程式比起一般Java程式在使用動態載入時容易出錯地方,使用ClassLoader動態載入一個外部的類是非常容易的事情,

所以很容易就能實現動態載入新的可執行程式碼的功能,但是比起一般的Java程式,在Android程式中使用動態載入主要有兩個麻煩的問題:

Android中許多元件類(如Activity、Service等)是需要在Manifest檔案裡面註冊後才能工作的(系統會檢查該元件有沒有註冊),

所以即使動態載入了一個新的元件類進來,沒有註冊的話還是無法工作;Res資源是Android開發中經常用到的,

而Android是把這些資源用對應的R.id註冊好,執行時通過這些ID從Resource例項中獲取對應的資源。

如果是執行時動態載入進來的新類,那類裡面用到R.id的地方將會丟擲找不到資源或者用錯資源的異常,

因為新類的資源ID根本和現有的Resource例項中儲存的資源ID對不上;

說到底,拋開虛擬機器的差別不說,一個Android程式和標準的Java程式最大的區別就在於他們的上下文環境(Context)不同。

Android中,這個環境可以給程式提供元件需要用到的功能,也可以提供一些主題、Res等資源,

其實上面說到的兩個問題都可以統一說是這個環境的問題,而現在的各種Android動態載入框架中,

核心要解決的東西也正是“如何給外部的新類提供上下文環境”的問題。